From 26455e9113fb97398b2c86273af64c42d218a71a Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 11 Jul 2022 14:09:32 -0700 Subject: [PATCH] Merge vscode source through 1.62 release (#19981) * Build breaks 1 * Build breaks * Build breaks * Build breaks * More build breaks * Build breaks (#2512) * Runtime breaks * Build breaks * Fix dialog location break * Update typescript * Fix ASAR break issue * Unit test breaks * Update distro * Fix breaks in ADO builds (#2513) * Bump to node 16 * Fix hygiene errors * Bump distro * Remove reference to node type * Delete vscode specific extension * Bump to node 16 in CI yaml * Skip integration tests in CI builds (while fixing) * yarn.lock update * Bump moment dependency in remote yarn * Fix drop-down chevron style * Bump to node 16 * Remove playwrite from ci.yaml * Skip building build scripts in hygine check --- .devcontainer/cache/before-cache.sh | 6 +- .devcontainer/cache/build-cache-image.sh | 8 +- .devcontainer/cache/cache-diff.sh | 7 +- .devcontainer/cache/cache.Dockerfile | 16 +- .devcontainer/cache/restore-diff.sh | 9 +- .devcontainer/prepare.sh | 11 +- .eslintignore | 4 +- .eslintrc.json | 49 +- .gitattributes | 2 +- .github/subscribers.json | 1 - .github/workflows/ci.yml | 17 +- .gitignore | 3 +- .vscode/notebooks/api.github-issues | 4 +- .vscode/notebooks/endgame.github-issues | 26 +- .../notebooks/grooming-delta.github-issues | 425 +- .vscode/notebooks/grooming.github-issues | 30 - .vscode/notebooks/inbox.github-issues | 24 +- .vscode/notebooks/my-endgame.github-issues | 10 +- .vscode/notebooks/my-work.github-issues | 18 +- .vscode/notebooks/verification.github-issues | 2 +- .vscode/settings.json | 7 +- .yarnrc | 1 + build/.cachesalt | 2 +- build/.moduleignore | 12 + .../common/computeNodeModulesCacheKey.js | 11 +- .../common/computeNodeModulesCacheKey.ts | 12 +- build/azure-pipelines/common/createAsset.js | 27 +- build/azure-pipelines/common/createAsset.ts | 27 +- .../common/installPlaywright.js | 2 +- .../common/installPlaywright.ts | 2 +- build/azure-pipelines/common/sign.js | 10 +- build/azure-pipelines/common/sign.ts | 9 +- .../darwin/product-build-darwin.yml | 5 +- .../darwin/sql-product-build-darwin.yml | 27 +- build/azure-pipelines/distro-build.yml | 2 +- .../docker/sql-product-build-docker.yml | 2 +- .../linux/continuous-build-linux.yml | 2 +- .../linux/product-build-alpine.yml | 52 +- .../linux/product-build-linux.yml | 7 +- .../linux/sql-product-build-linux.yml | 27 +- build/azure-pipelines/product-build.yml | 18 +- build/azure-pipelines/sdl-scan.yml | 7 +- build/azure-pipelines/sql-product-compile.yml | 2 +- build/azure-pipelines/upload-nlsmetadata.js | 88 + build/azure-pipelines/upload-nlsmetadata.ts | 107 + .../azure-pipelines/web/product-build-web.yml | 7 + .../web/sql-product-build-web.yml | 2 +- .../win32/product-build-win32.yml | 36 +- .../win32/sql-product-build-win32.yml | 28 +- .../win32/sql-product-test-win32.yml | 2 +- build/filters.js | 4 +- build/gulpfile.editor.js | 7 +- build/gulpfile.hygiene.js | 4 + build/gulpfile.reh.js | 246 +- build/gulpfile.vscode.js | 2 + build/gulpfile.vscode.web.js | 213 +- build/gulpfile.vscode.win32.js | 6 - build/lib/asar.ts | 2 +- build/lib/compilation.js | 10 +- build/lib/compilation.ts | 10 +- .../lib/eslint/vscode-dts-literal-or-types.js | 14 +- .../lib/eslint/vscode-dts-literal-or-types.ts | 14 +- build/lib/i18n.resources.json | 4 + build/lib/layersChecker.js | 3 +- build/lib/layersChecker.ts | 3 +- build/lib/monaco-api.js | 20 - build/lib/monaco-api.ts | 19 - build/lib/optimize.js | 13 +- build/lib/optimize.ts | 17 +- build/lib/typings/vinyl.d.ts | 25 +- build/lib/util.js | 19 +- build/lib/util.ts | 19 +- build/monaco/ThirdPartyNotices.txt | 35 +- build/monaco/monaco.d.ts.recipe | 4 +- build/monaco/package.json | 2 +- build/npm/preinstall.js | 2 +- build/package.json | 5 +- build/yarn.lock | 350 +- cglicenses.json | 16 - extensions/configuration-editing/package.json | 3 +- .../schemas/attachContainer.schema.json | 14 +- .../devContainer.schema.generated.json | 115 +- .../schemas/devContainer.schema.src.json | 33 +- .../src/settingsDocumentHelper.ts | 2 +- extensions/csharp/cgmanifest.json | 2 +- .../csharp/syntaxes/csharp.tmLanguage.json | 2 +- extensions/git/package.json | 27 +- extensions/git/package.nls.json | 5 + extensions/git/src/commands.ts | 21 +- extensions/git/src/fileSystemProvider.ts | 7 - extensions/git/src/git.ts | 2 +- extensions/git/src/repository.ts | 74 +- extensions/git/yarn.lock | 16 +- extensions/github-authentication/package.json | 2 +- .../src/common/keychain.ts | 38 - .../github-authentication/src/github.ts | 7 +- .../github-authentication/src/githubServer.ts | 54 +- extensions/github-authentication/yarn.lock | 8 +- extensions/github/src/pushErrorHandler.ts | 2 +- extensions/image-preview/media/main.js | 2 +- extensions/image-preview/package.json | 2 +- extensions/image-preview/src/preview.ts | 11 +- extensions/image-preview/yarn.lock | 8 +- .../javascript-language-configuration.json | 196 +- .../tags-language-configuration.json | 5 + .../client/src/jsonClient.ts | 5 +- .../client/src/node/jsonClientMain.ts | 11 +- .../client/src/requests.ts | 2 +- .../json-language-features/package.json | 2 +- .../server/package.json | 2 +- .../server/src/utils/runner.ts | 1 + .../json-language-features/server/yarn.lock | 8 +- extensions/json-language-features/yarn.lock | 8 +- .../language-configuration.json | 2 + .../snippets/markdown.code-snippets | 9 +- .../media/markdown.css | 4 +- .../notebook/index.ts | 59 +- .../notebook/tsconfig.json | 1 + .../markdown-language-features/package.json | 8 +- .../src/commands/openDocumentLink.ts | 133 +- .../src/commands/showPreview.ts | 2 +- .../src/extension.ts | 1 + .../src/features/documentLinkProvider.ts | 13 +- .../src/features/foldingProvider.ts | 16 +- .../src/features/preview.ts | 25 +- .../src/features/previewManager.ts | 4 +- .../src/features/smartSelect.ts | 14 +- .../src/features/workspaceSymbolProvider.ts | 2 +- .../src/markdownEngine.ts | 112 +- .../src/tableOfContentsProvider.ts | 4 + .../src/test/documentLink.test.ts | 11 + .../src/test/engine.test.ts | 2 +- .../src/util/openDocumentLink.ts | 161 + .../src/util/topmostLineMonitor.ts | 28 +- .../test-workspace/sub/foo.txt | 5 + .../markdown-language-features/yarn.lock | 49 +- extensions/markdown-math/notebook/katex.ts | 12 +- extensions/markdown-math/package.json | 3 +- extensions/markdown-math/yarn.lock | 7 +- .../extension-browser.webpack.config.js | 11 +- .../microsoft-authentication/package.json | 4 +- .../microsoft-authentication/src/AADHelper.ts | 249 +- .../microsoft-authentication/src/extension.ts | 2 - .../microsoft-authentication/src/keychain.ts | 47 +- .../microsoft-authentication/src/logger.ts | 6 +- extensions/microsoft-authentication/yarn.lock | 13 +- extensions/mssql/src/features.ts | 2 +- extensions/package.json | 2 +- extensions/python/package.json | 3 +- extensions/r/cgmanifest.json | 4 +- extensions/r/syntaxes/r.tmLanguage.json | 7 +- .../resource-deployment/src/test/stubs.ts | 11 +- .../ui/radioGroupLoadingComponentBuilder.ts | 2 +- extensions/search-result/images/icon.png | Bin 0 -> 2901 bytes extensions/search-result/package.json | 1 + extensions/search-result/src/extension.ts | 15 +- extensions/simple-browser/package.json | 5 +- .../simple-browser/preview-src/index.ts | 2 + extensions/simple-browser/src/extension.ts | 7 + .../src/simpleBrowserManager.ts | 25 +- .../simple-browser/src/simpleBrowserView.ts | 32 +- extensions/simple-browser/yarn.lock | 8 +- extensions/sql-migration/.eslintrc.json | 3 +- .../theme-seti/build/update-icon-theme.js | 156 +- extensions/theme-seti/cgmanifest.json | 2 +- extensions/theme-seti/icons/seti.woff | Bin 36672 -> 37032 bytes .../theme-seti/icons/vs-seti-icon-theme.json | 786 +-- .../themes/solarized-dark-color-theme.json | 5 + extensions/tsconfig.base.json | 5 +- .../vscode-test-resolver/src/extension.ts | 26 +- .../xml/xml.language-configuration.json | 2 + extensions/yaml/package.json | 3 +- extensions/yarn.lock | 14 +- package.json | 37 +- product.json | 3 +- remote/package.json | 26 +- remote/web/package.json | 12 +- remote/web/yarn.lock | 54 +- remote/yarn.lock | 231 +- resources/linux/debian/postinst.template | 5 +- resources/linux/snap/electron-launch | 2 +- resources/server/bin-dev/code-web.js | 87 + resources/server/bin-dev/code.cmd | 6 + resources/server/bin-dev/code.sh | 18 + resources/server/bin-dev/helpers/browser.cmd | 6 + resources/server/bin-dev/helpers/browser.sh | 18 + resources/server/bin-dev/server.bat | 43 + resources/server/bin-dev/server.sh | 28 + resources/server/bin/code.cmd | 4 + resources/server/bin/code.sh | 12 + resources/server/bin/helpers/browser.cmd | 4 + resources/server/bin/helpers/browser.sh | 12 + resources/server/bin/server.cmd | 24 + resources/server/bin/server.sh | 12 + resources/server/code-192.png | Bin 0 -> 2721 bytes resources/server/code-512.png | Bin 0 -> 2721 bytes resources/server/favicon.ico | Bin 0 -> 34494 bytes resources/server/manifest.json | 19 + .../server/test/test-remote-integration.bat | 79 + .../server/test/test-remote-integration.sh | 114 + .../server/test/test-web-integration.bat | 55 + resources/server/test/test-web-integration.sh | 36 + resources/server/web.bat | 24 + resources/server/web.sh | 26 + resources/web/code-web.js | 6 +- .../sample-notebook-provider/src/extension.ts | 2 +- .../src/sampleController.ts | 2 +- .../src/sampleSerializer.ts | 2 +- .../src/extension.ts | 2 +- scripts/test-integration.bat | 4 +- scripts/test-integration.sh | 4 +- src/bootstrap-window.js | 24 +- src/buildfile.js | 2 + src/main.js | 6 +- .../base/browser/ui/buttonMenu/buttonMenu.ts | 2 +- .../browser/ui/table/highPerf/tableWidget.ts | 2 +- .../plugins/additionalKeyBindings.plugin.ts | 2 +- .../ui/table/plugins/copyKeybind.plugin.ts | 2 +- .../ui/table/tableCellEditorFactory.ts | 5 +- .../query/browser/untitledQueryEditorInput.ts | 9 +- .../telemetry/common/adsTelemetryService.ts | 10 +- .../api/browser/mainThreadModelViewDialog.ts | 5 +- .../api/common/extHostRequireInterceptor.ts | 4 +- .../api/common/notebooks/notebookUtils.ts | 3 +- .../browser/actions/layoutActions.ts | 6 +- .../designer/designerIssuesTabPanelView.ts | 10 +- .../browser/designer/designerScriptEditor.ts | 9 +- .../browser/editData/editDataInput.ts | 3 +- .../tableDesigner/tableDesignerInput.ts | 4 +- .../modelComponents/diffeditor.component.ts | 9 +- .../modelComponents/editor.component.ts | 3 + .../modelComponents/listbox.component.ts | 2 +- .../browser/modelComponents/modelViewInput.ts | 3 +- .../modelComponents/queryTextEditor.ts | 9 +- .../browser/modelComponents/text.component.ts | 4 +- .../parts/editor/editorStatusModeSelect.ts | 8 +- .../common/editor/query/queryEditorInput.ts | 4 +- .../backup/browser/backup.component.ts | 2 +- .../connection/browser/connectionStatus.ts | 2 +- .../browser/dataExplorerExtensionPoint.ts | 3 +- .../browser/dataExplorerViewlet.ts | 2 +- .../test/browser/dataExplorerViewlet.test.ts | 29 +- .../editData/browser/editDataGridPanel.ts | 2 +- .../editData/browser/gridParentComponent.ts | 2 +- .../browser/extensions.contribution.ts | 4 +- .../extensions/browser/extensionsActions.ts | 11 +- .../browser/jobManagementView.ts | 2 +- .../browser/cellViews/code.component.ts | 2 +- .../notebook/browser/cellViews/interfaces.ts | 3 +- .../cellViews/markdownToolbar.component.ts | 10 +- .../browser/cellViews/textCell.component.ts | 16 +- .../browser/find/notebookFindModel.ts | 2 +- .../browser/models/diffNotebookInput.ts | 6 +- .../browser/models/notebookEditorFactory.ts | 9 +- .../notebook/browser/models/notebookInput.ts | 10 +- .../browser/models/notebookTextFileModel.ts | 4 +- .../notebook/browser/notebook.component.ts | 2 +- .../notebook/browser/notebook.contribution.ts | 20 +- .../notebook/browser/notebookEditor.ts | 4 +- .../notebookExplorerViewlet.ts | 2 +- .../notebookExplorer/notebookSearch.ts | 2 +- .../browser/markdownTextTransformer.test.ts | 13 +- .../browser/notebookExplorerViewlet.test.ts | 24 +- .../test/browser/notebookMarkdown.test.ts | 4 +- .../test/browser/outputProcessor.test.ts | 4 +- .../workbench/contrib/notebook/test/stubs.ts | 5 +- .../browser/profilerActions.contribution.ts | 8 +- .../profiler/browser/profilerEditor.ts | 2 +- .../browser/profilerResourceEditor.ts | 11 +- .../profiler/browser/profilerTableEditor.ts | 2 +- .../query/browser/fileQueryEditorInput.ts | 7 +- .../contrib/query/browser/flavorStatus.ts | 2 +- .../query/browser/query.contribution.ts | 42 +- .../contrib/query/browser/queryEditor.ts | 3 +- .../query/browser/queryEditorFactory.ts | 9 +- .../contrib/query/browser/statusBarItems.ts | 2 +- .../test/browser/queryInputFactory.test.ts | 6 +- .../contrib/queryPlan/browser/queryPlan.ts | 0 .../tasks/browser/tasks.contribution.ts | 8 +- .../contrib/tsgops/browser/tsgopsActions.ts | 16 +- .../welcome/page/browser/welcomePage.ts | 6 +- .../browser/urlBrowserDialog.ts | 1 + .../bootstrap/browser/bootstrapService.ts | 4 +- .../browser/connectionDialogWidget.ts | 7 +- .../browser/testConnectionDialogWidget.ts | 6 +- .../electron-browser/insightsUtils.test.ts | 10 +- .../common/languageAssociation.ts | 11 +- .../services/notebook/browser/interface.ts | 12 +- .../notebook/browser/notebookService.ts | 8 +- .../notebook/browser/notebookServiceImpl.ts | 14 +- .../queryEditorService.test.ts | 6 +- .../editor/editorStatusModeSelect.test.ts | 35 +- src/tsec.exemptions.json | 4 +- src/vs/base/browser/contextmenu.ts | 2 +- src/vs/base/browser/dom.ts | 80 +- src/vs/base/browser/dompurify/cgmanifest.json | 17 + src/vs/base/browser/dompurify/dompurify.d.ts | 104 + src/vs/base/browser/dompurify/dompurify.js | 1386 ++++ .../browser/dompurify/dompurify.license.txt | 377 ++ src/vs/base/browser/formattedTextRenderer.ts | 6 +- src/vs/base/browser/iframe.ts | 8 +- src/vs/base/browser/keyboardEvent.ts | 189 +- src/vs/base/browser/markdownRenderer.ts | 177 +- .../browser/ui/actionbar/actionViewItems.ts | 2 +- .../browser/ui/centered/centeredViewLayout.ts | 14 +- src/vs/base/browser/ui/checkbox/checkbox.ts | 8 +- .../browser/ui/codicons/codicon/codicon.css | 1 + .../browser/ui/codicons/codicon/codicon.ttf | Bin 66424 -> 68156 bytes .../browser/ui/contextview/contextview.ts | 1 + src/vs/base/browser/ui/dialog/dialog.css | 8 +- src/vs/base/browser/ui/dialog/dialog.ts | 1 - src/vs/base/browser/ui/dropdown/dropdown.ts | 2 +- .../ui/dropdown/dropdownActionViewItem.ts | 3 +- src/vs/base/browser/ui/findinput/findInput.ts | 2 + .../base/browser/ui/findinput/replaceInput.ts | 2 + src/vs/base/browser/ui/grid/grid.ts | 8 +- src/vs/base/browser/ui/grid/gridview.ts | 6 +- src/vs/base/browser/ui/hover/hover.css | 4 +- src/vs/base/browser/ui/hover/hoverWidget.ts | 2 +- .../browser/ui/iconLabel/iconHoverDelegate.ts | 8 +- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 4 +- .../browser/ui/iconLabel/iconLabelHover.ts | 251 +- .../base/browser/ui/iconLabel/iconlabel.css | 2 +- src/vs/base/browser/ui/inputbox/inputBox.ts | 64 +- .../ui/keybindingLabel/keybindingLabel.ts | 2 +- src/vs/base/browser/ui/list/list.ts | 1 + src/vs/base/browser/ui/list/listView.ts | 31 +- src/vs/base/browser/ui/list/listWidget.ts | 32 +- src/vs/base/browser/ui/menu/menu.ts | 52 +- src/vs/base/browser/ui/menu/menubar.ts | 316 +- .../browser/ui/scrollbar/scrollableElement.ts | 8 +- .../browser/ui/selectBox/selectBoxCustom.ts | 24 +- src/vs/base/browser/ui/splitview/paneview.ts | 2 + src/vs/base/browser/ui/toolbar/toolbar.ts | 2 +- src/vs/base/browser/ui/tree/abstractTree.ts | 6 +- src/vs/base/browser/ui/tree/indexTreeModel.ts | 23 +- src/vs/base/browser/ui/tree/objectTree.ts | 2 +- .../base/browser/ui/tree/objectTreeModel.ts | 4 +- src/vs/base/common/actions.ts | 10 +- src/vs/base/common/arrays.ts | 43 +- src/vs/base/common/async.ts | 110 +- src/vs/base/common/charCode.ts | 11 +- src/vs/base/common/codicons.ts | 20 +- src/vs/base/common/color.ts | 6 +- src/vs/base/common/event.ts | 27 + src/vs/base/common/fuzzyScorer.ts | 97 +- src/vs/base/common/hash.ts | 2 +- src/vs/base/common/htmlContent.ts | 10 +- src/vs/base/common/insane/cgmanifest.json | 17 - src/vs/base/common/insane/insane.d.ts | 17 - src/vs/base/common/insane/insane.js | 474 -- src/vs/base/common/insane/insane.license.txt | 20 - src/vs/base/common/keyCodes.ts | 1132 ++-- src/vs/base/common/keybindingLabels.ts | 8 +- src/vs/base/common/keybindingParser.ts | 4 +- src/vs/base/common/keybindings.ts | 281 + src/vs/base/common/lifecycle.ts | 9 + src/vs/base/common/map.ts | 332 +- src/vs/base/common/marked/cgmanifest.json | 4 +- src/vs/base/common/marked/marked.js | 5649 +++++++++-------- src/vs/base/common/mime.ts | 9 +- src/vs/base/common/path.ts | 24 +- src/vs/base/common/platform.ts | 1 + src/vs/base/common/process.ts | 12 +- src/vs/base/common/product.ts | 11 +- src/vs/base/common/resources.ts | 4 +- src/vs/base/common/scanCode.ts | 690 -- src/vs/base/common/strings.ts | 68 +- src/vs/base/common/uriIpc.ts | 2 + src/vs/base/common/worker/simpleWorker.ts | 338 +- src/vs/base/node/macAddress.ts | 1 + src/vs/base/node/pfs.ts | 11 +- src/vs/base/node/ports.ts | 10 +- src/vs/base/node/processes.ts | 29 + src/vs/base/node/watcher.ts | 60 + src/vs/base/parts/ipc/common/ipc.net.ts | 17 +- src/vs/base/parts/ipc/common/ipc.ts | 21 +- .../base/parts/ipc/electron-sandbox/ipc.mp.ts | 35 + src/vs/base/parts/ipc/node/ipc.cp.ts | 9 +- src/vs/base/parts/ipc/node/ipc.net.ts | 15 +- ...c.cp.test.ts => ipc.cp.integrationTest.ts} | 0 .../base/parts/ipc/test/node/ipc.net.test.ts | 271 +- .../quickinput/browser/media/quickInput.css | 4 +- .../parts/quickinput/browser/quickInput.ts | 56 +- .../quickinput/browser/quickInputList.ts | 10 +- .../parts/quickinput/common/quickInput.ts | 4 +- .../parts/sandbox/electron-browser/preload.js | 28 +- .../parts/sandbox/electron-sandbox/globals.ts | 13 +- src/vs/base/parts/storage/common/storage.ts | 6 + .../parts/storage/test/node/storage.test.ts | 254 +- .../base/parts/tree/browser/treeDefaults.ts | 7 +- .../test/browser/markdownRenderer.test.ts | 181 +- .../browser/ui/tree/indexTreeModel.test.ts | 34 + src/vs/base/test/common/async.test.ts | 38 + src/vs/base/test/common/codicon.test.ts | 28 - src/vs/base/test/common/event.test.ts | 25 +- src/vs/base/test/common/fuzzyScorer.test.ts | 53 +- src/vs/base/test/common/keyCodes.test.ts | 50 +- src/vs/base/test/common/map.test.ts | 250 +- src/vs/base/test/common/resources.test.ts | 9 +- .../test/common}/timeTravelScheduler.ts | 34 +- src/vs/base/test/common/utils.ts | 7 +- ...s.test.ts => processes.integrationTest.ts} | 0 .../quickinput/browser/quickinput.test.ts | 117 +- src/vs/base/worker/defaultWorkerFactory.ts | 4 +- src/vs/base/worker/workerMain.ts | 19 +- src/vs/code/browser/workbench/callback.html | 5 +- .../code/browser/workbench/workbench-dev.html | 6 + src/vs/code/browser/workbench/workbench.html | 8 +- src/vs/code/browser/workbench/workbench.ts | 24 +- .../sharedProcess/contrib/codeCacheCleaner.ts | 4 +- .../contrib/languagePackCachedDataCleaner.ts | 8 +- .../sharedProcess/contrib/logsDataCleaner.ts | 4 +- .../contrib/storageDataCleaner.ts | 4 +- .../sharedProcess/sharedProcessMain.ts | 125 +- src/vs/code/electron-main/app.ts | 66 +- .../electron-sandbox/workbench/workbench.html | 2 +- src/vs/code/node/cli.ts | 138 +- src/vs/code/node/cliProcessMain.ts | 25 +- src/vs/css.js | 8 +- .../editor/browser/config/charWidthReader.ts | 10 +- src/vs/editor/browser/config/configuration.ts | 34 +- .../editor/browser/controller/coreCommands.ts | 24 +- .../editor/browser/controller/mouseHandler.ts | 11 +- .../browser/controller/textAreaHandler.ts | 3 + .../browser/controller/textAreaInput.ts | 15 - .../editor/browser/core/markdownRenderer.ts | 44 +- src/vs/editor/browser/editorBrowser.ts | 81 +- src/vs/editor/browser/editorExtensions.ts | 12 +- .../browser/services/codeEditorServiceImpl.ts | 3 +- .../browser/view/domLineBreaksComputer.ts | 27 +- .../viewParts/indentGuides/indentGuides.css | 10 +- .../viewParts/indentGuides/indentGuides.ts | 213 +- .../browser/viewParts/lines/rangeUtil.ts | 49 +- .../browser/viewParts/lines/viewLine.ts | 54 +- .../browser/viewParts/lines/viewLines.ts | 16 +- .../browser/viewParts/minimap/minimap.ts | 376 +- .../viewParts/minimap/minimapCharRenderer.ts | 17 +- .../overviewRuler/decorationsOverviewRuler.ts | 21 +- .../browser/viewParts/viewZones/viewZones.ts | 14 +- .../editor/browser/widget/codeEditorWidget.ts | 14 +- .../editor/browser/widget/diffEditorWidget.ts | 243 +- src/vs/editor/browser/widget/diffNavigator.ts | 29 +- src/vs/editor/browser/widget/diffReview.ts | 20 +- .../widget/embeddedCodeEditorWidget.ts | 4 +- .../editor/browser/widget/inlineDiffMargin.ts | 30 +- .../editor/common/commands/replaceCommand.ts | 25 +- .../common/config/commonEditorConfig.ts | 56 + src/vs/editor/common/config/editorOptions.ts | 323 +- src/vs/editor/common/config/fontInfo.ts | 25 +- src/vs/editor/common/controller/cursor.ts | 44 +- .../editor/common/controller/cursorColumns.ts | 219 + .../editor/common/controller/cursorCommon.ts | 285 +- .../controller/cursorDeleteOperations.ts | 7 +- .../common/controller/cursorMoveCommands.ts | 14 +- .../common/controller/cursorMoveOperations.ts | 46 +- .../common/controller/cursorTypeOperations.ts | 4 +- src/vs/editor/common/controller/oneCursor.ts | 7 +- src/vs/editor/common/core/lineTokens.ts | 17 +- src/vs/editor/common/core/rgba.ts | 2 +- src/vs/editor/common/core/selection.ts | 18 + src/vs/editor/common/model.ts | 125 +- .../common/model/bracketPairColorizer/ast.ts | 480 -- .../bracketPairColorizer.ts | 300 - .../model/bracketPairColorizer/brackets.ts | 120 - .../bracketPairColorizer/concat23Trees.ts | 92 - .../common/model/bracketPairs/bracketPairs.ts | 110 + .../model/bracketPairs/bracketPairsImpl.ts | 762 +++ .../bracketPairs/bracketPairsTree/ast.ts | 684 ++ .../beforeEditPositionMapper.ts | 0 .../bracketPairsTree/bracketPairsTree.ts | 231 + .../bracketPairs/bracketPairsTree/brackets.ts | 140 + .../bracketPairsTree/concat23Trees.ts | 196 + .../bracketPairsTree}/length.ts | 2 +- .../bracketPairsTree}/nodeReader.ts | 35 +- .../bracketPairsTree}/parser.ts | 55 +- .../bracketPairsTree}/smallImmutableSet.ts | 32 +- .../bracketPairsTree}/tokenizer.ts | 55 +- ...colorizedBracketPairsDecorationProvider.ts | 115 + .../editor/common/model/decorationProvider.ts | 4 +- src/vs/editor/common/model/mirrorTextModel.ts | 2 +- .../model/pieceTreeTextBuffer/rbTreeBase.ts | 33 +- src/vs/editor/common/model/textModel.ts | 955 +-- src/vs/editor/common/model/textModelEvents.ts | 2 +- src/vs/editor/common/model/textModelSearch.ts | 2 +- src/vs/editor/common/model/textModelTokens.ts | 49 +- src/vs/editor/common/model/tokensStore.ts | 26 +- src/vs/editor/common/modes.ts | 58 +- src/vs/editor/common/modes/abstractMode.ts | 23 - .../common/modes/languageConfiguration.ts | 16 +- .../modes/languageConfigurationRegistry.ts | 566 +- .../common/modes/languageFeatureRegistry.ts | 4 +- src/vs/editor/common/modes/modesRegistry.ts | 5 +- src/vs/editor/common/modes/nullMode.ts | 8 +- src/vs/editor/common/modes/supports.ts | 12 +- .../common/modes/supports/characterPair.ts | 25 +- .../common/modes/supports/richEditBrackets.ts | 162 +- .../common/modes/textToHtmlTokenizer.ts | 43 +- .../common/modes/tokenization/typescript.ts | 304 - .../common/services/editorWorkerService.ts | 1 - .../services/editorWorkerServiceImpl.ts | 28 +- .../editor/common/services/getIconClasses.ts | 4 +- .../common/services/getSemanticTokens.ts | 188 +- .../common/services/languagesRegistry.ts | 147 +- .../services/markerDecorationsServiceImpl.ts | 30 +- src/vs/editor/common/services/modeService.ts | 18 +- .../editor/common/services/modeServiceImpl.ts | 107 +- .../common/services/modelServiceImpl.ts | 97 +- .../services/semanticTokensProviderStyling.ts | 25 +- .../textResourceConfigurationServiceImpl.ts | 2 +- .../common/standalone/standaloneEnums.ts | 320 +- .../editor/common/view/editorColorRegistry.ts | 28 +- src/vs/editor/common/view/renderingContext.ts | 40 +- .../common/viewModel/prefixSumComputer.ts | 131 +- .../common/viewModel/splitLinesCollection.ts | 575 +- src/vs/editor/common/viewModel/viewModel.ts | 40 +- .../viewModel/viewModelEventDispatcher.ts | 18 + .../editor/common/viewModel/viewModelImpl.ts | 103 +- .../contrib/anchorSelect/anchorSelect.ts | 30 +- .../bracketMatching/bracketMatching.ts | 42 +- .../test/bracketMatching.test.ts | 22 +- .../caretOperations/caretOperations.ts | 4 +- .../contrib/caretOperations/transpose.ts | 8 +- src/vs/editor/contrib/clipboard/clipboard.ts | 29 +- .../editor/contrib/codeAction/codeAction.ts | 6 +- .../contrib/codeAction/codeActionCommands.ts | 12 +- .../codeAction/codeActionContributions.ts | 2 +- .../contrib/codeAction/codeActionMenu.ts | 4 +- .../editor/contrib/codeAction/codeActionUi.ts | 1 + .../contrib/codeAction/lightBulbWidget.ts | 12 +- .../codeAction/test/codeAction.test.ts | 8 +- .../test/codeActionKeybindingResolver.test.ts | 13 +- .../codeAction/test/codeActionModel.test.ts | 14 +- src/vs/editor/contrib/codeAction/types.ts | 2 +- .../editor/contrib/codelens/codeLensCache.ts | 16 +- src/vs/editor/contrib/codelens/codelens.ts | 6 +- .../contrib/codelens/codelensController.ts | 33 +- .../editor/contrib/codelens/codelensWidget.ts | 15 +- .../contrib/colorPicker/colorContributions.ts | 8 +- .../contrib/colorPicker/colorDetector.ts | 10 +- .../contrib/colorPicker/colorPicker.css | 12 +- .../contrib/colorPicker/colorPickerWidget.ts | 7 +- .../contrib/comment/blockCommentCommand.ts | 4 +- src/vs/editor/contrib/comment/comment.ts | 16 +- .../contrib/comment/lineCommentCommand.ts | 4 +- .../comment/test/blockCommentCommand.test.ts | 6 +- .../comment/test/lineCommentCommand.test.ts | 133 +- .../editor/contrib/contextmenu/contextmenu.ts | 17 +- .../editor/contrib/cursorUndo/cursorUndo.ts | 6 +- .../cursorUndo/test/cursorUndo.test.ts | 6 +- src/vs/editor/contrib/dnd/dnd.ts | 22 +- .../editor/contrib/dnd/dragAndDropCommand.ts | 4 +- .../documentSymbols/documentSymbols.ts | 4 +- .../contrib/documentSymbols/outlineModel.ts | 6 +- .../documentSymbols/test/outlineModel.test.ts | 14 +- src/vs/editor/contrib/find/findController.ts | 26 +- src/vs/editor/contrib/find/findDecorations.ts | 5 +- src/vs/editor/contrib/find/findModel.ts | 32 +- .../editor/contrib/find/findOptionsWidget.ts | 2 +- src/vs/editor/contrib/find/findWidget.css | 1 + src/vs/editor/contrib/find/findWidget.ts | 67 +- .../editor/contrib/find/replaceAllCommand.ts | 2 +- .../contrib/find/test/findModel.test.ts | 13 +- .../contrib/find/test/replacePattern.test.ts | 2 +- src/vs/editor/contrib/folding/folding.ts | 102 +- .../contrib/folding/foldingDecorations.ts | 6 +- src/vs/editor/contrib/folding/foldingModel.ts | 20 +- .../contrib/folding/hiddenRangeModel.ts | 10 +- .../contrib/folding/indentRangeProvider.ts | 14 +- .../folding/intializingRangeProvider.ts | 6 +- .../contrib/folding/syntaxRangeProvider.ts | 10 +- .../contrib/folding/test/foldingModel.test.ts | 21 +- .../folding/test/foldingRanges.test.ts | 6 +- .../folding/test/hiddenRangeModel.test.ts | 12 +- .../contrib/folding/test/indentFold.test.ts | 2 + .../folding/test/indentRangeProvider.test.ts | 4 +- .../contrib/folding/test/syntaxFold.test.ts | 10 +- src/vs/editor/contrib/fontZoom/fontZoom.ts | 4 +- src/vs/editor/contrib/format/format.ts | 55 +- src/vs/editor/contrib/format/formatActions.ts | 18 +- src/vs/editor/contrib/gotoError/gotoError.ts | 28 +- .../contrib/gotoError/gotoErrorWidget.ts | 71 +- .../gotoError/markerNavigationService.ts | 45 +- .../editor/contrib/gotoSymbol/goToCommands.ts | 139 +- .../editor/contrib/gotoSymbol/goToSymbol.ts | 2 +- .../gotoSymbol/link/clickLinkGesture.ts | 8 +- .../link/goToDefinitionAtPosition.ts | 47 +- .../gotoSymbol/peek/referencesController.ts | 44 +- .../contrib/gotoSymbol/peek/referencesTree.ts | 34 +- .../gotoSymbol/peek/referencesWidget.ts | 28 +- .../contrib/gotoSymbol/referencesModel.ts | 28 +- .../contrib/gotoSymbol/symbolNavigation.ts | 22 +- .../gotoSymbol/test/referencesModel.test.ts | 2 +- .../contrib/hover/colorHoverParticipant.ts | 15 +- src/vs/editor/contrib/hover/hover.ts | 18 +- src/vs/editor/contrib/hover/hoverOperation.ts | 2 +- src/vs/editor/contrib/hover/hoverTypes.ts | 2 +- src/vs/editor/contrib/hover/hoverWidgets.ts | 103 - .../contrib/hover/markdownHoverParticipant.ts | 35 +- .../contrib/hover/markerHoverParticipant.ts | 36 +- .../editor/contrib/hover/modesContentHover.ts | 63 +- .../editor/contrib/hover/modesGlyphHover.ts | 101 +- .../contrib/inPlaceReplace/inPlaceReplace.ts | 22 +- .../inPlaceReplace/inPlaceReplaceCommand.ts | 4 +- .../editor/contrib/indentation/indentation.ts | 24 +- .../inlayHints/inlayHintsController.ts | 308 +- .../contrib/inlineCompletions/ghostText.css | 8 + .../contrib/inlineCompletions/ghostText.ts | 6 +- .../inlineCompletions/ghostTextController.ts | 45 +- .../inlineCompletions/ghostTextModel.ts | 51 +- .../inlineCompletions/ghostTextWidget.ts | 64 +- .../inlineCompletionToGhostText.ts | 214 + .../inlineCompletionsHoverParticipant.ts | 24 +- .../inlineCompletionsModel.ts | 244 +- .../suggestWidgetAdapterModel.ts | 231 - .../suggestWidgetInlineCompletionProvider.ts | 236 + .../suggestWidgetPreviewModel.ts | 163 + .../test/inlineCompletionsProvider.test.ts | 121 +- .../test/suggestWidgetModel.test.ts | 69 +- .../contrib/inlineCompletions/test/utils.ts | 24 +- .../editor/contrib/inlineCompletions/utils.ts | 20 + .../linesOperations/copyLinesCommand.ts | 2 +- .../linesOperations/linesOperations.ts | 171 +- .../linesOperations/moveLinesCommand.ts | 6 +- .../linesOperations/sortLinesCommand.ts | 2 +- .../test/copyLinesCommand.test.ts | 4 +- .../test/linesOperations.test.ts | 78 +- .../test/moveLinesCommand.test.ts | 21 +- .../contrib/linkedEditing/linkedEditing.ts | 44 +- .../linkedEditing/test/linkedEditing.test..ts | 20 +- src/vs/editor/contrib/links/getLinks.ts | 8 +- src/vs/editor/contrib/links/links.ts | 18 +- .../contrib/message/messageController.ts | 20 +- .../editor/contrib/multicursor/multicursor.ts | 42 +- .../multicursor/test/multicursor.test.ts | 25 + .../contrib/parameterHints/parameterHints.css | 1 - .../contrib/parameterHints/parameterHints.ts | 20 +- .../parameterHints/parameterHintsModel.ts | 2 +- .../parameterHints/parameterHintsWidget.ts | 27 +- .../parameterHints/provideSignatureHelp.ts | 10 +- .../test/parameterHintsModel.test.ts | 6 +- .../contrib/peekView/media/peekViewWidget.css | 5 +- src/vs/editor/contrib/peekView/peekView.ts | 39 +- .../quickAccess/commandsQuickAccess.ts | 8 +- .../editorNavigationQuickAccess.ts | 20 +- .../quickAccess/gotoLineQuickAccess.ts | 14 +- .../quickAccess/gotoSymbolQuickAccess.ts | 20 +- src/vs/editor/contrib/rename/rename.ts | 68 +- .../editor/contrib/rename/renameInputField.ts | 10 +- .../contrib/smartSelect/bracketSelections.ts | 10 +- .../editor/contrib/smartSelect/smartSelect.ts | 14 +- .../smartSelect/test/smartSelect.test.ts | 42 +- .../contrib/smartSelect/wordSelections.ts | 8 +- .../contrib/snippet/snippetController2.ts | 4 +- .../editor/contrib/snippet/snippetSession.ts | 23 +- .../contrib/snippet/snippetVariables.ts | 30 +- .../test/snippetController2.old.test.ts | 13 +- .../snippet/test/snippetController2.test.ts | 50 +- .../snippet/test/snippetParser.test.ts | 2 +- .../snippet/test/snippetSession.test.ts | 10 +- .../snippet/test/snippetVariables.test.ts | 50 +- .../editor/contrib/suggest/completionModel.ts | 14 +- .../editor/contrib/suggest/media/suggest.css | 4 + src/vs/editor/contrib/suggest/resizable.ts | 4 +- src/vs/editor/contrib/suggest/suggest.ts | 41 +- .../suggest/suggestCommitCharacters.ts | 4 +- .../contrib/suggest/suggestController.ts | 126 +- .../editor/contrib/suggest/suggestMemory.ts | 28 +- src/vs/editor/contrib/suggest/suggestModel.ts | 209 +- .../suggest/suggestOvertypingCapturer.ts | 2 +- .../editor/contrib/suggest/suggestWidget.ts | 50 +- .../contrib/suggest/suggestWidgetDetails.ts | 99 +- .../contrib/suggest/suggestWidgetRenderer.ts | 35 +- .../contrib/suggest/suggestWidgetStatus.ts | 2 +- .../suggest/test/completionModel.test.ts | 2 +- .../contrib/suggest/test/suggest.test.ts | 8 +- .../suggest/test/suggestController.test.ts | 50 +- .../suggest/test/suggestMemory.test.ts | 12 +- .../contrib/suggest/test/suggestModel.test.ts | 128 +- .../contrib/suggest/test/wordDistance.test.ts | 32 +- .../editor/contrib/suggest/wordContextKey.ts | 2 +- src/vs/editor/contrib/suggest/wordDistance.ts | 4 +- .../editor/contrib/symbolIcons/symbolIcons.ts | 6 +- .../toggleTabFocusMode/toggleTabFocusMode.ts | 8 +- .../contrib/tokenization/tokenization.ts | 6 +- .../unusualLineTerminators.ts | 4 +- .../viewportSemanticTokens.ts | 34 +- .../wordHighlighter/wordHighlighter.ts | 28 +- .../test/wordOperations.test.ts | 17 +- .../contrib/wordOperations/wordOperations.ts | 14 +- .../editor/contrib/zoneWidget/zoneWidget.ts | 2 +- .../accessibilityHelp/accessibilityHelp.ts | 4 +- src/vs/editor/standalone/browser/colorizer.ts | 25 +- .../browser/inspectTokens/inspectTokens.ts | 19 +- .../standaloneGotoLineQuickAccess.ts | 4 +- .../standaloneGotoSymbolQuickAccess.ts | 2 +- .../standalone/browser/simpleServices.ts | 12 +- .../browser/standaloneCodeEditor.ts | 35 +- .../standalone/browser/standaloneEditor.ts | 4 +- .../standalone/browser/standaloneLanguages.ts | 87 +- .../standalone/browser/standaloneServices.ts | 19 +- .../standalone/common/monarch/monarchLexer.ts | 66 +- .../test/browser/standaloneLanguages.test.ts | 70 +- .../standalone/test/monarch/monarch.test.ts | 5 + .../browser/commands/shiftCommand.test.ts | 21 +- .../test/browser/controller/cursor.test.ts | 326 +- src/vs/editor/test/browser/testCodeEditor.ts | 168 +- src/vs/editor/test/browser/testCommand.ts | 43 +- .../browser/view/minimapCharRenderer.test.ts | 6 +- src/vs/editor/test/common/commentMode.ts | 7 +- .../test/common/core/lineTokens.test.ts | 3 +- src/vs/editor/test/common/editorTestUtils.ts | 83 +- src/vs/editor/test/common/mocks/mockMode.ts | 24 +- .../beforeEditPositionMapper.test.ts | 4 +- .../bracketPairColorizer/brackets.test.ts | 82 + .../concat23Trees.test.ts | 30 +- .../getBracketPairsInRange.test.ts | 255 + .../model/bracketPairColorizer/length.test.ts | 2 +- .../smallImmutableSet.test.ts | 2 +- .../bracketPairColorizer/tokenizer.test.ts | 98 +- .../test/common/model/model.line.test.ts | 10 +- .../test/common/model/model.modes.test.ts | 4 +- src/vs/editor/test/common/model/model.test.ts | 41 +- .../common/model/modelInjectedText.test.ts | 3 + .../pieceTreeTextBuffer.test.ts | 8 + .../test/common/model/textModel.test.ts | 29 + .../test/common/model/textModelSearch.test.ts | 14 - .../common/model/textModelWithTokens.test.ts | 228 +- .../test/common/model/tokensStore.test.ts | 31 +- .../modes/languageConfiguration.test.ts | 6 +- .../modes/supports/electricCharacter.test.ts | 8 +- .../modes/testLanguageConfigurationService.ts | 32 + .../common/modes/textToHtmlTokenizer.test.ts | 49 +- src/vs/editor/test/common/modesTestUtils.ts | 3 +- .../common/services/languagesRegistry.test.ts | 52 +- .../test/common/services/modelService.test.ts | 124 +- .../semanticTokensProviderStyling.test.ts | 75 + .../viewModel/prefixSumComputer.test.ts | 14 +- .../viewModel/splitLinesCollection.test.ts | 29 +- .../common/viewModel/viewModelImpl.test.ts | 6 +- src/vs/loader.js | 137 +- src/vs/monaco.d.ts | 591 +- src/vs/nls.js | 2 +- .../accessibilityService.ts | 5 + .../accessibility/common/accessibility.ts | 1 + .../dropdownWithPrimaryActionViewItem.ts | 3 +- .../browser/menuEntryActionViewItem.ts | 2 +- src/vs/platform/actions/common/actions.ts | 14 +- src/vs/platform/actions/common/menuService.ts | 14 +- .../browser/contextScopedHistoryWidget.ts | 13 +- .../browser/historyWidgetKeybindingHint.ts | 10 + .../platform/checksum/node/checksumService.ts | 6 +- .../common/configurationModels.ts | 70 +- .../common/configurationRegistry.ts | 80 +- .../common/userConfigurationFileService.ts | 21 +- .../test/common/configurationService.test.ts | 4 +- .../test/common/testConfigurationService.ts | 2 +- .../platform/contextkey/common/contextkey.ts | 584 +- .../contextkey/test/common/contextkey.test.ts | 56 +- .../debug/common/extensionHostDebug.ts | 2 +- .../debug/common/extensionHostDebugIpc.ts | 6 +- .../electron-main/extensionHostDebugIpc.ts | 25 +- .../diagnostics/node/diagnosticsService.ts | 17 +- .../electron-main/dialogMainService.ts | 14 +- .../platform/driver/electron-main/driver.ts | 6 +- src/vs/platform/editor/common/editor.ts | 27 +- src/vs/platform/environment/common/argv.ts | 3 + .../environment/common/environment.ts | 1 + .../environment/common/environmentService.ts | 20 +- src/vs/platform/environment/node/argv.ts | 3 + src/vs/platform/environment/node/shellEnv.ts | 96 +- .../test/node/environmentService.test.ts | 45 +- .../test/node/nativeModules.test.ts | 20 +- .../abstractExtensionManagementService.ts | 102 +- .../common/extensionGalleryService.ts | 303 +- .../common/extensionManagement.ts | 204 +- .../common/extensionManagementCLIService.ts | 22 +- .../common/extensionManagementIpc.ts | 20 +- .../common/extensionTipsService.ts | 3 + .../node/extensionDownloader.ts | 7 +- .../node/extensionManagementService.ts | 119 +- .../node/extensionsScanner.ts | 32 +- .../common/extensionGalleryService.test.ts | 112 +- .../extensions/common/extensionHostStarter.ts | 35 + .../platform/extensions/common/extensions.ts | 19 +- .../extensions/node/extensionHostStarter.ts | 201 + .../node/externalTerminalService.ts | 14 +- .../files/browser/htmlFileSystemProvider.ts | 39 +- .../browser/indexedDBFileSystemProvider.ts | 149 +- .../files/common/diskFileSystemProvider.ts | 140 + src/vs/platform/files/common/fileService.ts | 72 +- src/vs/platform/files/common/files.ts | 52 +- src/vs/platform/files/common/io.ts | 3 +- .../files/common/ipcFileSystemProvider.ts | 171 +- src/vs/platform/files/common/watcher.ts | 295 + .../diskFileSystemProviderIpc.ts | 119 + .../files/node/diskFileSystemProvider.ts | 273 +- .../files/node/diskFileSystemProviderIpc.ts | 244 + .../node/watcher/nodejs/watcherService.ts | 8 +- .../node/watcher/nsfw/nsfwWatcherService.ts | 543 +- .../nsfw/test/nsfwWatcherService.test.ts | 58 - .../files/node/watcher/nsfw/watcher.ts | 30 +- .../files/node/watcher/nsfw/watcherService.ts | 53 +- .../watcher/parcel/parcelWatcherService.ts | 661 ++ .../watcher/{unix => parcel}/watcherApp.ts | 4 +- .../node/watcher/parcel/watcherService.ts | 46 + .../watcher/unix/chokidarWatcherService.ts | 374 -- .../unix/test/chockidarWatcherService.test.ts | 96 - .../files/node/watcher/unix/watcher.ts | 31 - .../files/node/watcher/unix/watcherService.ts | 100 - src/vs/platform/files/node/watcher/watcher.ts | 109 - .../files/node/watcher/win32/CodeHelper.exe | Bin 57856 -> 0 bytes .../files/node/watcher/win32/CodeHelper.md | 8 - .../watcher/win32/csharpWatcherService.ts | 142 - .../node/watcher/win32/watcherService.ts | 75 - .../files/test/browser/fileService.test.ts | 11 +- .../test/browser/indexedDBFileService.test.ts | 4 +- .../diskFileService.test.ts | 6 +- .../fixtures/resolver/examples/company.js | 0 .../fixtures/resolver/examples/conway.js | 0 .../fixtures/resolver/examples/employee.js | 0 .../fixtures/resolver/examples/small.js | 0 .../fixtures/resolver/index.html | 0 .../fixtures/resolver/other/deep/company.js | 0 .../fixtures/resolver/other/deep/conway.js | 0 .../fixtures/resolver/other/deep/employee.js | 0 .../fixtures/resolver/other/deep/small.js | 0 .../fixtures/resolver/site.css | 0 .../fixtures/service/binary.txt | Bin .../fixtures/service/deep/company.js | 0 .../fixtures/service/deep/conway.js | 0 .../fixtures/service/deep/employee.js | 0 .../fixtures/service/deep/small.js | 0 .../fixtures/service/index.html | 0 .../fixtures/service/lorem.txt | 0 .../fixtures/service/small.txt | 0 .../fixtures/service/small_umlaut.txt | 0 .../fixtures/service/some_utf16le.css | Bin .../fixtures/service/some_utf8_bom.txt | 0 .../node/recursiveWatcher.integrationTest.ts | 593 ++ .../watcherNormalizer.test.ts} | 135 +- .../instantiation/common/instantiation.ts | 1 + .../platform/ipc/electron-sandbox/services.ts | 4 + .../common/abstractKeybindingService.ts | 102 +- .../common/baseResolvedKeybinding.ts | 17 +- .../platform/keybinding/common/keybinding.ts | 3 +- .../keybinding/common/keybindingResolver.ts | 34 +- .../keybinding/common/keybindingsRegistry.ts | 76 +- .../common/resolvedKeybindingItem.ts | 2 +- .../common/usLayoutResolvedKeybinding.ts | 116 +- .../common/abstractKeybindingService.test.ts | 41 +- .../test/common/keybindingLabels.test.ts | 141 +- .../test/common/keybindingResolver.test.ts | 139 +- .../test/common/mockKeybindingService.ts | 2 +- .../keyboardLayout/common/keyboardLayout.ts | 3 +- .../keyboardLayout/common/keyboardMapper.ts | 3 +- .../launch/electron-main/launchMainService.ts | 35 +- .../platform/layout/browser/zIndexRegistry.ts | 75 + src/vs/platform/list/browser/listService.ts | 8 +- src/vs/platform/native/common/native.ts | 2 + .../electron-main/nativeHostMainService.ts | 25 +- src/vs/platform/opener/browser/link.ts | 54 +- src/vs/platform/product/common/product.ts | 16 +- .../quickinput/browser/pickerQuickAccess.ts | 9 +- .../remote/common/remoteAgentConnection.ts | 21 +- .../remote/common/remoteAgentEnvironment.ts | 1 + .../common/sharedProcessTunnelService.ts | 42 + src/vs/platform/remote/common/tunnel.ts | 90 +- .../sharedProcessTunnelService.ts | 9 + .../remote/node/sharedProcessTunnelService.ts | 123 + src/vs/platform/remote/node/tunnelService.ts | 51 +- src/vs/platform/request/common/request.ts | 6 +- .../platform/request/node/requestService.ts | 13 +- .../common/sharedProcessWorkerService.ts | 117 + .../electron-browser/sharedProcessWorker.ts | 47 + .../sharedProcessWorkerMain.ts | 248 + .../sharedProcessWorkerService.ts | 299 + .../electron-main/sharedProcess.ts | 77 +- src/vs/platform/sign/browser/signService.ts | 8 +- src/vs/platform/sign/common/sign.ts | 7 + src/vs/platform/sign/node/signService.ts | 49 +- .../storage/browser/storageService.ts | 141 +- src/vs/platform/storage/common/storage.ts | 11 +- .../electron-sandbox/storageService.ts | 15 +- .../test/browser/storageService.test.ts | 47 +- src/vs/platform/telemetry/common/telemetry.ts | 23 +- .../platform/telemetry/common/telemetryIpc.ts | 4 +- .../telemetry/common/telemetryService.ts | 238 +- .../telemetry/common/telemetryUtils.ts | 64 +- .../telemetry/node/appInsightsAppender.ts | 14 +- .../node/customEndpointTelemetryService.ts | 8 +- .../test/browser/telemetryService.test.ts | 354 +- src/vs/platform/terminal/common/terminal.ts | 122 +- .../common/terminalPlatformConfiguration.ts | 80 +- .../terminal/common/terminalProcess.ts | 3 +- .../terminal/common/terminalRecorder.ts | 9 +- .../terminal/node/childProcessMonitor.ts | 10 +- src/vs/platform/terminal/node/ptyHostMain.ts | 4 +- .../platform/terminal/node/ptyHostService.ts | 127 +- src/vs/platform/terminal/node/ptyService.ts | 276 +- .../platform/terminal/node/terminalProcess.ts | 123 +- .../terminal/node/terminalProfiles.ts | 8 +- .../terminal/node/windowsShellHelper.ts | 10 +- .../platform/theme/browser/iconsStyleSheet.ts | 2 +- src/vs/platform/theme/common/colorRegistry.ts | 12 +- .../common/tokenClassificationRegistry.ts | 1 + .../electron-main/abstractUpdateService.ts | 26 +- .../common/abstractSynchronizer.ts | 51 +- .../userDataSync/common/extensionsSync.ts | 16 +- .../userDataSync/common/globalStateSync.ts | 17 +- .../userDataSync/common/keybindingsSync.ts | 59 +- .../userDataSync/common/settingsSync.ts | 24 +- .../userDataSync/common/snippetsSync.ts | 2 +- .../userDataSync/common/userDataSync.ts | 10 +- .../common/userDataSyncService.ts | 16 +- .../common/userDataSyncStoreService.ts | 1 - .../test/common/keybindingsSync.test.ts | 25 +- .../test/common/settingsSync.test.ts | 2 +- .../test/common/snippetsSync.test.ts | 20 +- .../test/common/synchronizer.test.ts | 78 +- .../test/common/userDataSyncClient.ts | 2 + .../common/userDataSyncStoreService.test.ts | 54 +- src/vs/platform/windows/common/windows.ts | 19 +- .../platform/windows/electron-main/window.ts | 88 +- .../platform/windows/electron-main/windows.ts | 1 + .../electron-main/windowsMainService.ts | 80 +- src/vs/platform/workspace/common/workspace.ts | 15 +- .../workspace/test/common/testWorkspace.ts | 2 +- .../workspace/test/common/workspace.test.ts | 10 +- .../platform/workspaces/common/workspaces.ts | 1 + .../workspacesHistoryMainService.ts | 72 +- src/vs/server/cli.js | 17 + src/vs/server/extensionHostConnection.ts | 272 + src/vs/server/main.js | 158 + src/vs/server/remoteAgentEnvironmentImpl.ts | 508 ++ src/vs/server/remoteCli.ts | 412 ++ src/vs/server/remoteExtensionHostAgent.ts | 64 + src/vs/server/remoteExtensionHostAgentCli.ts | 145 + .../server/remoteExtensionHostAgentServer.ts | 1043 +++ src/vs/server/remoteExtensionHostProcess.ts | 8 + src/vs/server/remoteExtensionManagement.ts | 127 + src/vs/server/remoteFileSystemProviderIpc.ts | 128 + src/vs/server/remoteLanguagePacks.ts | 46 + src/vs/server/remoteTelemetryService.ts | 64 + src/vs/server/remoteTerminalChannel.ts | 328 + src/vs/server/remoteUriTransformer.ts | 15 + src/vs/server/serverEnvironmentService.ts | 126 + src/vs/server/uriTransformer.js | 64 + src/vs/server/webClientServer.ts | 354 ++ src/vs/vscode.d.ts | 368 +- src/vs/vscode.proposed.d.ts | 515 +- .../api/browser/mainThreadAuthentication.ts | 121 +- .../api/browser/mainThreadCommands.ts | 6 +- .../api/browser/mainThreadComments.ts | 25 +- .../api/browser/mainThreadConsole.ts | 13 +- .../api/browser/mainThreadCustomEditors.ts | 2 +- .../api/browser/mainThreadDebugService.ts | 13 +- .../api/browser/mainThreadDecorations.ts | 2 +- .../api/browser/mainThreadDocuments.ts | 13 +- .../browser/mainThreadDocumentsAndEditors.ts | 29 +- .../workbench/api/browser/mainThreadEditor.ts | 2 +- .../api/browser/mainThreadEditorTabs.ts | 287 +- .../api/browser/mainThreadFileSystem.ts | 16 +- .../mainThreadFileSystemEventService.ts | 6 +- .../api/browser/mainThreadLanguageFeatures.ts | 20 +- .../api/browser/mainThreadLanguages.ts | 33 +- .../api/browser/mainThreadNotebookEditors.ts | 19 +- .../api/browser/mainThreadNotebookKernels.ts | 6 + .../api/browser/mainThreadProgress.ts | 2 +- src/vs/workbench/api/browser/mainThreadSCM.ts | 1 + .../workbench/api/browser/mainThreadSearch.ts | 6 +- .../api/browser/mainThreadStatusBar.ts | 6 +- .../api/browser/mainThreadStorage.ts | 41 +- .../api/browser/mainThreadTelemetry.ts | 23 +- .../api/browser/mainThreadTerminalService.ts | 55 +- .../api/browser/mainThreadTesting.ts | 6 +- .../api/browser/mainThreadTunnelService.ts | 1 + .../api/browser/mainThreadWebviewPanels.ts | 6 +- .../api/browser/mainThreadWebviews.ts | 1 + .../api/browser/mainThreadWorkspace.ts | 5 +- .../api/browser/viewsExtensionPoint.ts | 18 +- .../workbench/api/common/extHost.api.impl.ts | 54 +- .../workbench/api/common/extHost.protocol.ts | 59 +- .../api/common/extHostApiCommands.ts | 21 +- .../api/common/extHostAuthentication.ts | 1 + .../workbench/api/common/extHostCommands.ts | 44 +- .../workbench/api/common/extHostComments.ts | 11 +- .../api/common/extHostDebugService.ts | 11 +- .../api/common/extHostDiagnostics.ts | 80 +- .../api/common/extHostDocumentData.ts | 10 +- .../workbench/api/common/extHostDocuments.ts | 19 +- .../api/common/extHostDocumentsAndEditors.ts | 2 +- .../workbench/api/common/extHostEditorTabs.ts | 79 +- .../api/common/extHostExtensionActivator.ts | 5 + .../api/common/extHostExtensionService.ts | 63 +- .../workbench/api/common/extHostFileSystem.ts | 15 +- .../api/common/extHostFileSystemInfo.ts | 23 +- .../api/common/extHostInteractive.ts | 8 +- .../api/common/extHostLanguageFeatures.ts | 51 +- .../workbench/api/common/extHostLanguages.ts | 90 +- .../common/extHostNotebookConcatDocument.ts | 4 +- .../api/common/extHostNotebookDocument.ts | 2 +- .../api/common/extHostNotebookKernels.ts | 74 +- .../workbench/api/common/extHostProgress.ts | 5 +- .../workbench/api/common/extHostQuickOpen.ts | 20 +- .../api/common/extHostRequireInterceptor.ts | 22 +- src/vs/workbench/api/common/extHostSCM.ts | 20 +- .../workbench/api/common/extHostStatusBar.ts | 33 +- src/vs/workbench/api/common/extHostTask.ts | 23 +- .../api/common/extHostTerminalService.ts | 105 +- src/vs/workbench/api/common/extHostTesting.ts | 67 +- .../api/common/extHostTunnelService.ts | 14 +- .../api/common/extHostTypeConverters.ts | 56 +- src/vs/workbench/api/common/extHostTypes.ts | 61 +- src/vs/workbench/api/common/extHostWebview.ts | 14 + .../workbench/api/common/extHostWorkspace.ts | 10 +- .../api/common/menusExtensionPoint.ts | 8 +- .../api/node/extHostExtensionService.ts | 2 +- src/vs/workbench/api/node/extHostSearch.ts | 7 + .../workbench/api/node/extHostStoragePaths.ts | 19 +- .../api/node/extHostTerminalService.ts | 14 - .../api/node/extHostTunnelService.ts | 24 +- .../api/worker/extHostExtensionService.ts | 4 +- .../browser/actions/developerActions.ts | 46 +- .../workbench/browser/actions/helpActions.ts | 10 +- .../browser/actions/layoutActions.ts | 47 +- .../workbench/browser/actions/listCommands.ts | 9 +- .../browser/actions/media/actions.css | 4 + .../browser/actions/navigationActions.ts | 74 +- .../browser/actions/quickAccessActions.ts | 14 +- .../browser/actions/windowActions.ts | 21 +- .../browser/actions/workspaceActions.ts | 22 +- src/vs/workbench/browser/codeeditor.ts | 2 +- src/vs/workbench/browser/contextkeys.ts | 42 +- src/vs/workbench/browser/dnd.ts | 103 +- src/vs/workbench/browser/editor.ts | 31 +- src/vs/workbench/browser/labels.ts | 123 +- src/vs/workbench/browser/layout.ts | 330 +- src/vs/workbench/browser/media/style.css | 6 +- src/vs/workbench/browser/panecomposite.ts | 99 +- src/vs/workbench/browser/panel.ts | 135 - src/vs/workbench/browser/part.ts | 2 +- .../parts/activitybar/activitybarActions.ts | 42 +- .../parts/activitybar/activitybarPart.ts | 84 +- .../activitybar/media/activityaction.css | 32 +- .../parts/auxiliarybar/auxiliaryBarActions.ts | 93 + .../parts/auxiliarybar/auxiliaryBarPart.ts | 193 + .../auxiliarybar/media/auxiliaryBarPart.css | 9 + .../browser/parts/banner/bannerPart.ts | 25 +- .../browser/parts/banner/media/bannerpart.css | 11 + .../workbench/browser/parts/compositeBar.ts | 91 +- .../workbench/browser/parts/compositePart.ts | 4 +- .../browser/parts/editor/binaryDiffEditor.ts | 16 +- .../browser/parts/editor/binaryEditor.ts | 6 +- .../parts/editor/breadcrumbsControl.ts | 10 +- .../browser/parts/editor/breadcrumbsModel.ts | 1 + .../browser/parts/editor/breadcrumbsPicker.ts | 4 +- .../parts/editor/editor.contribution.ts | 209 +- .../workbench/browser/parts/editor/editor.ts | 82 +- .../browser/parts/editor/editorActions.ts | 185 +- .../browser/parts/editor/editorAutoSave.ts | 11 +- .../browser/parts/editor/editorCommands.ts | 588 +- .../parts/editor/editorConfiguration.ts | 84 + .../browser/parts/editor/editorDropTarget.ts | 64 +- .../browser/parts/editor/editorGroupView.ts | 363 +- .../browser/parts/editor/editorPane.ts | 19 +- .../{editorControl.ts => editorPanes.ts} | 138 +- .../browser/parts/editor/editorPart.ts | 172 +- .../browser/parts/editor/editorPlaceholder.ts | 72 +- .../browser/parts/editor/editorQuickAccess.ts | 6 +- .../browser/parts/editor/editorStatus.ts | 176 +- .../parts/editor/editorWithViewState.ts | 249 + .../browser/parts/editor/editorsObserver.ts | 19 +- .../parts/editor/media/binaryeditor.css | 2 +- .../parts/editor/media/editorgroupview.css | 2 +- .../parts/editor/media/editorplaceholder.css | 2 +- .../parts/editor/media/titlecontrol.css | 1 - .../parts/editor/noTabsTitleControl.ts | 90 +- .../browser/parts/editor/sideBySideEditor.ts | 452 +- .../browser/parts/editor/tabsTitleControl.ts | 285 +- .../browser/parts/editor/textDiffEditor.ts | 109 +- .../browser/parts/editor/textEditor.ts | 107 +- .../parts/editor/textResourceEditor.ts | 64 +- .../browser/parts/editor/titleControl.ts | 73 +- .../notifications/notificationsAlerts.ts | 4 +- .../notifications/notificationsCenter.ts | 4 +- .../notifications/notificationsCommands.ts | 16 +- .../parts/notifications/notificationsList.ts | 12 +- .../notifications/notificationsStatus.ts | 2 +- .../notifications/notificationsToasts.ts | 10 +- .../browser/parts/paneCompositePart.ts | 151 + .../parts/panel/media/basepanelpart.css | 186 + .../browser/parts/panel/media/panelpart.css | 176 - .../browser/parts/panel/panelActions.ts | 46 +- .../browser/parts/panel/panelPart.ts | 368 +- .../browser/parts/sidebar/sidebarActions.ts | 51 + .../browser/parts/sidebar/sidebarPart.ts | 118 +- .../parts/statusbar/media/statusbarpart.css | 30 +- .../parts/statusbar/statusbarActions.ts | 130 + .../browser/parts/statusbar/statusbarItem.ts | 288 + .../browser/parts/statusbar/statusbarModel.ts | 437 ++ .../browser/parts/statusbar/statusbarPart.ts | 1002 +-- .../browser/parts/titlebar/menubarControl.ts | 2 +- .../browser/parts/titlebar/titlebarPart.ts | 1 - .../workbench/browser/parts/views/treeView.ts | 29 +- .../workbench/browser/parts/views/viewPane.ts | 14 +- .../browser/parts/views/viewPaneContainer.ts | 146 +- .../browser/parts/views/viewsService.ts | 302 +- .../browser/parts/views/viewsViewlet.ts | 4 +- src/vs/workbench/browser/viewlet.ts | 104 - src/vs/workbench/browser/web.main.ts | 91 +- src/vs/workbench/browser/window.ts | 24 +- .../browser/workbench.contribution.ts | 63 +- src/vs/workbench/browser/workbench.ts | 19 +- src/vs/workbench/buildfile.desktop.js | 2 +- src/vs/workbench/common/auxiliarybar.ts | 11 + src/vs/workbench/common/editor.ts | 599 +- .../common/editor/binaryEditorModel.ts | 2 +- .../common/editor/diffEditorInput.ts | 184 +- .../common/editor/editorGroupModel.ts | 114 +- src/vs/workbench/common/editor/editorInput.ts | 168 +- .../common/editor/resourceEditorInput.ts | 48 +- .../common/editor/sideBySideEditorInput.ts | 233 +- .../common/editor/textEditorModel.ts | 23 +- .../common/editor/textResourceEditorInput.ts | 44 +- .../common/editor/textResourceEditorModel.ts | 6 +- src/vs/workbench/common/panecomposite.ts | 5 + src/vs/workbench/common/panel.ts | 3 - src/vs/workbench/common/resources.ts | 92 +- src/vs/workbench/common/theme.ts | 28 +- src/vs/workbench/common/viewlet.ts | 9 - src/vs/workbench/common/views.ts | 16 + .../bulkEdit/browser/bulkEditService.ts | 27 +- .../browser/preview/bulkEdit.contribution.ts | 48 +- .../bulkEdit/browser/preview/bulkEditPane.ts | 9 +- .../browser/preview/bulkEditPreview.ts | 17 +- .../bulkEdit/browser/preview/bulkEditTree.ts | 6 +- .../browser/callHierarchy.contribution.ts | 6 +- .../browser/accessibility/accessibility.ts | 4 +- .../browser/find/simpleFindReplaceWidget.ts | 8 + .../inspectEditorTokens.ts | 22 +- .../languageConfigurationExtensionPoint.ts | 261 +- .../quickaccess/gotoLineQuickAccess.ts | 10 +- .../quickaccess/gotoSymbolQuickAccess.ts | 16 +- .../codeEditor/browser/saveParticipants.ts | 34 +- .../codeEditor/browser/simpleEditorOptions.ts | 4 +- .../suggestEnabledInput.ts | 7 +- .../codeEditor/browser/toggleWordWrap.ts | 2 +- .../browser/untitledTextEditorHint.ts | 14 +- .../electron-sandbox/inputClipboardActions.ts | 6 +- .../test/browser/saveParticipant.test.ts | 6 +- .../comments/browser/commentService.ts | 12 +- .../comments/browser/commentThreadWidget.ts | 10 +- .../browser/commentsEditorContribution.ts | 3 +- .../comments/browser/commentsTreeViewer.ts | 7 +- .../contrib/comments/browser/commentsView.ts | 4 +- .../contrib/comments/browser/media/review.css | 2 +- .../customEditor/browser/customEditorInput.ts | 44 +- .../customEditor/browser/customEditors.ts | 25 +- .../common/contributedCustomEditors.ts | 16 +- .../contrib/debug/browser/baseDebugView.ts | 3 +- .../browser/breakpointEditorContribution.ts | 13 +- .../contrib/debug/browser/breakpointWidget.ts | 10 +- .../contrib/debug/browser/breakpointsView.ts | 9 +- .../contrib/debug/browser/callStackView.ts | 8 +- .../debug/browser/debug.contribution.ts | 12 +- .../debug/browser/debugAdapterManager.ts | 96 +- .../contrib/debug/browser/debugCommands.ts | 16 +- .../browser/debugConfigurationManager.ts | 15 +- .../debug/browser/debugEditorActions.ts | 42 +- .../debug/browser/debugEditorContribution.ts | 2 +- .../contrib/debug/browser/debugQuickAccess.ts | 8 +- .../contrib/debug/browser/debugService.ts | 141 +- .../contrib/debug/browser/debugSession.ts | 15 +- .../contrib/debug/browser/debugStatus.ts | 2 +- .../contrib/debug/browser/debugTaskRunner.ts | 27 +- .../contrib/debug/browser/debugToolBar.ts | 6 +- .../contrib/debug/browser/debugViewlet.ts | 14 +- .../contrib/debug/browser/disassemblyView.ts | 289 +- .../browser/extensionHostDebugService.ts | 62 +- .../browser/media/debug.contribution.css | 14 +- .../contrib/debug/browser/rawDebugSession.ts | 18 +- .../workbench/contrib/debug/browser/repl.ts | 279 +- .../contrib/debug/browser/replFilter.ts | 4 +- .../contrib/debug/browser/variablesView.ts | 6 +- .../debug/browser/watchExpressionsView.ts | 8 +- .../contrib/debug/browser/welcomeView.ts | 2 +- .../workbench/contrib/debug/common/debug.ts | 9 +- .../contrib/debug/common/debugLifecycle.ts | 9 +- .../contrib/debug/common/debugModel.ts | 2 +- .../contrib/debug/common/debugSchemas.ts | 5 + .../contrib/debug/common/debugSource.ts | 4 +- .../contrib/debug/common/debugger.ts | 19 +- .../contrib/debug/node/telemetryApp.ts | 2 +- .../workbench/contrib/debug/node/terminals.ts | 7 +- .../debug/test/browser/baseDebugView.test.ts | 9 +- .../debug/test/browser/breakpoints.test.ts | 9 +- .../debug/test/browser/callStack.test.ts | 4 +- .../test/browser/debugANSIHandling.test.ts | 9 +- .../debug/test/browser/linkDetector.test.ts | 9 +- .../contrib/debug/test/node/debugger.test.ts | 2 +- .../contrib/emmet/browser/emmetActions.ts | 15 +- .../emmet/test/browser/emmetAction.test.ts | 27 +- .../experiments/browser/experimentalPrompt.ts | 7 +- .../experiments/common/experimentService.ts | 59 +- .../experimentService.test.ts | 67 +- .../abstractRuntimeExtensionsEditor.ts | 4 +- .../dynamicWorkspaceRecommendations.ts | 2 +- .../browser/experimentalRecommendations.ts | 22 +- .../extensions/browser/extensionEditor.ts | 117 +- ...ensionRecommendationNotificationService.ts | 22 +- .../extensionRecommendationsService.ts | 16 +- .../browser/extensions.contribution.ts | 126 +- .../extensions/browser/extensionsActions.ts | 242 +- .../extensions/browser/extensionsIcons.ts | 1 + .../extensions/browser/extensionsList.ts | 22 +- .../browser/extensionsQuickAccess.ts | 17 +- .../extensions/browser/extensionsViewer.ts | 10 +- .../extensions/browser/extensionsViewlet.ts | 16 +- .../extensions/browser/extensionsViews.ts | 40 +- .../extensions/browser/extensionsWidgets.ts | 17 +- .../browser/extensionsWorkbenchService.ts | 71 +- .../browser/fileBasedRecommendations.ts | 30 +- .../extensions/browser/media/extension.css | 21 +- .../browser/media/extensionEditor.css | 18 +- .../browser/media/extensionsViewlet.css | 7 +- .../browser/media/extensionsWidgets.css | 2 +- .../extensions/browser/webRecommendations.ts | 39 + .../browser/workspaceRecommendations.ts | 8 +- .../extensions/common/extensionQuery.ts | 4 +- .../contrib/extensions/common/extensions.ts | 3 +- .../extensions/common/extensionsInput.ts | 4 +- .../common/runtimeExtensionsInput.ts | 4 +- .../extensionProfileService.ts | 2 +- .../extensionRecommendationsService.test.ts | 13 +- .../extensionsActions.test.ts | 55 +- .../electron-browser/extensionsViews.test.ts | 10 +- .../extensionsWorkbenchService.test.ts | 27 +- .../browser/externalTerminal.contribution.ts | 3 +- .../externalTerminal.contribution.ts | 2 +- .../contrib/feedback/browser/feedback.ts | 90 +- .../feedback/browser/feedbackStatusbarItem.ts | 57 +- .../files/browser/editors/binaryFileEditor.ts | 19 +- .../browser/editors/fileEditorHandler.ts | 10 +- .../files/browser/editors/fileEditorInput.ts | 37 +- .../files/browser/editors/textFileEditor.ts | 106 +- .../browser/editors/textFileEditorTracker.ts | 6 +- .../contrib/files/browser/explorerViewlet.ts | 6 +- .../files/browser/fileActions.contribution.ts | 14 +- .../contrib/files/browser/fileActions.ts | 19 +- .../contrib/files/browser/fileCommands.ts | 54 +- .../contrib/files/browser/fileImportExport.ts | 29 +- .../files/browser/files.contribution.ts | 62 +- .../views/explorerDecorationsProvider.ts | 2 +- .../files/browser/views/explorerView.ts | 28 +- .../files/browser/views/explorerViewer.ts | 25 +- .../files/browser/views/openEditorsView.ts | 26 +- .../{common => browser}/workspaceWatcher.ts | 106 +- .../workbench/contrib/files/common/files.ts | 8 +- .../fileActions.contribution.ts | 10 +- .../files/electron-sandbox/textFileEditor.ts | 6 +- .../files/test/browser/editorAutoSave.test.ts | 11 +- .../test/browser/fileEditorInput.test.ts | 31 +- .../test/browser/fileOnDiskProvider.test.ts | 9 +- .../browser/textFileEditorTracker.test.ts | 24 +- .../format/browser/formatActionsMultiple.ts | 36 +- .../format/browser/formatActionsNone.ts | 17 +- .../browser/interactive.contribution.ts | 77 +- .../interactive/browser/interactiveEditor.ts | 64 +- .../browser/interactiveEditorInput.ts | 40 +- .../issue/browser/issue.web.contribution.ts | 15 +- .../contrib/issue/browser/issueService.ts | 59 +- .../issue/electron-sandbox/issueActions.ts | 2 +- .../browser/languageStatus.contribution.ts | 333 + .../browser/media/languageStatus.css | 76 + .../browser/localizations.contribution.ts | 16 +- .../browser/localizationsActions.ts | 7 +- .../contrib/logs/common/logs.contribution.ts | 49 +- .../markdownDocumentRenderer.ts | 83 +- .../contrib/markers/browser/constants.ts | 1 + .../markers/browser/markers.contribution.ts | 60 +- .../contrib/markers/browser/markers.ts | 4 +- .../markers/browser/markersFileDecorations.ts | 2 +- .../markers/browser/markersTreeViewer.ts | 11 +- .../contrib/markers/browser/markersView.ts | 41 +- .../markers/browser/markersViewActions.ts | 10 +- .../contrib/markers/browser/messages.ts | 3 + .../breakpoints/notebookBreakpoints.ts | 40 +- .../contrib/cellCommands/cellCommands.ts | 484 ++ .../contrib/cellOperations/cellOperations.ts | 551 -- .../contributedStatusBarItemController.ts | 2 +- .../executionStatusBarItemController.ts | 13 +- .../notebookVisibleCellObserver.ts | 14 +- .../cellStatusBar/statusBarProviders.ts | 4 +- .../contrib/clipboard/notebookClipboard.ts | 219 +- .../clipboard/test/notebookClipboard.test.ts | 6 +- .../contrib/codeRenderer/codeRenderer.ts | 14 +- .../notebook/browser/contrib/coreActions.ts | 2180 ------- .../editorStatusBar/editorStatusBar.ts | 147 +- .../browser/contrib/find/findController.ts | 55 +- .../browser/contrib/find/findModel.ts | 20 +- .../browser/contrib/find/test/find.test.ts | 16 +- .../notebook/browser/contrib/fold/folding.ts | 45 +- .../contrib/fold/test/notebookFolding.test.ts | 14 +- .../browser/contrib/format/formatting.ts | 14 +- .../browser/contrib/layout/layoutActions.ts | 6 +- .../browser/contrib/marker/markerProvider.ts | 4 +- .../browser/contrib/navigation/arrow.ts | 55 +- .../contrib/outline/notebookOutline.ts | 66 +- .../outline/test/notebookOutline.test.ts | 68 +- .../contrib/profile/notebookProfile.ts | 3 +- .../browser/contrib/troubleshoot/layout.ts | 22 +- .../contrib/undoRedo/notebookUndoRedo.ts | 21 +- .../viewportCustomMarkdown.ts | 30 +- .../notebook/browser/controller/apiActions.ts | 78 + .../browser/controller/cellOperations.ts | 675 ++ .../browser/controller/coreActions.ts | 432 ++ .../browser/controller/editActions.ts | 477 ++ .../browser/controller/executeActions.ts | 523 ++ .../browser/controller/insertCellActions.ts | 342 + .../browser/controller/layoutActions.ts | 217 + .../notebook/browser/diff/diffComponents.ts | 32 +- .../browser/diff/diffElementViewModel.ts | 65 +- .../browser/diff/diffNestedCellViewModel.ts | 2 +- .../notebook/browser/diff/notebookDiff.css | 2 +- .../browser/diff/notebookDiffActions.ts | 8 +- .../browser/diff/notebookDiffEditorBrowser.ts | 8 +- .../browser/diff/notebookTextDiffEditor.ts | 45 +- .../browser/diff/notebookTextDiffList.ts | 15 +- .../notebook/browser/media/notebook.css | 62 +- .../notebook/browser/notebook.contribution.ts | 31 +- .../notebook/browser/notebookBrowser.ts | 488 +- .../browser/notebookDiffEditorInput.ts | 24 +- .../notebook/browser/notebookEditor.ts | 20 +- .../browser/notebookEditorKernelManager.ts | 4 +- .../notebook/browser/notebookEditorWidget.ts | 556 +- .../browser/notebookExecutionServiceImpl.ts | 18 +- .../contrib/notebook/browser/notebookIcons.ts | 2 +- .../browser/notebookKernelServiceImpl.ts | 13 +- .../browser/notebookKeymapServiceImpl.ts | 3 +- .../notebook/browser/notebookLogger.ts | 31 + .../notebook/browser/notebookServiceImpl.ts | 181 +- .../notebook/browser/view/notebookCellList.ts | 29 +- .../browser/view/notebookRenderingCommon.ts | 162 + .../browser/view/output/outputRenderer.ts | 41 +- .../browser/view/output/rendererRegistry.ts | 6 +- .../view/output/transforms/richTransform.ts | 39 +- .../view/output/transforms/textHelper.ts | 8 +- .../view/renderers/backLayerWebView.ts | 115 +- .../browser/view/renderers/cellContextKeys.ts | 6 +- .../browser/view/renderers/cellDnd.ts | 49 +- .../view/renderers/cellEditorOptions.ts | 93 +- .../browser/view/renderers/cellOutput.ts | 237 +- .../browser/view/renderers/cellRenderer.ts | 143 +- .../browser/view/renderers/cellWidgets.ts | 2 +- .../browser/view/renderers/codeCell.ts | 81 +- .../browser/view/renderers/markdownCell.ts | 11 +- .../browser/view/renderers/webviewMessages.ts | 7 +- .../browser/view/renderers/webviewPreloads.ts | 370 +- .../view/renderers/webviewThemeMapping.ts | 1 + .../browser/viewModel/baseCellViewModel.ts | 7 +- .../browser/viewModel/codeCellViewModel.ts | 53 +- .../browser/viewModel/eventDispatcher.ts | 37 +- .../browser/viewModel/markupCellViewModel.ts | 48 +- .../browser/viewModel/notebookViewModel.ts | 257 +- .../notebookEditorDecorations.ts | 0 .../{ => viewParts}/notebookEditorToolbar.ts | 32 +- .../notebookEditorWidgetContextKeys.ts | 21 +- .../notebookKernelActionViewItem.css | 0 .../notebookKernelActionViewItem.ts | 6 +- .../common/model/notebookCellTextModel.ts | 85 +- .../common/model/notebookTextModel.ts | 29 +- .../contrib/notebook/common/notebookCommon.ts | 115 +- .../notebook/common/notebookEditorInput.ts | 22 +- .../notebook/common/notebookEditorModel.ts | 15 +- .../notebook/common/notebookKernelService.ts | 4 +- .../notebook/common/notebookOptions.ts | 18 +- .../notebook/common/notebookOutputRenderer.ts | 9 +- .../contrib/notebook/common/notebookRange.ts | 23 +- .../notebook/common/notebookService.ts | 4 +- .../common/services/notebookSimpleWorker.ts | 14 +- .../test/cellOperations.test.ts | 103 +- .../contrib/notebook/test/cellOutput.test.ts | 89 +- .../notebook/test/notebookBrowser.test.ts | 18 +- .../notebook/test/notebookCellList.test.ts | 19 +- .../notebook/test/notebookCommon.test.ts | 191 +- .../notebook/test/notebookDiff.test.ts | 60 +- .../notebook/test/notebookEditor.test.ts | 27 +- .../test/notebookEditorKernelManager.test.ts | 21 +- .../notebook/test/notebookEditorModel.test.ts | 13 +- .../test/notebookKernelService.test.ts | 11 +- .../notebook/test/notebookSelection.test.ts | 22 +- .../notebook/test/notebookServiceImpl.test.ts | 9 +- .../notebook/test/notebookTextModel.test.ts | 17 +- .../notebook/test/notebookViewModel.test.ts | 176 +- .../notebook/test/testNotebookEditor.ts | 152 +- .../contrib/outline/browser/outlinePane.ts | 60 +- .../contrib/output/browser/logViewer.ts | 7 +- .../output/browser/output.contribution.ts | 14 +- .../output/common/outputChannelModel.ts | 30 +- .../performance/browser/perfviewEditor.ts | 7 +- .../electron-sandbox/startupTimings.ts | 13 +- .../preferences/browser/keybindingWidgets.ts | 13 +- .../preferences/browser/keybindingsEditor.ts | 63 +- .../browser/keybindingsEditorContribution.ts | 6 +- .../browser/keyboardLayoutPicker.ts | 2 +- .../browser/media/settingsEditor2.css | 31 +- .../browser/preferences.contribution.ts | 49 +- .../preferences/browser/preferencesActions.ts | 6 +- .../preferences/browser/preferencesWidgets.ts | 9 +- .../preferences/browser/settingsEditor2.ts | 13 +- .../preferences/browser/settingsLayout.ts | 1 + .../preferences/browser/settingsTree.ts | 94 +- .../preferences/browser/settingsTreeModels.ts | 12 +- .../preferences/browser/settingsWidgets.ts | 8 +- .../common/preferencesContribution.ts | 10 +- .../browser/commandsQuickAccess.ts | 2 +- .../browser/quickAccess.contribution.ts | 18 +- .../quickaccess/browser/viewQuickAccess.ts | 76 +- .../browser/relauncher.contribution.ts | 25 +- .../remote/browser/explorerViewItems.ts | 4 +- .../contrib/remote/browser/remote.ts | 36 +- .../contrib/remote/browser/remoteExplorer.ts | 12 +- .../contrib/remote/browser/remoteIcons.ts | 1 - .../contrib/remote/browser/remoteIndicator.ts | 9 +- .../contrib/remote/browser/tunnelView.ts | 192 +- .../remote/common/remote.contribution.ts | 88 +- .../contrib/remote/common/tunnelFactory.ts | 76 +- .../electron-sandbox/remote.contribution.ts | 14 +- .../workbench/contrib/scm/browser/activity.ts | 2 +- .../contrib/scm/browser/dirtydiffDecorator.ts | 27 +- .../contrib/scm/browser/media/scm.css | 19 +- .../contrib/scm/browser/scm.contribution.ts | 11 +- .../scm/browser/scmRepositoryRenderer.ts | 15 +- .../contrib/scm/browser/scmViewPane.ts | 165 +- src/vs/workbench/contrib/scm/browser/util.ts | 6 +- src/vs/workbench/contrib/scm/common/scm.ts | 7 + .../search/browser/anythingQuickAccess.ts | 27 +- .../search/browser/patternInputWidget.ts | 15 +- .../contrib/search/browser/replaceService.ts | 2 +- .../search/browser/search.contribution.ts | 53 +- .../contrib/search/browser/searchActions.ts | 2 +- .../contrib/search/browser/searchMessage.ts | 3 +- .../contrib/search/browser/searchView.ts | 24 +- .../contrib/search/browser/searchWidget.ts | 18 +- .../workbench/contrib/search/common/search.ts | 2 +- .../search/test/browser/searchActions.test.ts | 2 +- .../search/test/browser/searchViewlet.test.ts | 3 + .../search/test/common/searchModel.test.ts | 5 +- .../contrib/searchEditor/browser/constants.ts | 2 + .../browser/searchEditor.contribution.ts | 16 +- .../searchEditor/browser/searchEditor.ts | 51 +- .../searchEditor/browser/searchEditorInput.ts | 14 +- .../contrib/snippets/browser/insertSnippet.ts | 26 +- .../browser/snippetCompletionProvider.ts | 126 +- .../snippets/browser/snippets.contribution.ts | 5 +- .../snippets/browser/snippetsService.ts | 15 +- .../test/browser/snippetsService.test.ts | 127 +- .../surveys/browser/ces.contribution.ts | 3 +- .../browser/languageSurveys.contribution.ts | 3 - .../tags/electron-sandbox/workspaceTags.ts | 5 +- .../electron-sandbox/workspaceTagsService.ts | 16 +- .../tasks/browser/abstractTaskService.ts | 31 +- .../tasks/browser/runAutomaticTasks.ts | 10 +- .../tasks/browser/task.contribution.ts | 4 +- .../tasks/browser/taskTerminalStatus.ts | 17 +- .../tasks/browser/terminalTaskSystem.ts | 34 +- .../tasks/electron-sandbox/taskService.ts | 6 +- .../browser/telemetry.contribution.ts | 16 +- .../browser/addons/lineDataEventAddon.ts | 74 + .../browser/links/terminalLinkManager.ts | 20 +- .../terminalValidatedLocalLinkProvider.ts | 8 +- .../browser/links/terminalWordLinkProvider.ts | 22 +- .../terminal/browser/media/terminal.css | 38 +- .../contrib/terminal/browser/remotePty.ts | 88 +- .../terminal/browser/remoteTerminalService.ts | 53 +- .../terminal/browser/terminal.contribution.ts | 31 +- .../contrib/terminal/browser/terminal.ts | 43 +- .../browser/terminal.web.contribution.ts | 2 +- .../terminal/browser/terminalActions.ts | 382 +- .../terminal/browser/terminalConfigHelper.ts | 2 +- .../browser/terminalDecorationsProvider.ts | 2 +- .../terminal/browser/terminalEditor.ts | 12 +- .../terminal/browser/terminalEditorInput.ts | 10 +- .../terminal/browser/terminalEditorService.ts | 12 +- .../contrib/terminal/browser/terminalGroup.ts | 34 +- .../terminal/browser/terminalGroupService.ts | 23 +- .../contrib/terminal/browser/terminalIcon.ts | 65 +- .../terminal/browser/terminalInstance.ts | 625 +- .../browser/terminalInstanceService.ts | 3 + .../contrib/terminal/browser/terminalMenus.ts | 220 +- .../browser/terminalProcessExtHostProxy.ts | 60 +- .../browser/terminalProcessManager.ts | 75 +- .../browser/terminalProfileResolverService.ts | 63 +- .../terminal/browser/terminalQuickAccess.ts | 5 +- .../terminal/browser/terminalService.ts | 301 +- .../terminal/browser/terminalTabbedView.ts | 18 +- .../terminal/browser/terminalTabsList.ts | 13 +- .../contrib/terminal/browser/terminalView.ts | 29 +- .../terminal/browser/xterm-private.d.ts | 6 +- .../terminal/common/remoteTerminalChannel.ts | 40 +- .../contrib/terminal/common/terminal.ts | 39 +- .../terminal/common/terminalConfiguration.ts | 66 +- .../terminal/common/terminalContextKey.ts | 12 + .../terminal/common/terminalEnvironment.ts | 4 +- .../terminal/common/terminalStorageKeys.ts | 2 + .../terminal/common/terminalStrings.ts | 8 +- .../terminal/electron-sandbox/localPty.ts | 79 +- .../electron-sandbox/localTerminalService.ts | 44 +- .../terminalNativeContribution.ts | 4 + .../browser/addons/lineDataEventAddon.test.ts | 71 + ...terminalValidatedLocalLinkProvider.test.ts | 5 +- .../test/browser/terminalConfigHelper.test.ts | 2 +- .../test/browser/terminalInstance.test.ts | 202 + .../browser/terminalProcessManager.test.ts | 9 +- .../hierarchalByLocation.ts | 9 +- .../browser/explorerProjections/nodeHelper.ts | 2 +- .../contrib/testing/browser/media/testing.css | 14 +- .../testing/browser/testExplorerActions.ts | 129 +- .../testing/browser/testing.contribution.ts | 27 +- .../testing/browser/testingDecorations.ts | 656 +- .../testing/browser/testingExplorerFilter.ts | 151 +- .../testing/browser/testingExplorerView.ts | 33 +- .../testing/browser/testingOutputPeek.ts | 106 +- .../browser/testingOutputTerminalService.ts | 39 +- .../browser/testingProgressUiService.ts | 59 +- .../contrib/testing/browser/theme.ts | 18 +- .../contrib/testing/common/constants.ts | 1 + .../testing/common/getComputedState.ts | 6 +- .../testing/common/ownedTestCollection.ts | 3 +- .../contrib/testing/common/testCollection.ts | 9 +- .../testing/common/testExplorerFilterState.ts | 191 + .../testing/common/testResultService.ts | 9 + .../testing/common/testingDecorations.ts | 131 + .../testing/common/testingPeekOpener.ts | 7 + .../contrib/testing/common/testingStates.ts | 4 +- .../common/testExplorerFilterState.test.ts | 80 + .../themes/browser/themes.contribution.ts | 26 +- .../browser/themes.test.contribution.ts | 4 +- .../contrib/timeline/browser/timelinePane.ts | 2 +- .../browser/typeHierarchy.contribution.ts | 4 +- .../update/browser/releaseNotesEditor.ts | 5 +- .../update/browser/update.contribution.ts | 2 +- .../contrib/update/browser/update.ts | 4 +- .../userDataSync/browser/userDataSync.ts | 15 +- .../browser/userDataSyncMergesView.ts | 16 +- .../browser/userDataSyncTrigger.ts | 4 +- .../userDataSync/browser/userDataSyncViews.ts | 66 +- .../contrib/watermark/browser/watermark.ts | 29 +- .../contrib/webview/browser/pre/main.js | 75 +- .../webview/browser/resourceLoading.ts | 2 +- .../contrib/webview/browser/webview.ts | 5 +- .../contrib/webview/browser/webviewElement.ts | 62 +- .../webviewPanel/browser/webviewCommands.ts | 2 +- .../browser/webviewEditorInput.ts | 8 +- .../browser/webviewPanel.contribution.ts | 5 +- .../browser/webviewWorkbenchService.ts | 5 +- .../webviewView/browser/webviewViewPane.ts | 22 +- .../browser/welcomeBanner.contribution.ts | 53 + .../welcome/common/newFile.contribution.ts | 4 +- .../browser/gettingStarted.contribution.ts | 33 +- .../gettingStarted/browser/gettingStarted.css | 23 +- .../gettingStarted/browser/gettingStarted.ts | 324 +- .../browser/gettingStartedColors.ts | 4 +- .../browser/gettingStartedExtensionPoint.ts | 17 +- .../browser/gettingStartedInput.ts | 6 +- .../browser/gettingStartedList.ts | 24 +- .../browser/gettingStartedService.ts | 31 +- .../common/gettingStartedContent.ts | 189 +- .../common/media/example_markdown_media.ts | 8 +- .../common/media/extensions-web.svg | 216 + .../gettingStarted/common/media/menuBar.svg | 100 + .../browser/telemetryOptOut.ts | 6 +- .../browser/editor/editorWalkThrough.ts | 6 +- .../editor/vs_code_editor_walkthrough.ts | 19 +- .../browser/walkThrough.contribution.ts | 8 +- .../walkThrough/browser/walkThroughInput.ts | 8 +- .../walkThrough/browser/walkThroughPart.ts | 10 +- .../common/walkThroughContentProvider.ts | 13 +- .../browser/workspace.contribution.ts | 42 +- .../browser/workspaceTrustEditor.css | 1 + .../workspace/browser/workspaceTrustEditor.ts | 88 +- .../electron-browser/desktop.main.ts | 34 +- .../actions/developerActions.ts | 4 +- .../electron-sandbox/actions/windowActions.ts | 24 +- .../electron-sandbox/desktop.contribution.ts | 10 +- .../electron-sandbox/desktop.main.ts | 35 +- .../sandbox.simpleservices.ts | 310 - .../electron-sandbox/shared.desktop.main.ts | 47 +- src/vs/workbench/electron-sandbox/window.ts | 23 +- .../electron-sandbox/accessibilityService.ts | 2 +- .../activity/browser/activityService.ts | 17 +- .../activityBar/browser/activityBarService.ts | 39 - .../browser/authenticationService.ts | 13 +- .../services/banner/browser/bannerService.ts | 3 +- .../configuration/browser/configuration.ts | 19 +- .../browser/configurationService.ts | 13 +- .../common/configurationEditingService.ts | 26 +- .../common/configurationModels.ts | 7 + .../common/jsonEditingService.ts | 2 +- .../test/browser/configurationService.test.ts | 70 +- .../credentials/browser/credentialsService.ts | 12 +- .../credentials/common/credentials.ts | 1 + .../electron-sandbox/credentialsService.ts | 4 + .../test/browser/credentialsService.test.ts | 60 + .../decorations/browser/decorationsService.ts | 371 +- .../{browser => common}/decorations.ts | 2 + .../test/browser/decorationsService.test.ts | 7 +- .../browser/abstractFileDialogService.ts | 18 +- .../dialogs/browser/fileDialogService.ts | 77 +- .../dialogs/browser/simpleFileDialog.ts | 21 +- .../electron-sandbox/fileDialogService.ts | 10 +- .../dialogs/test/fileDialogService.test.ts | 19 +- .../editor/browser/codeEditorService.ts | 4 +- .../editor/browser/editorResolverService.ts | 116 +- .../services/editor/browser/editorService.ts | 165 +- .../editor/common/editorGroupColumn.ts | 32 +- .../editor/common/editorGroupFinder.ts | 70 +- .../editor/common/editorGroupsService.ts | 112 +- .../editor/common/editorResolverService.ts | 25 +- .../services/editor/common/editorService.ts | 40 +- .../test/browser/editorGroupsService.test.ts | 128 +- .../browser/editorResolverService.test.ts | 279 +- .../editor/test/browser/editorService.test.ts | 569 +- .../test/browser/editorsObserver.test.ts | 32 +- .../environment/browser/environmentService.ts | 11 +- .../experiment/common/experimentService.ts | 4 +- .../electron-browser/experimentService.ts | 257 - .../browser/extensionBisect.ts | 22 +- .../browser/extensionEnablementService.ts | 13 +- .../common/extensionManagement.ts | 12 +- .../extensionManagementServerService.ts | 16 +- .../common/extensionManagementService.ts | 105 +- .../remoteExtensionManagementService.ts | 31 - .../common/webExtensionManagementService.ts | 57 +- .../common/webExtensionsScannerService.ts | 113 +- .../extensionManagementServerService.ts | 2 +- .../extensionManagementService.ts | 4 +- .../remoteExtensionManagementService.ts | 109 +- .../extensionEnablementService.test.ts | 16 +- .../extensionIgnoredRecommendationsService.ts | 10 +- .../common/workspaceExtensionsConfig.ts | 8 +- .../browser/extensionResourceLoaderService.ts | 10 +- .../extensions/browser/extensionService.ts | 16 +- .../extensions/browser/extensionUrlHandler.ts | 45 +- .../browser/webWorkerExtensionHost.ts | 11 +- .../common/abstractExtensionService.ts | 161 +- .../extensions/common/extensionHostMain.ts | 14 +- .../extensions/common/extensionHostManager.ts | 79 +- .../extensionManifestPropertiesService.ts | 16 +- .../services/extensions/common/extensions.ts | 41 + .../extensions/common/extensionsRegistry.ts | 4 +- .../extensions/common/proxyIdentifier.ts | 2 +- .../extensions/common/remoteConsoleUtil.ts | 31 +- .../extensions/common/remoteExtensionHost.ts | 47 +- .../services/extensions/common/rpcProtocol.ts | 14 +- .../electron-browser/extensionService.ts | 25 +- .../localProcessExtensionHost.ts | 341 +- .../electron-sandbox/extensionHostStarter.ts | 9 + .../node/extensionHostProcessSetup.ts | 7 +- .../services/extensions/node/proxyResolver.ts | 2 +- ...extensionManifestPropertiesService.test.ts | 44 +- .../extensions/worker/extensionHostWorker.ts | 68 +- .../diskFileSystemProvider.ts | 4 + .../diskFileSystemProvider.ts | 172 + .../electron-sandbox/parcelWatcherService.ts | 45 + .../common/filesConfigurationService.ts | 14 +- .../services/history/browser/history.ts | 268 +- .../services/history/common/history.ts | 7 +- .../history/test/browser/history.test.ts | 3 +- .../workbench/services/hover/browser/hover.ts | 6 +- .../services/hover/browser/hoverService.ts | 4 +- .../services/hover/browser/hoverWidget.ts | 3 +- .../keybinding/browser/keybindingService.ts | 162 +- .../browser/keyboardLayouts/de.linux.ts | 2 +- .../browser/keyboardLayouts/en.linux.ts | 4 +- .../browser/keyboardLayouts/es.linux.ts | 2 +- .../browser/keyboardLayouts/fr.linux.ts | 2 +- .../browser/keyboardLayouts/ru.linux.ts | 2 +- .../keybinding/common/keybindingEditing.ts | 2 +- .../keybinding/common/keybindingIO.ts | 3 +- .../common/macLinuxFallbackKeyboardMapper.ts | 86 +- .../common/macLinuxKeyboardMapper.ts | 174 +- .../common/windowsKeyboardMapper.ts | 315 +- .../electron-sandbox/nativeKeyboardLayout.ts | 11 +- .../test/browser/keybindingEditing.test.ts | 28 +- .../test/browser/keybindingIO.test.ts | 92 +- .../keyboardMapperTestUtils.ts | 7 +- .../macLinuxFallbackKeyboardMapper.test.ts | 20 +- .../macLinuxKeyboardMapper.test.ts | 116 +- .../test/electron-browser/mac_de_ch.txt | 16 +- .../test/electron-browser/mac_en_us.txt | 4 +- .../test/electron-browser/mac_zh_hant.txt | 64 +- .../test/electron-browser/mac_zh_hant2.js | 1188 ++++ .../test/electron-browser/mac_zh_hant2.txt | 507 ++ .../windowsKeyboardMapper.test.ts | 46 +- .../services/label/common/labelService.ts | 5 +- .../services/label/test/browser/label.test.ts | 8 + .../browser/languageDetectionSimpleWorker.ts | 87 +- .../languageDetectionWorkerServiceImpl.ts | 4 +- .../common/languageDetectionWorkerService.ts | 4 +- .../common/languageStatusService.ts | 33 +- .../services/layout/browser/layoutService.ts | 24 +- .../lifecycle/browser/lifecycleService.ts | 4 +- .../mode/common/workbenchModeService.ts | 15 +- .../model/common/workbenchModelService.ts | 41 + .../panecomposite/browser/panecomposite.ts | 73 + .../services/panel/common/panelService.ts | 72 - .../browser/keybindingsEditorInput.ts | 4 +- .../browser/keybindingsEditorModel.ts | 2 +- .../preferences/browser/preferencesService.ts | 24 +- .../preferences/common/preferences.ts | 8 +- .../common/preferencesEditorInput.ts | 4 +- .../preferences/common/preferencesModels.ts | 13 +- .../browser/keybindingsEditorModel.test.ts | 27 +- .../test/browser/preferencesService.test.ts | 9 +- .../progress/browser/progressIndicator.ts | 18 +- .../progress/browser/progressService.ts | 39 +- .../test/browser/progressIndicator.test.ts | 41 +- .../remote/browser/tunnelServiceImpl.ts | 4 +- .../common/remoteAgentEnvironmentChannel.ts | 2 + .../common/remoteAgentFileSystemChannel.ts | 49 +- .../remote/common/remoteExplorerService.ts | 109 +- .../electron-browser/tunnelServiceImpl.ts | 35 - .../remoteAgentServiceImpl.ts | 47 +- .../electron-sandbox/tunnelServiceImpl.ts | 108 + .../services/search/browser/searchService.ts | 167 +- .../search/common/fileSearchManager.ts | 2 +- .../services/search/common/getFileResults.ts | 128 + .../services/search/common/ignoreFile.ts | 126 + .../common/localFileSearchWorkerTypes.ts | 30 + .../services/search/common/search.ts | 9 + .../services/search/common/searchService.ts | 4 +- .../search/node/ripgrepTextSearchEngine.ts | 10 +- .../search/test/common/ignoreFile.test.ts | 567 ++ ...ts => rawSearchService.integrationTest.ts} | 0 ...s => ripgrepTextSearchEngineUtils.test.ts} | 2 + ...arch.test.ts => search.integrationTest.ts} | 0 .../services/search/worker/localFileSearch.ts | 328 + .../electron-sandbox/sharedProcessService.ts | 42 +- .../sharedProcessWorkerWorkbenchService.ts | 120 + .../{common => browser}/statusbar.ts | 34 +- .../telemetry/browser/telemetryService.ts | 14 +- .../electron-sandbox/telemetryService.ts | 16 +- .../browser/abstractTextMateService.ts | 74 +- .../textMate/common/TMGrammarFactory.ts | 19 +- .../textMate/common/TMScopeRegistry.ts | 2 +- .../textMate/common/textMateService.ts | 5 +- .../electron-sandbox/textMateService.ts | 16 +- .../electron-sandbox/textMateWorker.ts | 33 +- .../browser/browserTextFileService.ts | 6 +- .../textfile/browser/textFileService.ts | 104 +- .../textfile/common/textEditorService.ts | 246 + .../textfile/common/textFileEditorModel.ts | 25 +- .../common/textFileEditorModelManager.ts | 117 +- .../services/textfile/common/textfiles.ts | 16 +- .../electron-sandbox/nativeTextFileService.ts | 6 +- .../browser/browserTextFileService.io.test.ts | 2 +- .../test/browser/textEditorService.test.ts | 235 + .../test/browser/textFileEditorModel.test.ts | 8 +- .../textFileEditorModelManager.test.ts | 143 +- .../test/browser/textFileService.test.ts | 6 +- .../test/common/textFileService.io.test.ts | 27 +- .../nativeTextFileService.io.test.ts | 2 +- .../nativeTextFileService.test.ts | 2 +- .../node/encoding/encoding.integrationTest.ts | 15 + .../common/textModelResolverService.ts | 10 +- .../browser/textModelResolverService.test.ts | 6 +- .../browser/browserHostColorSchemeService.ts | 6 +- .../themes/browser/fileIconThemeData.ts | 4 +- .../themes/browser/productIconThemeData.ts | 2 +- .../nativeHostColorSchemeService.ts | 73 +- .../services/timer/browser/timerService.ts | 11 +- .../timer/electron-sandbox/timerService.ts | 8 +- .../common/untitledTextEditorHandler.ts | 8 +- .../common/untitledTextEditorInput.ts | 13 +- .../common/untitledTextEditorModel.ts | 21 +- .../test/browser/untitledTextEditor.test.ts | 6 +- .../uriIdentity/common/uriIdentityService.ts | 4 +- .../test/common/uriIdentityService.test.ts | 2 +- .../services/userData/browser/userDataInit.ts | 2 +- .../browser/userDataSyncWorkbenchService.ts | 2 +- .../services/viewlet/browser/viewlet.ts | 57 - .../views/browser/viewDescriptorService.ts | 38 +- .../views/common/viewContainerModel.ts | 187 +- .../test/browser/viewContainerModel.test.ts | 75 +- .../browser/viewDescriptorService.test.ts | 6 +- .../browser/workingCopyBackupTracker.ts | 6 +- .../common/abstractFileWorkingCopyManager.ts | 4 +- .../common/fileWorkingCopyManager.ts | 100 +- .../common/storedFileWorkingCopy.ts | 22 +- .../common/storedFileWorkingCopyManager.ts | 204 +- .../common/untitledFileWorkingCopy.ts | 4 +- .../common/untitledFileWorkingCopyManager.ts | 6 +- .../common/workingCopyBackupService.ts | 4 +- .../common/workingCopyBackupTracker.ts | 13 +- .../common/workingCopyEditorService.ts | 9 +- .../common/workingCopyFileService.ts | 2 +- .../workingCopyBackupTracker.ts | 20 +- .../browser/fileWorkingCopyManager.test.ts | 8 +- .../test/browser/resourceWorkingCopy.test.ts | 6 +- .../browser/storedFileWorkingCopy.test.ts | 7 +- .../storedFileWorkingCopyManager.test.ts | 100 +- .../browser/untitledFileWorkingCopy.test.ts | 7 +- .../untitledFileWorkingCopyManager.test.ts | 12 +- .../browser/workingCopyBackupTracker.test.ts | 13 +- .../browser/workingCopyEditorService.test.ts | 3 +- .../browser/workingCopyFileService.test.ts | 6 +- .../workingCopyBackupService.test.ts | 4 +- .../workingCopyBackupTracker.test.ts | 23 +- .../abstractWorkspaceEditingService.ts | 2 +- .../browser/workspaceTrustEditorInput.ts | 4 +- .../workspaces/common/workspaceTrust.ts | 18 +- .../browser/api/extHostApiCommands.test.ts | 75 +- .../browser/api/extHostAuthentication.test.ts | 358 ++ .../test/browser/api/extHostBulkEdits.test.ts | 2 +- .../browser/api/extHostDiagnostics.test.ts | 91 +- .../extHostDocumentSaveParticipant.test.ts | 30 +- .../api/extHostDocumentsAndEditors.test.ts | 2 +- .../api/extHostLanguageFeatures.test.ts | 45 +- .../api/extHostMessagerService.test.ts | 3 +- .../test/browser/api/extHostNotebook.test.ts | 2 +- .../browser/api/extHostNotebookKernel.test.ts | 5 +- .../test/browser/api/extHostTesting.test.ts | 90 +- .../test/browser/api/extHostTypes.test.ts | 16 + .../test/browser/api/extHostWebview.test.ts | 23 + .../api/mainThreadAuthentication.test.ts | 162 - ...mainThreadDocumentContentProviders.test.ts | 1 + .../browser/api/mainThreadDocuments.test.ts | 72 +- .../api/mainThreadDocumentsAndEditors.test.ts | 93 +- .../browser/api/mainThreadEditors.test.ts | 30 +- .../browser/api/mainThreadTreeViews.test.ts | 6 +- .../workbench/test/browser/codeeditor.test.ts | 6 +- .../parts/editor/diffEditorInput.test.ts | 59 +- .../test/browser/parts/editor/editor.test.ts | 163 +- .../parts/editor/editorDiffModel.test.ts | 9 +- .../parts/editor/editorGroupModel.test.ts | 281 +- .../browser/parts/editor/editorInput.test.ts | 3 +- .../browser/parts/editor/editorModel.test.ts | 7 +- .../browser/parts/editor/editorPane.test.ts | 15 +- .../parts/editor/resourceEditorInput.test.ts | 10 +- .../editor/sideBySideEditorInput.test.ts | 109 +- .../editor/textResourceEditorInput.test.ts | 17 +- .../parts/statusbar/statusbarModel.test.ts | 226 + .../test/browser/quickAccess.test.ts | 8 +- src/vs/workbench/test/browser/viewlet.test.ts | 28 +- .../test/browser/workbenchTestServices.ts | 259 +- .../api/extHostSearch.test.ts | 37 + .../api/mainThreadWorkspace.test.ts | 9 +- .../colorRegistry.releaseTest.ts | 8 +- .../test/electron-browser/testing.ts | 19 + .../textsearch.perf.integrationTest.ts | 57 +- .../electron-browser/workbenchTestServices.ts | 17 +- src/vs/workbench/workbench.common.main.ts | 13 +- src/vs/workbench/workbench.desktop.main.ts | 2 +- .../workbench.desktop.sandbox.main.ts | 1 + src/vs/workbench/workbench.sandbox.main.ts | 3 +- src/vs/workbench/workbench.web.api.ts | 101 +- src/vs/workbench/workbench.web.main.ts | 5 +- test/automation/src/editor.ts | 9 - test/automation/src/extensions.ts | 1 + test/automation/src/playwrightDriver.ts | 56 +- test/integration/browser/README.md | 10 +- test/integration/browser/src/index.ts | 26 +- test/monaco/package.json | 3 +- test/monaco/webpack.config.js | 6 +- test/monaco/yarn.lock | 5 + test/smoke/README.md | 6 +- test/smoke/src/areas/search/search.test.ts | 6 +- .../src/areas/statusbar/statusbar.test.ts | 12 - test/smoke/src/utils.ts | 26 +- test/unit/electron/index.js | 4 + test/unit/electron/renderer.js | 44 +- test/unit/fullJsonStreamReporter.js | 2 + tsfmt.json | 3 +- yarn.lock | 306 +- 1876 files changed, 72050 insertions(+), 37997 deletions(-) mode change 100644 => 100755 .eslintrc.json delete mode 100644 .vscode/notebooks/grooming.github-issues create mode 100644 build/azure-pipelines/upload-nlsmetadata.js create mode 100644 build/azure-pipelines/upload-nlsmetadata.ts create mode 100644 extensions/markdown-language-features/src/util/openDocumentLink.ts create mode 100644 extensions/markdown-language-features/test-workspace/sub/foo.txt create mode 100644 extensions/search-result/images/icon.png mode change 100644 => 100755 package.json mode change 100644 => 100755 remote/package.json mode change 100644 => 100755 remote/web/package.json create mode 100644 resources/server/bin-dev/code-web.js create mode 100644 resources/server/bin-dev/code.cmd create mode 100755 resources/server/bin-dev/code.sh create mode 100644 resources/server/bin-dev/helpers/browser.cmd create mode 100755 resources/server/bin-dev/helpers/browser.sh create mode 100644 resources/server/bin-dev/server.bat create mode 100755 resources/server/bin-dev/server.sh create mode 100644 resources/server/bin/code.cmd create mode 100644 resources/server/bin/code.sh create mode 100644 resources/server/bin/helpers/browser.cmd create mode 100644 resources/server/bin/helpers/browser.sh create mode 100644 resources/server/bin/server.cmd create mode 100644 resources/server/bin/server.sh create mode 100644 resources/server/code-192.png create mode 100644 resources/server/code-512.png create mode 100644 resources/server/favicon.ico create mode 100644 resources/server/manifest.json create mode 100644 resources/server/test/test-remote-integration.bat create mode 100755 resources/server/test/test-remote-integration.sh create mode 100644 resources/server/test/test-web-integration.bat create mode 100755 resources/server/test/test-web-integration.sh create mode 100644 resources/server/web.bat create mode 100755 resources/server/web.sh mode change 100644 => 100755 src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts create mode 100644 src/vs/base/browser/dompurify/cgmanifest.json create mode 100644 src/vs/base/browser/dompurify/dompurify.d.ts create mode 100644 src/vs/base/browser/dompurify/dompurify.js create mode 100644 src/vs/base/browser/dompurify/dompurify.license.txt delete mode 100644 src/vs/base/common/insane/cgmanifest.json delete mode 100644 src/vs/base/common/insane/insane.d.ts delete mode 100644 src/vs/base/common/insane/insane.js delete mode 100644 src/vs/base/common/insane/insane.license.txt create mode 100644 src/vs/base/common/keybindings.ts delete mode 100644 src/vs/base/common/scanCode.ts create mode 100644 src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts rename src/vs/base/parts/ipc/test/node/{ipc.cp.test.ts => ipc.cp.integrationTest.ts} (100%) delete mode 100644 src/vs/base/test/common/codicon.test.ts rename src/vs/{editor/contrib/inlineCompletions/test => base/test/common}/timeTravelScheduler.ts (89%) rename src/vs/base/test/node/processes/{processes.test.ts => processes.integrationTest.ts} (100%) create mode 100644 src/vs/editor/common/controller/cursorColumns.ts delete mode 100644 src/vs/editor/common/model/bracketPairColorizer/ast.ts delete mode 100644 src/vs/editor/common/model/bracketPairColorizer/bracketPairColorizer.ts delete mode 100644 src/vs/editor/common/model/bracketPairColorizer/brackets.ts delete mode 100644 src/vs/editor/common/model/bracketPairColorizer/concat23Trees.ts create mode 100644 src/vs/editor/common/model/bracketPairs/bracketPairs.ts create mode 100644 src/vs/editor/common/model/bracketPairs/bracketPairsImpl.ts create mode 100644 src/vs/editor/common/model/bracketPairs/bracketPairsTree/ast.ts rename src/vs/editor/common/model/{bracketPairColorizer => bracketPairs/bracketPairsTree}/beforeEditPositionMapper.ts (100%) create mode 100644 src/vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree.ts create mode 100644 src/vs/editor/common/model/bracketPairs/bracketPairsTree/brackets.ts create mode 100644 src/vs/editor/common/model/bracketPairs/bracketPairsTree/concat23Trees.ts rename src/vs/editor/common/model/{bracketPairColorizer => bracketPairs/bracketPairsTree}/length.ts (99%) rename src/vs/editor/common/model/{bracketPairColorizer => bracketPairs/bracketPairsTree}/nodeReader.ts (81%) rename src/vs/editor/common/model/{bracketPairColorizer => bracketPairs/bracketPairsTree}/parser.ts (64%) rename src/vs/editor/common/model/{bracketPairColorizer => bracketPairs/bracketPairsTree}/smallImmutableSet.ts (84%) rename src/vs/editor/common/model/{bracketPairColorizer => bracketPairs/bracketPairsTree}/tokenizer.ts (81%) create mode 100644 src/vs/editor/common/model/bracketPairs/colorizedBracketPairsDecorationProvider.ts delete mode 100644 src/vs/editor/common/modes/abstractMode.ts delete mode 100644 src/vs/editor/common/modes/tokenization/typescript.ts delete mode 100644 src/vs/editor/contrib/hover/hoverWidgets.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/inlineCompletionToGhostText.ts delete mode 100644 src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/suggestWidgetInlineCompletionProvider.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel.ts create mode 100644 src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts create mode 100644 src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts create mode 100644 src/vs/editor/test/common/modes/testLanguageConfigurationService.ts create mode 100644 src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts rename src/vs/platform/accessibility/{common => browser}/accessibilityService.ts (96%) create mode 100644 src/vs/platform/browser/historyWidgetKeybindingHint.ts create mode 100644 src/vs/platform/extensions/common/extensionHostStarter.ts create mode 100644 src/vs/platform/extensions/node/extensionHostStarter.ts create mode 100644 src/vs/platform/files/common/diskFileSystemProvider.ts create mode 100644 src/vs/platform/files/common/watcher.ts create mode 100644 src/vs/platform/files/electron-main/diskFileSystemProviderIpc.ts create mode 100644 src/vs/platform/files/node/diskFileSystemProviderIpc.ts delete mode 100644 src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts create mode 100644 src/vs/platform/files/node/watcher/parcel/parcelWatcherService.ts rename src/vs/platform/files/node/watcher/{unix => parcel}/watcherApp.ts (79%) create mode 100644 src/vs/platform/files/node/watcher/parcel/watcherService.ts delete mode 100644 src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts delete mode 100644 src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts delete mode 100644 src/vs/platform/files/node/watcher/unix/watcher.ts delete mode 100644 src/vs/platform/files/node/watcher/unix/watcherService.ts delete mode 100644 src/vs/platform/files/node/watcher/watcher.ts delete mode 100644 src/vs/platform/files/node/watcher/win32/CodeHelper.exe delete mode 100644 src/vs/platform/files/node/watcher/win32/CodeHelper.md delete mode 100644 src/vs/platform/files/node/watcher/win32/csharpWatcherService.ts delete mode 100644 src/vs/platform/files/node/watcher/win32/watcherService.ts rename src/vs/platform/files/test/{electron-browser => node}/diskFileService.test.ts (99%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/examples/company.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/examples/conway.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/examples/employee.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/examples/small.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/index.html (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/other/deep/company.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/other/deep/conway.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/other/deep/employee.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/other/deep/small.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/resolver/site.css (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/binary.txt (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/deep/company.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/deep/conway.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/deep/employee.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/deep/small.js (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/index.html (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/lorem.txt (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/small.txt (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/small_umlaut.txt (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/some_utf16le.css (100%) rename src/vs/platform/files/test/{electron-browser => node}/fixtures/service/some_utf8_bom.txt (100%) create mode 100644 src/vs/platform/files/test/node/recursiveWatcher.integrationTest.ts rename src/vs/platform/files/test/{electron-browser/normalizer.test.ts => node/watcherNormalizer.test.ts} (51%) create mode 100644 src/vs/platform/layout/browser/zIndexRegistry.ts create mode 100644 src/vs/platform/remote/common/sharedProcessTunnelService.ts create mode 100644 src/vs/platform/remote/electron-sandbox/sharedProcessTunnelService.ts create mode 100644 src/vs/platform/remote/node/sharedProcessTunnelService.ts create mode 100644 src/vs/platform/sharedProcess/common/sharedProcessWorkerService.ts create mode 100644 src/vs/platform/sharedProcess/electron-browser/sharedProcessWorker.ts create mode 100644 src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts create mode 100644 src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService.ts create mode 100644 src/vs/server/cli.js create mode 100644 src/vs/server/extensionHostConnection.ts create mode 100644 src/vs/server/main.js create mode 100644 src/vs/server/remoteAgentEnvironmentImpl.ts create mode 100644 src/vs/server/remoteCli.ts create mode 100644 src/vs/server/remoteExtensionHostAgent.ts create mode 100644 src/vs/server/remoteExtensionHostAgentCli.ts create mode 100644 src/vs/server/remoteExtensionHostAgentServer.ts create mode 100644 src/vs/server/remoteExtensionHostProcess.ts create mode 100644 src/vs/server/remoteExtensionManagement.ts create mode 100644 src/vs/server/remoteFileSystemProviderIpc.ts create mode 100644 src/vs/server/remoteLanguagePacks.ts create mode 100644 src/vs/server/remoteTelemetryService.ts create mode 100644 src/vs/server/remoteTerminalChannel.ts create mode 100644 src/vs/server/remoteUriTransformer.ts create mode 100644 src/vs/server/serverEnvironmentService.ts create mode 100644 src/vs/server/uriTransformer.js create mode 100644 src/vs/server/webClientServer.ts delete mode 100644 src/vs/workbench/browser/panel.ts create mode 100644 src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts create mode 100644 src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts create mode 100644 src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css create mode 100644 src/vs/workbench/browser/parts/editor/editorConfiguration.ts rename src/vs/workbench/browser/parts/editor/{editorControl.ts => editorPanes.ts} (77%) create mode 100644 src/vs/workbench/browser/parts/editor/editorWithViewState.ts create mode 100644 src/vs/workbench/browser/parts/paneCompositePart.ts create mode 100644 src/vs/workbench/browser/parts/panel/media/basepanelpart.css create mode 100644 src/vs/workbench/browser/parts/sidebar/sidebarActions.ts create mode 100644 src/vs/workbench/browser/parts/statusbar/statusbarActions.ts create mode 100644 src/vs/workbench/browser/parts/statusbar/statusbarItem.ts create mode 100644 src/vs/workbench/browser/parts/statusbar/statusbarModel.ts delete mode 100644 src/vs/workbench/browser/viewlet.ts create mode 100644 src/vs/workbench/common/auxiliarybar.ts create mode 100644 src/vs/workbench/contrib/extensions/browser/webRecommendations.ts rename src/vs/workbench/contrib/files/{common => browser}/workspaceWatcher.ts (59%) create mode 100644 src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts create mode 100644 src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css rename src/vs/workbench/contrib/markdown/{common => browser}/markdownDocumentRenderer.ts (61%) create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/controller/editActions.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookLogger.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts rename src/vs/workbench/contrib/notebook/browser/{ => viewParts}/notebookEditorDecorations.ts (100%) rename src/vs/workbench/contrib/notebook/browser/{ => viewParts}/notebookEditorToolbar.ts (93%) rename src/vs/workbench/contrib/notebook/browser/{ => viewParts}/notebookEditorWidgetContextKeys.ts (84%) rename src/vs/workbench/contrib/notebook/browser/{media => viewParts}/notebookKernelActionViewItem.css (100%) rename src/vs/workbench/contrib/notebook/browser/{ => viewParts}/notebookKernelActionViewItem.ts (94%) rename src/vs/workbench/contrib/notebook/{browser/contrib/cellOperations => }/test/cellOperations.test.ts (83%) create mode 100644 src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts create mode 100644 src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.test.ts create mode 100644 src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts create mode 100644 src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts create mode 100644 src/vs/workbench/contrib/testing/common/testingDecorations.ts create mode 100644 src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts create mode 100644 src/vs/workbench/contrib/welcome/banner/browser/welcomeBanner.contribution.ts create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/extensions-web.svg create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/common/media/menuBar.svg delete mode 100644 src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts delete mode 100644 src/vs/workbench/services/activityBar/browser/activityBarService.ts create mode 100644 src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts rename src/vs/workbench/services/decorations/{browser => common}/decorations.ts (96%) delete mode 100644 src/vs/workbench/services/experiment/electron-browser/experimentService.ts delete mode 100644 src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts create mode 100644 src/vs/workbench/services/extensions/electron-sandbox/extensionHostStarter.ts rename src/vs/{platform => workbench/services}/files/electron-browser/diskFileSystemProvider.ts (94%) create mode 100644 src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts create mode 100644 src/vs/workbench/services/files/electron-sandbox/parcelWatcherService.ts create mode 100644 src/vs/workbench/services/keybinding/test/electron-browser/mac_zh_hant2.js create mode 100644 src/vs/workbench/services/keybinding/test/electron-browser/mac_zh_hant2.txt create mode 100644 src/vs/workbench/services/model/common/workbenchModelService.ts create mode 100644 src/vs/workbench/services/panecomposite/browser/panecomposite.ts delete mode 100644 src/vs/workbench/services/panel/common/panelService.ts delete mode 100644 src/vs/workbench/services/remote/electron-browser/tunnelServiceImpl.ts create mode 100644 src/vs/workbench/services/remote/electron-sandbox/tunnelServiceImpl.ts create mode 100644 src/vs/workbench/services/search/common/getFileResults.ts create mode 100644 src/vs/workbench/services/search/common/ignoreFile.ts create mode 100644 src/vs/workbench/services/search/common/localFileSearchWorkerTypes.ts create mode 100644 src/vs/workbench/services/search/test/common/ignoreFile.test.ts rename src/vs/workbench/services/search/test/electron-browser/{rawSearchService.test.ts => rawSearchService.integrationTest.ts} (100%) rename src/vs/workbench/services/search/test/node/{ripgrepTextSearchEngine.test.ts => ripgrepTextSearchEngineUtils.test.ts} (99%) rename src/vs/workbench/services/search/test/node/{search.test.ts => search.integrationTest.ts} (100%) create mode 100644 src/vs/workbench/services/search/worker/localFileSearch.ts rename src/vs/workbench/services/{ipc => sharedProcess}/electron-sandbox/sharedProcessService.ts (57%) create mode 100644 src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService.ts rename src/vs/workbench/services/statusbar/{common => browser}/statusbar.ts (75%) create mode 100644 src/vs/workbench/services/textfile/common/textEditorService.ts create mode 100644 src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts create mode 100644 src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts delete mode 100644 src/vs/workbench/services/viewlet/browser/viewlet.ts create mode 100644 src/vs/workbench/test/browser/api/extHostAuthentication.test.ts delete mode 100644 src/vs/workbench/test/browser/api/mainThreadAuthentication.test.ts create mode 100644 src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts create mode 100644 src/vs/workbench/test/electron-browser/testing.ts mode change 100644 => 100755 yarn.lock diff --git a/.devcontainer/cache/before-cache.sh b/.devcontainer/cache/before-cache.sh index cfa7acc9d9..9548a154c3 100755 --- a/.devcontainer/cache/before-cache.sh +++ b/.devcontainer/cache/before-cache.sh @@ -1,11 +1,11 @@ -#!/bin/bash +#!/usr/bin/env bash -# This file establishes a basline for the reposuitory before any steps in the "prepare.sh" +# This file establishes a basline for the repository before any steps in the "prepare.sh" # are run. Its just a find command that filters out a few things we don't need to watch. set -e -SCRIPT_PATH="$(cd "$(dirname $0)" && pwd)" +SCRIPT_PATH="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" SOURCE_FOLDER="${1:-"."}" cd "${SOURCE_FOLDER}" diff --git a/.devcontainer/cache/build-cache-image.sh b/.devcontainer/cache/build-cache-image.sh index 42e143d7af..865b860898 100755 --- a/.devcontainer/cache/build-cache-image.sh +++ b/.devcontainer/cache/build-cache-image.sh @@ -1,12 +1,12 @@ #!/bin/bash -# This file simply wraps the dockeer build command used to build the image with the -# cached result of the commands from "prepare.sh" and pushes it to the specified -# container image registry. +# This file simply wraps the docker build command to build an image that includes +# a cache.tar file with the result of "prepare.sh" inside of it. See cache.Dockerfile +# for the steps that are actually taken to do this. set -e -SCRIPT_PATH="$(cd "$(dirname $0)" && pwd)" +SCRIPT_PATH="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" CONTAINER_IMAGE_REPOSITORY="$1" BRANCH="${2:-"main"}" diff --git a/.devcontainer/cache/cache-diff.sh b/.devcontainer/cache/cache-diff.sh index 362337ce6e..3f8b77e560 100755 --- a/.devcontainer/cache/cache-diff.sh +++ b/.devcontainer/cache/cache-diff.sh @@ -1,12 +1,11 @@ -#!/bin/bash +#!/usr/bin/env bash # This file is used to archive off a copy of any differences in the source tree into another location -# in the image. Once the codespace is up, this will be restored into its proper location (which is -# quick and happens parallel to other startup activities) +# in the image. Once the codespace / container is up, this will be restored into its proper location. set -e -SCRIPT_PATH="$(cd "$(dirname $0)" && pwd)" +SCRIPT_PATH="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" SOURCE_FOLDER="${1:-"."}" CACHE_FOLDER="${2:-"/usr/local/etc/devcontainer-cache"}" diff --git a/.devcontainer/cache/cache.Dockerfile b/.devcontainer/cache/cache.Dockerfile index 79af3ee8a3..a2c2866fe2 100644 --- a/.devcontainer/cache/cache.Dockerfile +++ b/.devcontainer/cache/cache.Dockerfile @@ -1,7 +1,8 @@ -# This dockerfile is used to build up from a base image to create an image with cached results of running "prepare.sh". +# This dockerfile is used to build up from a base image to create an image a cache.tar file containing the results of running "prepare.sh". # Other image contents: https://github.com/microsoft/vscode-dev-containers/blob/master/repository-containers/images/github.com/microsoft/vscode/.devcontainer/base.Dockerfile -FROM mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:dev +# This first stage generates cache.tar +FROM mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:dev as cache ARG USERNAME=node COPY --chown=${USERNAME}:${USERNAME} . /repo-source-tmp/ RUN mkdir /usr/local/etc/devcontainer-cache \ @@ -10,5 +11,12 @@ RUN mkdir /usr/local/etc/devcontainer-cache \ cd /repo-source-tmp \ && .devcontainer/cache/before-cache.sh \ && .devcontainer/prepare.sh \ - && .devcontainer/cache/cache-diff.sh" \ - && rm -rf /repo-source-tmp + && .devcontainer/cache/cache-diff.sh" + +# This second stage starts fresh and just copies in cache.tar from the previous stage. The related +# devcontainer.json file is then setup to have postCreateCommand fire restore-diff.sh to expand it. +FROM mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:dev as dev-container +ARG USERNAME=node +ARG CACHE_FOLDER="/usr/local/etc/devcontainer-cache" +RUN mkdir -p "${CACHE_FOLDER}" && chown "${USERNAME}:${USERNAME}" "${CACHE_FOLDER}" +COPY --from=cache ${CACHE_FOLDER}/cache.tar ${CACHE_FOLDER}/ diff --git a/.devcontainer/cache/restore-diff.sh b/.devcontainer/cache/restore-diff.sh index 2f418d8748..827afc45ab 100755 --- a/.devcontainer/cache/restore-diff.sh +++ b/.devcontainer/cache/restore-diff.sh @@ -1,9 +1,8 @@ -#!/bin/bash +#!/usr/bin/env bash -# This file restores the results of the "prepare.sh" into their proper locations -# once the container has been created. It runs as a postCreateCommand which -# in GitHub Codespaces occurs parallel to other startup activities and does not -# really add to the overal startup time given how quick the operation ends up being. +# This file expands the cache.tar file in the image that contains the results of "prepare.sh" +# on top of the source tree. It runs as a postCreateCommand which runs after the container/codespace +# is already up where you would typically run a command like "yarn install". set -e diff --git a/.devcontainer/prepare.sh b/.devcontainer/prepare.sh index 47a77a533a..9b5c81ff40 100755 --- a/.devcontainer/prepare.sh +++ b/.devcontainer/prepare.sh @@ -1,10 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash -# This file contains the steps that should be run when creating the intermediary image that contains -# contents for that should be in the image by default. It will be used to build up from the base image -# to create an image that speeds up first time use of the dev container by "caching" the results -# of these commands. Developers can still run these commands without an issue once the container is -# up, but only differences will be processed which also speeds up the first time these operations occur. +# This file contains the steps that should be run when building a "cache" image with contents that should be +# layered directly **on top of the source tree** once a dev container is created. This avoids having to run long +# running commands like "yarn install" from the ground up. Developers (and should) still run these commands +# after the actual dev container is created, but only differences will be processed. yarn install yarn electron diff --git a/.eslintignore b/.eslintignore index 37a61b4065..d9bd42093e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,7 +3,7 @@ **/vs/css.build.js **/vs/css.js **/vs/loader.js -**/insane/** +**/dompurify/** **/marked/** **/semver/** **/test/**/*.js @@ -21,5 +21,5 @@ /test/automation/out # These files are not linted by `yarn eslint`, so we exclude them from being linted in the editor. -# This ensures that if we add new rules and they pass CI, the are also no errors in the editor. +# This ensures that if we add new rules and they pass CI, there are also no errors in the editor. /resources/web/code-web.js diff --git a/.eslintrc.json b/.eslintrc.json old mode 100644 new mode 100755 index 2681ae4bbc..086d9e4423 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,6 +20,7 @@ "no-duplicate-case": "warn", "no-duplicate-imports": "warn", "no-eval": "warn", + "no-async-promise-executor": "off", "no-extra-semi": "warn", "no-new-wrappers": "warn", "no-redeclare": "off", @@ -134,7 +135,7 @@ "restrictions": [ "vs/nls", "**/{vs,sql}/base/{common,node}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -176,7 +177,7 @@ "vs/nls", "**/{vs,sql}/base/{common,node}/**", "**/{vs,sql}/base/parts/*/{common,node}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -195,7 +196,7 @@ "vs/css!./**/*", "**/{vs,sql}/base/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -204,7 +205,7 @@ "vs/nls", "**/{vs,sql}/base/{common,node,electron-main}/**", "**/{vs,sql}/base/parts/*/{common,node,electron-main}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -254,7 +255,7 @@ "**/{vs,sql}/base/{common,node}/**", "**/{vs,sql}/base/parts/*/{common,node}/**", "**/{vs,sql}/platform/*/{common,node}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -276,7 +277,7 @@ "**/{vs,sql}/base/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -287,7 +288,7 @@ "**/{vs,sql}/base/{common,node,electron-main}/**", "**/{vs,sql}/base/parts/*/{common,node,electron-main}/**", "**/{vs,sql}/platform/*/{common,node,electron-main}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -524,7 +525,7 @@ "**/{vs,sql}/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/workbench/services/*/{common,browser,node,electron-sandbox,electron-browser}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -539,7 +540,7 @@ "vs/workbench/contrib/files/browser/editors/fileEditorInput", "**/{vs,sql}/workbench/services/**", "**/{vs,sql}/workbench/test/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -615,7 +616,7 @@ "**/{vs,sql}/workbench/{common,node}/**", "**/{vs,sql}/workbench/api/{common,node}/**", "**/{vs,sql}/workbench/services/**/{common,node}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -646,7 +647,7 @@ "**/{vs,sql}/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -785,7 +786,7 @@ "**/{vs,sql}/workbench/api/{common,node}/**", "**/{vs,sql}/workbench/services/**/{common,node}/**", "**/{vs,sql}/workbench/contrib/**/{common,node}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -818,7 +819,7 @@ "**/{vs,sql}/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/workbench/contrib/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -841,7 +842,7 @@ "**/{vs,sql}/base/parts/**/{common,node}/**", "**/{vs,sql}/platform/**/{common,node}/**", "**/{vs,sql}/code/**/{common,node}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -853,7 +854,7 @@ "**/{vs,sql}/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/code/**/{common,browser,node,electron-sandbox,electron-browser}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -864,7 +865,7 @@ "**/{vs,sql}/base/parts/**/{common,node,electron-main}/**", "**/{vs,sql}/platform/**/{common,node,electron-main}/**", "**/{vs,sql}/code/**/{common,node,electron-main}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -876,7 +877,7 @@ "**/{vs,sql}/platform/**/{common,node}/**", "**/{vs,sql}/workbench/**/{common,node}/**", "**/{vs,sql}/server/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -947,28 +948,28 @@ "target": "**/test/smoke/**", "restrictions": [ "**/test/smoke/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { "target": "**/test/automation/**", "restrictions": [ "**/test/automation/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { "target": "**/test/integration/**", "restrictions": [ "**/test/integration/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { "target": "**/test/monaco/**", "restrictions": [ "**/test/monaco/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { @@ -988,7 +989,7 @@ "target": "**/{node,electron-browser,electron-main}/**/*.test.ts", "restrictions": [ "**/{vs,sql}/**", - "@vscode/*", "*", // node modules + "@vscode/*", "@parcel/*", "*", // node modules "@angular/*" // {{SQL CARBON EDIT}} ] }, @@ -996,14 +997,14 @@ "target": "**/{node,electron-browser,electron-main}/**/test/**", "restrictions": [ "**/{vs,sql}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { "target": "**/test/{node,electron-browser,electron-main}/**", "restrictions": [ "**/{vs,sql}/**", - "@vscode/*", "*" // node modules + "@vscode/*", "@parcel/*", "*" // node modules ] }, { diff --git a/.gitattributes b/.gitattributes index 6065a65f70..f6263094d0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,4 +8,4 @@ ThirdPartyNotices.txt eol=crlf *.ps1 eol=lf *.sh eol=lf *.rtf -text -*.json linguist-language=jsonc +**/*.json linguist-language=jsonc diff --git a/.github/subscribers.json b/.github/subscribers.json index 144bcb15a4..8ee4f2678e 100644 --- a/.github/subscribers.json +++ b/.github/subscribers.json @@ -6,7 +6,6 @@ "donjayamanne", "jilljac", "IanMatthewHuff", - "tanhakabir", "dynamicwebpaige" ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d80d86d6b..e0cef08e00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 - uses: actions/setup-python@v2 with: @@ -101,7 +101,7 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 # {{SQL CARBON EDIT}} Skip caching for now # - name: Compute node modules cache key # id: nodeModulesCacheKey @@ -133,7 +133,7 @@ jobs: # Don't inline source maps so that we generate code coverage for ts files - name: Compile and Download - run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions + run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" # {{SQL CARBON EDIT}} Remove unused options playwright-install download-builtin-extensions env: SQL_NO_INLINE_SOURCEMAP: 1 @@ -173,7 +173,7 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 # {{SQL CARBON EDIT}} Skip caching for now # - name: Compute node modules cache key @@ -205,7 +205,7 @@ jobs: run: yarn --frozen-lockfile --network-timeout 180000 - name: Compile and Download - run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions + run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" # {{SQL CARBON EDIT}} Remove unused options playwright-install download-builtin-extensions # This is required for keytar unittests, otherwise we hit # https://github.com/atom/node-keytar/issues/76 @@ -235,7 +235,7 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 - name: Compute node modules cache key id: nodeModulesCacheKey @@ -279,8 +279,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 +# skip while fixing + # - name: Compile /build/ + # run: yarn --cwd build compile - name: Run eslint run: yarn eslint diff --git a/.gitignore b/.gitignore index dbfd09874f..74504885b3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ node_modules/ extensions/**/dist/ /out*/ /extensions/**/out/ -src/vs/server -resources/server build/node_modules coverage/ test_data/ @@ -18,3 +16,4 @@ yarn-error.log vscode.lsif vscode.db /.profile-oss +*.orig diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 6f2518449e..8eb4c01846 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"August 2021\"" + "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"October 2021\"" }, { "kind": 1, @@ -27,6 +27,6 @@ { "kind": 2, "language": "github-issues", - "value": "$repo $milestone is:open label:api-proposal " + "value": "$repo $milestone is:open label:api-proposal sort:created-asc" } ] \ No newline at end of file diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 9a7842a357..a242c4053a 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:\"July 2021\"" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev 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 repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-unpkg\n\n$MILESTONE=milestone:\"October 2021\"" }, { "kind": 1, @@ -24,6 +24,26 @@ "language": "github-issues", "value": "$REPOS $MILESTONE is:pr is:open" }, + { + "kind": 1, + "language": "markdown", + "value": "## Unverified Older Insiders-Released Issues" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$REPOS -$MILESTONE is:issue is:closed label:bug label:insiders-released -label:verified -label:*duplicate -label:*as-designed -label:z-author-verified -label:on-testplan" + }, + { + "kind": 1, + "language": "markdown", + "value": "## Unverified Older Insiders-Released Feature Requests" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$REPOS -$MILESTONE is:issue is:closed label:feature-request label:insiders-released -label:on-testplan -label:verified -label:*duplicate" + }, { "kind": 1, "language": "markdown", @@ -57,7 +77,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE is:issue is:open label:testplan-item" + "value": "$REPOS is:issue is:open label:testplan-item" }, { "kind": 1, @@ -67,7 +87,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE is:issue is:closed label:feature-request label:verification-needed -label:verified" + "value": "$REPOS $MILESTONE is:issue is:closed label:verification-needed -label:verified" }, { "kind": 1, diff --git a/.vscode/notebooks/grooming-delta.github-issues b/.vscode/notebooks/grooming-delta.github-issues index 6602ea4b1c..dc588fc5ba 100644 --- a/.vscode/notebooks/grooming-delta.github-issues +++ b/.vscode/notebooks/grooming-delta.github-issues @@ -2,769 +2,666 @@ { "kind": 1, "language": "markdown", - "value": "## Config", - "editable": true + "value": "## Config" }, { "kind": 2, "language": "github-issues", - "value": "$since=2020-10-01", - "editable": true + "value": "$since=2021-10-01" }, { "kind": 1, "language": "markdown", - "value": "# vscode\n\nQuery exceeds the maximum result. Run the query manually: `is:issue is:open closed:>2020-10-01`", - "editable": true + "value": "# vscode\n\nQuery exceeds the maximum result. Run the query manually: `is:issue is:open closed:>2021-10-01`" }, { "kind": 2, "language": "github-issues", - "value": "//repo:microsoft/vscode is:issue closed:>$since", - "editable": true + "value": "//repo:microsoft/vscode is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "//repo:microsoft/vscode is:issue created:>$since", - "editable": true + "value": "//repo:microsoft/vscode is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-remote-release", - "editable": true + "value": "# vscode-remote-release" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-remote-release is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-remote-release is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-remote-release is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-remote-release is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# monaco-editor", - "editable": true + "value": "# monaco-editor" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-editor is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/monaco-editor is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-editor is:issue created:>$since", - "editable": true + "value": "repo:microsoft/monaco-editor is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-docs", - "editable": true + "value": "# vscode-docs" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-docs is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-docs is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-docs is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-docs is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-js-debug", - "editable": true + "value": "# vscode-js-debug" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-js-debug is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-js-debug is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-js-debug is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-js-debug is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# language-server-protocol", - "editable": true + "value": "# language-server-protocol" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/language-server-protocol is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/language-server-protocol is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/language-server-protocol is:issue created:>$since", - "editable": true + "value": "repo:microsoft/language-server-protocol is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-eslint", - "editable": true + "value": "# vscode-eslint" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-eslint is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-eslint is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-eslint is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-eslint is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-css-languageservice", - "editable": true + "value": "# vscode-css-languageservice" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-css-languageservice is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-css-languageservice is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-css-languageservice is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-css-languageservice is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-test", - "editable": true + "value": "# vscode-test" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-test is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-test is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-test is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-test is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-pull-request-github", - "editable": true + "value": "# vscode-pull-request-github" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-pull-request-github is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-pull-request-github is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-test is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-test is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-chrome-debug (deprecated)", - "editable": true + "value": "# vscode-chrome-debug-core" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-chrome-debug is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-chrome-debug-core is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-chrome-debug is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-chrome-debug-core is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-chrome-debug-core", - "editable": true + "value": "# vscode-debugadapter-node" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-chrome-debug-core is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-debugadapter-node is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-chrome-debug-core is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-debugadapter-node is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-debugadapter-node", - "editable": true + "value": "# vscode-emmet-helper" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-debugadapter-node is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-emmet-helper is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-debugadapter-node is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-emmet-helper is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-emmet-helper", - "editable": true + "value": "# vscode-extension-vscode\n\nDeprecated" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-emmet-helper is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-extension-vscode is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-emmet-helper is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-extension-vscode is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-extension-vscode\n\nDeprecated", - "editable": true + "value": "# vscode-extension-samples" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-extension-vscode is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-extension-samples is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-extension-vscode is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-extension-samples is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-extension-samples", - "editable": true + "value": "# vscode-filewatcher-windows" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-extension-samples is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-filewatcher-windows is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-extension-samples is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-filewatcher-windows is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-filewatcher-windows", - "editable": true + "value": "# vscode-generator-code" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-filewatcher-windows is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-generator-code is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-filewatcher-windows is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-generator-code is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-generator-code", - "editable": true + "value": "# vscode-html-languageservice" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-generator-code is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-html-languageservice is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-generator-code is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-html-languageservice is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-html-languageservice", - "editable": true + "value": "# vscode-json-languageservice" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-html-languageservice is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-json-languageservice is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-html-languageservice is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-json-languageservice is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-jshint", - "editable": true + "value": "# vscode-languageserver-node" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-jshint is:issue closed:>$since", - "editable": true - }, - { - "kind": 2, - "language": "github-issues", - "value": "repo:microsoft/vscode-jshint is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-languageserver-node is:issue closed:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-json-languageservice", - "editable": true + "value": "" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-json-languageservice is:issue closed:>$since", - "editable": true - }, - { - "kind": 2, - "language": "github-issues", - "value": "repo:microsoft/vscode-json-languageservice is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-languageserver-node is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-languageserver-node", - "editable": true + "value": "# vscode-loader" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-languageserver-node is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-loader is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-languageserver-node is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-loader is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-loader", - "editable": true + "value": "# vscode-mono-debug" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-loader is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-mono-debug is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-loader is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-mono-debug is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-mono-debug", - "editable": true + "value": "# vscode-node-debug" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-mono-debug is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-node-debug is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-mono-debug is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-node-debug is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-node-debug", - "editable": true + "value": "# vscode-node-debug2" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-node-debug is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-node-debug2 is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-node-debug is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-node-debug2 is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-node-debug2", - "editable": true + "value": "# vscode-recipes" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-node-debug2 is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-recipes is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-node-debug2 is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-recipes is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-recipes", - "editable": true + "value": "# vscode-textmate" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-recipes is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-textmate is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-recipes is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-textmate is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-textmate", - "editable": true + "value": "# vscode-themes" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-textmate is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-themes is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-textmate is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-themes is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-themes", - "editable": true + "value": "# vscode-vsce" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-themes is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-vsce is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-themes is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-vsce is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-vsce", - "editable": true + "value": "# vscode-website" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-vsce is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-website is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-vsce is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-website is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-website", - "editable": true + "value": "# vscode-windows-process-tree" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-website is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-windows-process-tree is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-website is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-windows-process-tree is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# vscode-windows-process-tree", - "editable": true + "value": "# debug-adapter-protocol" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-windows-process-tree is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/debug-adapter-protocol is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-windows-process-tree is:issue created:>$since", - "editable": true + "value": "repo:microsoft/debug-adapter-protocol is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# debug-adapter-protocol", - "editable": true + "value": "# inno-updater" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/debug-adapter-protocol is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/inno-updater is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/debug-adapter-protocol is:issue created:>$since", - "editable": true + "value": "repo:microsoft/inno-updater is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# inno-updater", - "editable": true + "value": "# monaco-languages" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/inno-updater is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/monaco-languages is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/inno-updater is:issue created:>$since", - "editable": true + "value": "repo:microsoft/monaco-languages is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# language-server-protocol-inspector", - "editable": true + "value": "# monaco-typescript" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/language-server-protocol-inspector is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/monaco-typescript is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/language-server-protocol-inspector is:issue created:>$since", - "editable": true + "value": "repo:microsoft/monaco-typescript is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# monaco-languages", - "editable": true + "value": "# monaco-css" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-languages is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/monaco-css is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-languages is:issue created:>$since", - "editable": true + "value": "repo:microsoft/monaco-css is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# monaco-typescript", - "editable": true + "value": "# monaco-json" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-typescript is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/monaco-json is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-typescript is:issue created:>$since", - "editable": true + "value": "repo:microsoft/monaco-json is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# monaco-css", - "editable": true + "value": "# monaco-html" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-css is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/monaco-html is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-css is:issue created:>$since", - "editable": true + "value": "repo:microsoft/monaco-html is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# monaco-json", - "editable": true + "value": "# monaco-editor-webpack-plugin" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-json is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/monaco-editor-webpack-plugin is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-json is:issue created:>$since", - "editable": true + "value": "repo:microsoft/monaco-editor-webpack-plugin is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# monaco-html", - "editable": true + "value": "# node-jsonc-parser" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-html is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/node-jsonc-parser is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-html is:issue created:>$since", - "editable": true + "value": "repo:microsoft/node-jsonc-parser is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# monaco-editor-webpack-plugin", - "editable": true + "value": "# vscode-jupyter" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-editor-webpack-plugin is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-jupyter is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/monaco-editor-webpack-plugin is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-jupyter is:issue created:>$since" }, { "kind": 1, "language": "markdown", - "value": "# node-jsonc-parser", - "editable": true + "value": "# vscode-python" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/node-jsonc-parser is:issue closed:>$since", - "editable": true + "value": "repo:microsoft/vscode-python is:issue closed:>$since" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/node-jsonc-parser is:issue created:>$since", - "editable": true + "value": "repo:microsoft/vscode-python is:issue created:>$since" + }, + { + "kind": 1, + "language": "markdown", + "value": "# vscode-livepreview" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-livepreview is:issue closed:>$since" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-livepreview is:issue created:>$since" + }, + { + "kind": 1, + "language": "markdown", + "value": "" + }, + { + "kind": 1, + "language": "markdown", + "value": "# vscode-test" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-test is:issue closed:>$since" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode-test is:issue created:>$since" } ] \ No newline at end of file diff --git a/.vscode/notebooks/grooming.github-issues b/.vscode/notebooks/grooming.github-issues deleted file mode 100644 index 407895eccf..0000000000 --- a/.vscode/notebooks/grooming.github-issues +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "kind": 1, - "language": "markdown", - "value": "### Categorizing Issues\n\nEach issue must have a type label. Most type labels are grey, some are yellow. Bugs are grey with a touch of red.", - "editable": true, - "outputs": [] - }, - { - "kind": 2, - "language": "github-issues", - "value": "repo:microsoft/vscode is:open is:issue assignee:@me -label:\"needs more info\" -label:bug -label:feature-request -label:under-discussion -label:debt -label:*question -label:upstream -label:electron -label:engineering -label:plan-item ", - "editable": true, - "outputs": [] - }, - { - "kind": 1, - "language": "markdown", - "value": "### Feature Areas\n\nEach issue should be assigned to a feature area", - "editable": true, - "outputs": [] - }, - { - "kind": 2, - "language": "github-issues", - "value": "repo:microsoft/vscode is:open is:issue assignee:@me -label:L10N -label:VIM -label:api -label:api-finalization -label:api-proposal -label:authentication -label:breadcrumbs -label:callhierarchy -label:code-lens -label:color-palette -label:comments -label:config -label:context-keys -label:css-less-scss -label:custom-editors -label:debug -label:debug-console -label:dialogs -label:diff-editor -label:dropdown -label:editor -label:editor-RTL -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-scrollbar -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet -label:error-list -label:explorer-custom -label:extension-host -label:extension-recommendations -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-guess-encoding -label:file-io -label:file-watcher -label:font-rendering -label:formatting -label:git -label:github -label:gpu -label:grammar -label:grid-view -label:html -label:i18n -label:icon-brand -label:icons-product -label:install-update -label:integrated-terminal -label:integrated-terminal-conpty -label:integrated-terminal-links -label:integrated-terminal-rendering -label:integrated-terminal-winpty -label:intellisense-config -label:ipc -label:issue-bot -label:issue-reporter -label:javascript -label:json -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:notebook -label:outline -label:output -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-pick -label:references-viewlet -label:release-notes -label:remote -label:remote-explorer -label:rename -label:sandbox -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:settings-editor -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview -label:suggest -label:sync-error-handling -label:tasks -label:telemetry -label:themes -label:timeline -label:timeline-git -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree -label:typescript -label:undo-redo -label:uri -label:ux -label:variable-resolving -label:vscode-build -label:vscode-website -label:web -label:webview -label:workbench-actions -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editors -label:workbench-electron -label:workbench-feedback -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-views -label:workbench-welcome -label:workbench-window -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:zoom", - "editable": true, - "outputs": [] - } -] \ No newline at end of file diff --git a/.vscode/notebooks/inbox.github-issues b/.vscode/notebooks/inbox.github-issues index d19b240adc..be6afc784c 100644 --- a/.vscode/notebooks/inbox.github-issues +++ b/.vscode/notebooks/inbox.github-issues @@ -2,49 +2,41 @@ { "kind": 1, "language": "markdown", - "value": "## tl;dr: Triage Inbox\n\nAll inbox issues but not those that need more information. These issues need to be triaged, e.g assigned to a user or ask for more information", - "editable": true + "value": "## tl;dr: Triage Inbox\n\nAll inbox issues but not those that need more information. These issues need to be triaged, e.g assigned to a user or ask for more information" }, { "kind": 2, "language": "github-issues", - "value": "$inbox -label:\"needs more info\"", - "editable": true + "value": "$inbox -label:\"needs more info\" sort:created-asc" }, { "kind": 1, "language": "markdown", - "value": "##### `Config`: defines the inbox query", - "editable": true + "value": "##### `Config`: defines the inbox query" }, { "kind": 2, "language": "github-issues", - "value": "$inbox=repo:microsoft/vscode is:open no:assignee -label:feature-request -label:testplan-item -label:plan-item ", - "editable": true + "value": "$inbox=repo:microsoft/vscode is:open no:assignee -label:feature-request -label:testplan-item -label:plan-item " }, { "kind": 1, "language": "markdown", - "value": "## Inbox tracking and Issue triage", - "editable": true + "value": "## Inbox tracking and Issue triage" }, { "kind": 1, "language": "markdown", - "value": "New issues or pull requests submitted by the community are initially triaged by an [automatic classification bot](https://github.com/microsoft/vscode-github-triage-actions/tree/master/classifier-deep). Issues that the bot does not correctly triage are then triaged by a team member. The team rotates the inbox tracker on a weekly basis.\n\nA [mirror](https://github.com/JacksonKearl/testissues/issues) of the VS Code issue stream is available with details about how the bot classifies issues, including feature-area classifications and confidence ratings. Per-category confidence thresholds and feature-area ownership data is maintained in [.github/classifier.json](https://github.com/microsoft/vscode/blob/main/.github/classifier.json). \n\n💡 The bot is being run through a GitHub action that runs every 30 minutes. Give the bot the opportunity to classify an issue before doing it manually.\n\n### Inbox Tracking\n\nThe inbox tracker is responsible for the [global inbox](https://github.com/microsoft/vscode/issues?utf8=%E2%9C%93&q=is%3Aopen+no%3Aassignee+-label%3Afeature-request+-label%3Atestplan-item+-label%3Aplan-item) containing all **open issues and pull requests** that\n- are neither **feature requests** nor **test plan items** nor **plan items** and\n- have **no owner assignment**.\n\nThe **inbox tracker** may perform any step described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) but its main responsibility is to route issues to the actual feature area owner.\n\nFeature area owners track the **feature area inbox** containing all **open issues and pull requests** that\n- are personally assigned to them and are not assigned to any milestone\n- are labeled with their feature area label and are not assigned to any milestone.\nThis secondary triage may involve any of the steps described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) and results in a fully triaged or closed issue.\n\nThe [github triage extension](https://github.com/microsoft/vscode-github-triage-extension) can be used to assist with triaging — it provides a \"Command Palette\"-style list of triaging actions like assignment, labeling, and triggers for various bot actions.", - "editable": true + "value": "New issues or pull requests submitted by the community are initially triaged by an [automatic classification bot](https://github.com/microsoft/vscode-github-triage-actions/tree/master/classifier-deep). Issues that the bot does not correctly triage are then triaged by a team member. The team rotates the inbox tracker on a weekly basis.\n\nA [mirror](https://github.com/JacksonKearl/testissues/issues) of the VS Code issue stream is available with details about how the bot classifies issues, including feature-area classifications and confidence ratings. Per-category confidence thresholds and feature-area ownership data is maintained in [.github/classifier.json](https://github.com/microsoft/vscode/blob/main/.github/classifier.json). \n\n💡 The bot is being run through a GitHub action that runs every 30 minutes. Give the bot the opportunity to classify an issue before doing it manually.\n\n### Inbox Tracking\n\nThe inbox tracker is responsible for the [global inbox](https://github.com/microsoft/vscode/issues?utf8=%E2%9C%93&q=is%3Aopen+no%3Aassignee+-label%3Afeature-request+-label%3Atestplan-item+-label%3Aplan-item) containing all **open issues and pull requests** that\n- are neither **feature requests** nor **test plan items** nor **plan items** and\n- have **no owner assignment**.\n\nThe **inbox tracker** may perform any step described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) but its main responsibility is to route issues to the actual feature area owner.\n\nFeature area owners track the **feature area inbox** containing all **open issues and pull requests** that\n- are personally assigned to them and are not assigned to any milestone\n- are labeled with their feature area label and are not assigned to any milestone.\nThis secondary triage may involve any of the steps described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) and results in a fully triaged or closed issue.\n\nThe [github triage extension](https://github.com/microsoft/vscode-github-triage-extension) can be used to assist with triaging — it provides a \"Command Palette\"-style list of triaging actions like assignment, labeling, and triggers for various bot actions." }, { "kind": 1, "language": "markdown", - "value": "## All Inbox Items\n\nAll issues that have no assignee and that have neither **feature requests** nor **test plan items** nor **plan items**.", - "editable": true + "value": "## All Inbox Items\n\nAll issues that have no assignee and that have neither **feature requests** nor **test plan items** nor **plan items**." }, { "kind": 2, "language": "github-issues", - "value": "$inbox", - "editable": true + "value": "$inbox" } ] \ No newline at end of file diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index fe93406d48..f8a9349d7f 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 repo:microsoft/vscode-emmet-helper\n\n$MILESTONE=milestone:\"July 2021\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev 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 repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal\n\n$MILESTONE=milestone:\"October 2021\"\n\n$MINE=assignee:@me" }, { "kind": 1, @@ -52,7 +52,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE is:issue is:open author:@me label:testplan-item" + "value": "$REPOS is:issue is:open author:@me label:testplan-item" }, { "kind": 1, @@ -77,7 +77,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE $MINE is:issue is:open label:testplan-item" + "value": "$REPOS $MINE is:issue is:open label:testplan-item" }, { "kind": 1, @@ -147,7 +147,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed author:@me 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" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed author:@me 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:needs-triage -label:verification-found" }, { "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 -author:hediet" + "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:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:dynamicwebpaige -author:eamodio -author:egamma -author:fiveisprime -author:greazer -author:gregvanl -author:hediet -author:IanMatthewHuff -author:isidorn -author:ItalyPaleAle -author:JacksonKearl -author:joaomoreno -author:joyceerhl -author:jrieken -author:karrtikr-author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:ornellaalt -author:orta -author:rchiodo -author:rebornix -author:RMacfarlane -author:roblourens -author:rzhao271 -author:sana-ajani -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand " }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 76cc184489..f1f8388260 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog\n\n// current milestone name\n$milestone=milestone:\"August 2021\"" + "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github\n\n// current milestone name\n$milestone=milestone:\"November 2021\"" }, { "kind": 1, @@ -43,7 +43,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:debt OR $repos assignee:@me is:open label:engineering" + "value": "$repos assignee:@me is:open label:debt,engineering" }, { "kind": 1, @@ -53,7 +53,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open label:perf OR $repos assignee:@me is:open label:perf-startup OR $repos assignee:@me is:open label:perf-bloat OR $repos assignee:@me is:open label:freeze-slow-crash-leak" + "value": "$repos assignee:@me is:open label:perf,perf-startup,perf-bloat,freeze-slow-crash-leak" }, { "kind": 1, @@ -84,7 +84,17 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open type:issue -label:bug -label:\"needs more info\" -label:feature-request -label:under-discussion -label:debt -label:plan-item -label:upstream" + "value": "$repos assignee:@me is:open type:issue -label:bug -label:\"needs more info\" -label:feature-request -label:under-discussion -label:debt -label:plan-item -label:upstream -label:polish -label:testplan-item" + }, + { + "kind": 1, + "language": "markdown", + "value": "#### Missing Area Label\n\nFeature area labels are light or strong blue (`1d76db` or `c5def5`) and they denote a specific feature or feature area, like `editor-clipboard` or `file-explorer`" + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"needs more info\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:color-palette -label:comments -label:config -label:context-keys -label:css-less-scss -label:custom-editors -label:debug -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet -label:engineering -label:error-list -label:extension-host -label:extension-recommendations -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:gpu -label:grammar -label:grid-view -label:html -label:i18n -label:icon-brand -label:icons-product -label:image-preview -label:inlay-hints -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-window -label:ipc -label:issue-bot -label:issue-reporter -label:javascript -label:json -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list -label:live-server -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:network -label:notebook -label:notebook-api -label:notebook-celltoolbar -label:notebook-diff -label:notebook-dnd -label:notebook-folding -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-keybinding -label:notebook-layout -label:notebook-markdown -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-statusbar -label:open-editors -label:opener -label:outline -label:output -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:references-viewlet -label:release-notes -label:remote -label:remote-explorer -label:remotehub -label:rename -label:sandbox -label:sash -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:settings-editor -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview -label:suggest -label:sync-error-handling -label:table -label:tasks -label:telemetry -label:terminal -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-links -label:terminal-local-echo -label:terminal-profiles -label:terminal-reconnection -label:terminal-rendering -label:terminal-tabs -label:terminal-winpty -label:testing -label:themes -label:timeline -label:timeline-git -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typehierarchy -label:typescript -label:undo-redo -label:uri -label:ux -label:variable-resolving -label:VIM -label:virtual-workspaces -label:vscode-build -label:vscode-website -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-feedback -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-welcome -label:workbench-window -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom" }, { "kind": 1, diff --git a/.vscode/notebooks/verification.github-issues b/.vscode/notebooks/verification.github-issues index 80ea9dc2e2..3a8b490648 100644 --- a/.vscode/notebooks/verification.github-issues +++ b/.vscode/notebooks/verification.github-issues @@ -12,7 +12,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-jupyter repo:microsoft/vscode-python\r\n$milestone=milestone:\"July 2021\"" + "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev 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-emmet-helper repo:microsoft/vscode-jupyter repo:microsoft/vscode-python\n$milestone=milestone:\"August 2021\"" }, { "kind": 1, diff --git a/.vscode/settings.json b/.vscode/settings.json index 83dd7960e4..ff21db52b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,8 +8,7 @@ "**/.DS_Store": true, "build/**/*.js": { "when": "$(basename).ts" - }, - "src/vs/server": false + } }, "files.associations": { "cglicenses.json": "jsonc" @@ -27,8 +26,7 @@ "test/automation/out/**": true, "test/integration/browser/out/**": true, "src/vs/base/test/node/uri.test.data.txt": true, - "src/vs/workbench/test/browser/api/extHostDocumentData.test.perf-data.ts": true, - "src/vs/server": false + "src/vs/workbench/test/browser/api/extHostDocumentData.test.perf-data.ts": true }, "lcov.path": [ "./.build/coverage/lcov.info", @@ -81,6 +79,5 @@ "editor.defaultFormatter": "vscode.typescript-language-features" }, "typescript.tsc.autoDetect": "off", - "notebook.experimental.useMarkdownRenderer": true, "testing.autoRun.mode": "rerun", } diff --git a/.yarnrc b/.yarnrc index a542d5661f..a2a3d2fb3b 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,4 @@ disturl "https://electronjs.org/headers" target "13.6.6" runtime "electron" +build_from_source "true" diff --git a/build/.cachesalt b/build/.cachesalt index b7ac004d6c..93303e0b97 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2021-11-19T02:27:18.022Z +2021-08-20T17:19:02.924Z diff --git a/build/.moduleignore b/build/.moduleignore index f8dadb2b83..986fe2a915 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -110,6 +110,18 @@ nsfw/src/** nsfw/includes/** !nsfw/build/Release/*.node +vscode-nsfw/binding.gyp +vscode-nsfw/build/** +vscode-nsfw/src/** +vscode-nsfw/includes/** +!vscode-nsfw/build/Release/*.node + +@parcel/watcher/binding.gyp +@parcel/watcher/build/** +@parcel/watcher/prebuilds/** +@parcel/watcher/src/** +!@parcel/watcher/build/Release/*.node + vsda/build/** vsda/ci/** vsda/src/** diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.js b/build/azure-pipelines/common/computeNodeModulesCacheKey.js index e607dee136..ebe6d97415 100644 --- a/build/azure-pipelines/common/computeNodeModulesCacheKey.js +++ b/build/azure-pipelines/common/computeNodeModulesCacheKey.js @@ -13,8 +13,17 @@ const shasum = crypto.createHash('sha1'); shasum.update(fs.readFileSync(path.join(ROOT, 'build/.cachesalt'))); shasum.update(fs.readFileSync(path.join(ROOT, '.yarnrc'))); shasum.update(fs.readFileSync(path.join(ROOT, 'remote/.yarnrc'))); -// Add `yarn.lock` files +// Add `package.json` and `yarn.lock` files for (let dir of dirs) { + const packageJsonPath = path.join(ROOT, dir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); + const relevantPackageJsonSections = { + dependencies: packageJson.dependencies, + devDependencies: packageJson.devDependencies, + optionalDependencies: packageJson.optionalDependencies, + resolutions: packageJson.resolutions + }; + shasum.update(JSON.stringify(relevantPackageJsonSections)); const yarnLockPath = path.join(ROOT, dir, 'yarn.lock'); shasum.update(fs.readFileSync(yarnLockPath)); } diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts index 9bcd769591..8594304df7 100644 --- a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts +++ b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts @@ -18,8 +18,18 @@ shasum.update(fs.readFileSync(path.join(ROOT, 'build/.cachesalt'))); shasum.update(fs.readFileSync(path.join(ROOT, '.yarnrc'))); shasum.update(fs.readFileSync(path.join(ROOT, 'remote/.yarnrc'))); -// Add `yarn.lock` files +// Add `package.json` and `yarn.lock` files for (let dir of dirs) { + const packageJsonPath = path.join(ROOT, dir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); + const relevantPackageJsonSections = { + dependencies: packageJson.dependencies, + devDependencies: packageJson.devDependencies, + optionalDependencies: packageJson.optionalDependencies, + resolutions: packageJson.resolutions + }; + shasum.update(JSON.stringify(relevantPackageJsonSections)); + const yarnLockPath = path.join(ROOT, dir, 'yarn.lock'); shasum.update(fs.readFileSync(yarnLockPath)); } diff --git a/build/azure-pipelines/common/createAsset.js b/build/azure-pipelines/common/createAsset.js index 228036894c..339aa2f7dd 100644 --- a/build/azure-pipelines/common/createAsset.js +++ b/build/azure-pipelines/common/createAsset.js @@ -30,20 +30,29 @@ function getPlatform(product, os, arch, type) { case 'user-setup': return `${asset}-user`; default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } case 'server': if (arch === 'arm64') { - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } return arch === 'ia32' ? 'server-win32' : `server-win32-${arch}`; case 'web': if (arch === 'arm64') { - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } return arch === 'ia32' ? 'server-win32-web' : `server-win32-${arch}-web`; default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); + } + case 'alpine': + switch (product) { + case 'server': + return `server-alpine-${arch}`; + case 'web': + return `server-alpine-${arch}-web`; + default: + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } case 'linux': switch (type) { @@ -58,14 +67,14 @@ function getPlatform(product, os, arch, type) { case 'web': return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`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}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } case 'darwin': switch (product) { @@ -78,14 +87,14 @@ function getPlatform(product, os, arch, type) { return 'server-darwin'; case 'web': if (arch !== 'x64') { - throw `What should the platform be?: ${product} ${os} ${arch} ${type}`; + throw new Error(`What should the platform be?: ${product} ${os} ${arch} ${type}`); } return 'server-darwin-web'; default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } } // Contains all of the logic for mapping types to our actual types in CosmosDB diff --git a/build/azure-pipelines/common/createAsset.ts b/build/azure-pipelines/common/createAsset.ts index 37a49bd237..0d46146aeb 100644 --- a/build/azure-pipelines/common/createAsset.ts +++ b/build/azure-pipelines/common/createAsset.ts @@ -45,20 +45,29 @@ function getPlatform(product: string, os: string, arch: string, type: string): s case 'user-setup': return `${asset}-user`; default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } case 'server': if (arch === 'arm64') { - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } return arch === 'ia32' ? 'server-win32' : `server-win32-${arch}`; case 'web': if (arch === 'arm64') { - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } return arch === 'ia32' ? 'server-win32-web' : `server-win32-${arch}-web`; default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); + } + case 'alpine': + switch (product) { + case 'server': + return `server-alpine-${arch}`; + case 'web': + return `server-alpine-${arch}-web`; + default: + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } case 'linux': switch (type) { @@ -73,14 +82,14 @@ function getPlatform(product: string, os: string, arch: string, type: string): s case 'web': return arch === 'standalone' ? 'web-standalone' : `server-linux-${arch}-web`; default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`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}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } case 'darwin': switch (product) { @@ -93,14 +102,14 @@ function getPlatform(product: string, os: string, arch: string, type: string): s return 'server-darwin'; case 'web': if (arch !== 'x64') { - throw `What should the platform be?: ${product} ${os} ${arch} ${type}`; + throw new Error(`What should the platform be?: ${product} ${os} ${arch} ${type}`); } return 'server-darwin-web'; default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } default: - throw `Unrecognized: ${product} ${os} ${arch} ${type}`; + throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } } diff --git a/build/azure-pipelines/common/installPlaywright.js b/build/azure-pipelines/common/installPlaywright.js index d46fa8cc2a..8f42155fa3 100644 --- a/build/azure-pipelines/common/installPlaywright.js +++ b/build/azure-pipelines/common/installPlaywright.js @@ -5,7 +5,7 @@ *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const retry_1 = require("./retry"); -const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/utils/registry'); +const { installBrowsersWithProgressBar } = require('playwright/lib/install/installer'); async function install() { await (0, retry_1.retry)(() => installDefaultBrowsersForNpmInstall()); } diff --git a/build/azure-pipelines/common/installPlaywright.ts b/build/azure-pipelines/common/installPlaywright.ts index 449746a1c9..d28f5932a7 100644 --- a/build/azure-pipelines/common/installPlaywright.ts +++ b/build/azure-pipelines/common/installPlaywright.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { retry } from './retry'; -const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/utils/registry'); +const { installBrowsersWithProgressBar } = require('playwright/lib/install/installer'); async function install() { await retry(() => installDefaultBrowsersForNpmInstall()); diff --git a/build/azure-pipelines/common/sign.js b/build/azure-pipelines/common/sign.js index 568b40f268..a8d685f815 100644 --- a/build/azure-pipelines/common/sign.js +++ b/build/azure-pipelines/common/sign.js @@ -69,9 +69,17 @@ function main([esrpCliPath, type, cert, username, password, folderPath, pattern] '-r', 'true', '-e', keyFile, ]; - cp.spawnSync('dotnet', args, { stdio: 'inherit' }); + try { + cp.execFileSync('dotnet', args, { stdio: 'inherit' }); + } + catch (err) { + console.error('ESRP failed'); + console.error(err); + process.exit(1); + } } exports.main = main; if (require.main === module) { main(process.argv.slice(2)); + process.exit(0); } diff --git a/build/azure-pipelines/common/sign.ts b/build/azure-pipelines/common/sign.ts index 824b0ed668..a1ccf3576a 100644 --- a/build/azure-pipelines/common/sign.ts +++ b/build/azure-pipelines/common/sign.ts @@ -76,9 +76,16 @@ export function main([esrpCliPath, type, cert, username, password, folderPath, p '-e', keyFile, ]; - cp.spawnSync('dotnet', args, { stdio: 'inherit' }); + try { + cp.execFileSync('dotnet', args, { stdio: 'inherit' }); + } catch (err) { + console.error('ESRP failed'); + console.error(err); + process.exit(1); + } } if (require.main === module) { main(process.argv.slice(2)); + process.exit(0); } diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 157a338acf..f21bd633a7 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -84,7 +84,6 @@ steps: set -e export npm_config_arch=$(VSCODE_ARCH) export npm_config_node_gyp=$(which node-gyp) - export npm_config_build_from_source=true export SDKROOT=/Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk for i in {1..3}; do # try 3 times, for Terrapin @@ -224,7 +223,7 @@ steps: set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" - yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" --screenshots .build/logs/smoke-tests + yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" --screenshots $(Build.SourcesDirectory)/.build/logs/smoke-tests timeoutInMinutes: 5 displayName: Run smoke tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) @@ -234,7 +233,7 @@ steps: APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \ - yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" --remote --screenshots .build/logs/smoke-tests + yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" --remote --screenshots $(Build.SourcesDirectory)/.build/logs/smoke-tests timeoutInMinutes: 5 displayName: Run smoke tests (Remote) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) diff --git a/build/azure-pipelines/darwin/sql-product-build-darwin.yml b/build/azure-pipelines/darwin/sql-product-build-darwin.yml index f2bd0a916c..ab12eecea2 100644 --- a/build/azure-pipelines/darwin/sql-product-build-darwin.yml +++ b/build/azure-pipelines/darwin/sql-product-build-darwin.yml @@ -17,7 +17,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: @@ -110,18 +110,19 @@ steps: displayName: Run unit tests condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) - - script: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - set -e - APP_ROOT=$(agent.builddirectory)/azuredatastudio-darwin-x64 - APP_NAME="`ls $APP_ROOT | head -n 1`" - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/azuredatastudio-reh-darwin" \ - ./scripts/test-integration.sh --build --tfs "Integration Tests" - displayName: Run integration tests (Electron) - condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) +# skip while fixing + # - script: | + # # Figure out the full absolute path of the product we just built + # # including the remote server and configure the integration tests + # # to run with these builds instead of running out of sources. + # set -e + # APP_ROOT=$(agent.builddirectory)/azuredatastudio-darwin-x64 + # APP_NAME="`ls $APP_ROOT | head -n 1`" + # INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ + # VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/azuredatastudio-reh-darwin" \ + # ./scripts/test-integration.sh --build --tfs "Integration Tests" + # displayName: Run integration tests (Electron) + # condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) - script: | set -e diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index fbfec3dbd9..82b5c92c42 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -11,7 +11,7 @@ pr: steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" diff --git a/build/azure-pipelines/docker/sql-product-build-docker.yml b/build/azure-pipelines/docker/sql-product-build-docker.yml index d2a5990778..e84e38d2db 100644 --- a/build/azure-pipelines/docker/sql-product-build-docker.yml +++ b/build/azure-pipelines/docker/sql-product-build-docker.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index e5533cec4c..cb40a97a3a 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -10,7 +10,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: diff --git a/build/azure-pipelines/linux/product-build-alpine.yml b/build/azure-pipelines/linux/product-build-alpine.yml index d9f37d25fa..2a74a37f5b 100644 --- a/build/azure-pipelines/linux/product-build-alpine.yml +++ b/build/azure-pipelines/linux/product-build-alpine.yml @@ -31,7 +31,7 @@ steps: azureSubscriptionEndpoint: "vscode-builds-subscription" azureContainerRegistry: vscodehub.azurecr.io command: "Run an image" - imageName: "vscode-linux-build-agent:alpine" + imageName: "vscode-linux-build-agent:alpine-$(VSCODE_ARCH)" containerCommand: uname - script: | @@ -106,15 +106,31 @@ steps: node build/azure-pipelines/mixin displayName: Mix in quality + - script: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + displayName: 'Register Docker QEMU' + condition: eq(variables['VSCODE_ARCH'], 'arm64') + - script: | set -e - docker run -e VSCODE_QUALITY -v $(pwd):/root/vscode -v ~/.netrc:/root/.netrc vscodehub.azurecr.io/vscode-linux-build-agent:alpine /root/vscode/build/azure-pipelines/linux/alpine/install-dependencies.sh + docker run -e VSCODE_QUALITY -v $(pwd):/root/vscode -v ~/.netrc:/root/.netrc vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) /root/vscode/build/azure-pipelines/linux/alpine/install-dependencies.sh displayName: Prebuild - script: | set -e - yarn gulp vscode-reh-linux-alpine-min-ci - yarn gulp vscode-reh-web-linux-alpine-min-ci + + case $VSCODE_ARCH in + x64) + reh='vscode-reh-linux-alpine-min-ci' + rehweb='vscode-reh-web-linux-alpine-min-ci' + ;; + arm64) + reh='vscode-reh-alpine-arm64-min-ci' + rehweb='vscode-reh-web-alpine-arm64-min-ci' + ;; + esac + + yarn gulp $reh + yarn gulp $rehweb displayName: Build - script: | @@ -122,7 +138,14 @@ steps: REPO="$(pwd)" ROOT="$REPO/.." - PLATFORM_LINUX="linux-alpine" + case $VSCODE_ARCH in + x64) + PLATFORM_LINUX='linux-alpine' + ;; + arm64) + PLATFORM_LINUX='alpine-arm64' + ;; + esac # Publish Remote Extension Host LEGACY_SERVER_BUILD_NAME="vscode-reh-$PLATFORM_LINUX" @@ -144,12 +167,23 @@ steps: displayName: Prepare for publish condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + - publish: $(Agent.BuildDirectory)/vscode-server-alpine-$(VSCODE_ARCH).tar.gz + artifact: vscode_server_alpine_$(VSCODE_ARCH)_archive-unsigned + displayName: Publish server archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'x64')) + + - publish: $(Agent.BuildDirectory)/vscode-server-alpine-$(VSCODE_ARCH)-web.tar.gz + artifact: vscode_web_alpine_$(VSCODE_ARCH)_archive-unsigned + displayName: Publish web server archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'x64')) + + # Legacy x64 artifact name - publish: $(Agent.BuildDirectory)/vscode-server-linux-alpine.tar.gz artifact: vscode_server_linux_alpine_archive-unsigned - displayName: Publish server archive - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + displayName: Publish x64 server archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), eq(variables['VSCODE_ARCH'], 'x64')) - publish: $(Agent.BuildDirectory)/vscode-server-linux-alpine-web.tar.gz artifact: vscode_web_linux_alpine_archive-unsigned - displayName: Publish web server archive - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + displayName: Publish x64 web server archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), eq(variables['VSCODE_ARCH'], 'x64')) diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index d011d5b026..5c742c2503 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -76,7 +76,6 @@ steps: - script: | set -e export npm_config_arch=$(NPM_ARCH) - export npm_config_build_from_source=true if [ -z "$CC" ] || [ -z "$CXX" ]; then # Download clang based on chromium revision used by vscode @@ -91,7 +90,7 @@ steps: # Set compiler toolchain export CC=$PWD/.build/CR_Clang/bin/clang export CXX=$PWD/.build/CR_Clang/bin/clang++ - export CXXFLAGS="-nostdinc++ -D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit" + export CXXFLAGS="-nostdinc++ -D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS -D__NO_INLINE__ -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit" export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -flto=thin -fsplit-lto-unit -L$PWD/.build/libcxx-objects -lc++abi" fi @@ -212,7 +211,7 @@ steps: - script: | set -e APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - yarn smoketest-no-compile --build "$APP_PATH" --electronArgs="--disable-dev-shm-usage --use-gl=swiftshader" --screenshots .build/logs/smoke-tests + yarn smoketest-no-compile --build "$APP_PATH" --electronArgs="--disable-dev-shm-usage --use-gl=swiftshader" --screenshots $(Build.SourcesDirectory)/.build/logs/smoke-tests timeoutInMinutes: 5 displayName: Run smoke tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) @@ -221,7 +220,7 @@ steps: set -e APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --build "$APP_PATH" --remote --electronArgs="--disable-dev-shm-usage --use-gl=swiftshader" --screenshots .build/logs/smoke-tests + yarn smoketest-no-compile --build "$APP_PATH" --remote --electronArgs="--disable-dev-shm-usage --use-gl=swiftshader" --screenshots $(Build.SourcesDirectory)/.build/logs/smoke-tests timeoutInMinutes: 5 displayName: Run smoke tests (Remote) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) diff --git a/build/azure-pipelines/linux/sql-product-build-linux.yml b/build/azure-pipelines/linux/sql-product-build-linux.yml index 662d6152f7..0375bcb3e7 100644 --- a/build/azure-pipelines/linux/sql-product-build-linux.yml +++ b/build/azure-pipelines/linux/sql-product-build-linux.yml @@ -4,7 +4,7 @@ parameters: steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: @@ -123,18 +123,19 @@ steps: displayName: Run unit tests (Electron) condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) - - script: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - set -e - APP_ROOT=$(agent.builddirectory)/azuredatastudio-linux-x64 - APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/azuredatastudio-reh-linux-x64" \ - DISPLAY=:10 ./scripts/test-integration.sh --build --tfs "Integration Tests" - displayName: Run integration tests (Electron) - condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) +# skip while fixing + # - script: | + # # Figure out the full absolute path of the product we just built + # # including the remote server and configure the integration tests + # # to run with these builds instead of running out of sources. + # set -e + # APP_ROOT=$(agent.builddirectory)/azuredatastudio-linux-x64 + # APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + # INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ + # VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/azuredatastudio-reh-linux-x64" \ + # DISPLAY=:10 ./scripts/test-integration.sh --build --tfs "Integration Tests" + # displayName: Run integration tests (Electron) + # condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) - script: | # Figure out the full absolute path of the product we just built diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index d2f0d5b451..a67086500a 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -6,6 +6,7 @@ schedules: branches: include: - main + - joao/web parameters: - name: VSCODE_QUALITY @@ -45,7 +46,11 @@ parameters: type: boolean default: true - name: VSCODE_BUILD_LINUX_ALPINE - displayName: "🎯 Alpine Linux" + displayName: "🎯 Alpine Linux x64" + type: boolean + default: true + - name: VSCODE_BUILD_LINUX_ALPINE_ARM64 + displayName: "🎯 Alpine Linux arm64" type: boolean default: true - name: VSCODE_BUILD_MACOS @@ -91,7 +96,7 @@ variables: - name: VSCODE_BUILD_STAGE_WINDOWS value: ${{ or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_32BIT, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_LINUX - value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true), eq(parameters.VSCODE_BUILD_LINUX_ALPINE, true), eq(parameters.VSCODE_BUILD_WEB, true)) }} + value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true), eq(parameters.VSCODE_BUILD_LINUX_ALPINE, true), eq(parameters.VSCODE_BUILD_LINUX_ALPINE_ARM64, true), eq(parameters.VSCODE_BUILD_WEB, true)) }} - name: VSCODE_BUILD_STAGE_MACOS value: ${{ or(eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }} - name: VSCODE_CIBUILD @@ -248,6 +253,15 @@ stages: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ALPINE, true)) }}: - job: LinuxAlpine + variables: + VSCODE_ARCH: x64 + steps: + - template: linux/product-build-alpine.yml + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ALPINE_ARM64, true)) }}: + - job: LinuxAlpineArm64 + variables: + VSCODE_ARCH: arm64 steps: - template: linux/product-build-alpine.yml diff --git a/build/azure-pipelines/sdl-scan.yml b/build/azure-pipelines/sdl-scan.yml index 6fd354c9d0..edccd0845b 100644 --- a/build/azure-pipelines/sdl-scan.yml +++ b/build/azure-pipelines/sdl-scan.yml @@ -58,7 +58,7 @@ stages: inputs: azureSubscription: "vscode-builds-subscription" KeyVaultName: vscode - SecretsFilter: "github-distro-mixin-password,ESRP-SSL-AADAuth,vscode-storage-key,builds-docdb-key-readwrite" + SecretsFilter: "github-distro-mixin-password" - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -94,7 +94,6 @@ stages: addProjectDirToScanningExclusionList: true env: npm_config_arch: "$(NPM_ARCH)" - npm_config_build_from_source: true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: CodeQL @@ -106,7 +105,6 @@ stages: retry { exec { yarn --frozen-lockfile } } env: npm_config_arch: "$(NPM_ARCH)" - npm_config_build_from_source: true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" CHILD_CONCURRENCY: 1 @@ -155,7 +153,7 @@ stages: inputs: azureSubscription: "vscode-builds-subscription" KeyVaultName: vscode - SecretsFilter: "github-distro-mixin-password,ESRP-SSL-AADAuth,vscode-storage-key,builds-docdb-key-readwrite" + SecretsFilter: "github-distro-mixin-password" - script: | set -e @@ -190,7 +188,6 @@ stages: - script: | set -e export npm_config_arch=$(NPM_ARCH) - export npm_config_build_from_source=true if [ -z "$CC" ] || [ -z "$CXX" ]; then # Download clang based on chromium revision used by vscode diff --git a/build/azure-pipelines/sql-product-compile.yml b/build/azure-pipelines/sql-product-compile.yml index 26c975c7c0..c555b457d8 100644 --- a/build/azure-pipelines/sql-product-compile.yml +++ b/build/azure-pipelines/sql-product-compile.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: diff --git a/build/azure-pipelines/upload-nlsmetadata.js b/build/azure-pipelines/upload-nlsmetadata.js new file mode 100644 index 0000000000..27c9438187 --- /dev/null +++ b/build/azure-pipelines/upload-nlsmetadata.js @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const path = require("path"); +const es = require("event-stream"); +const vfs = require("vinyl-fs"); +const util = require("../lib/util"); +const merge = require("gulp-merge-json"); +const gzip = require("gulp-gzip"); +const azure = require('gulp-azure-storage'); +const root = path.dirname(path.dirname(__dirname)); +const commit = util.getVersion(root); +function main() { + return es.merge(vfs.src('out-vscode-web-min/nls.metadata.json', { base: 'out-vscode-web-min' }), vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })) + .pipe(merge({ + fileName: 'combined.nls.metadata.json', + jsonSpace: '', + edit: (parsedJson, file) => { + let key; + if (file.base === 'out-vscode-web-min') { + return { vscode: parsedJson }; + } + // Handle extensions and follow the same structure as the Core nls file. + switch (file.basename) { + case 'package.nls.json': + // put package.nls.json content in Core NlsMetadata format + // language packs use the key "package" to specify that + // translations are for the package.json file + parsedJson = { + messages: { + package: Object.values(parsedJson) + }, + keys: { + package: Object.keys(parsedJson) + }, + bundles: { + main: ['package'] + } + }; + break; + case 'nls.metadata.header.json': + parsedJson = { header: parsedJson }; + break; + case 'nls.metadata.json': + // put nls.metadata.json content in Core NlsMetadata format + const modules = Object.keys(parsedJson); + const json = { + keys: {}, + messages: {}, + bundles: { + main: [] + } + }; + for (const module of modules) { + json.messages[module] = parsedJson[module].messages; + json.keys[module] = parsedJson[module].keys; + json.bundles.main.push(module); + } + parsedJson = json; + break; + } + key = 'vscode.' + file.relative.split('/')[0]; + return { [key]: parsedJson }; + }, + })) + .pipe(gzip({ append: false })) + .pipe(vfs.dest('./nlsMetadata')) + .pipe(es.through(function (data) { + console.log(`Uploading ${data.path}`); + // trigger artifact upload + console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=combined.nls.metadata.json]${data.path}`); + this.emit('data', data); + })) + .pipe(azure.upload({ + account: process.env.AZURE_STORAGE_ACCOUNT, + key: process.env.AZURE_STORAGE_ACCESS_KEY, + container: 'nlsmetadata', + prefix: commit + '/', + contentSettings: { + contentEncoding: 'gzip', + cacheControl: 'max-age=31536000, public' + } + })); +} +main(); diff --git a/build/azure-pipelines/upload-nlsmetadata.ts b/build/azure-pipelines/upload-nlsmetadata.ts new file mode 100644 index 0000000000..fcbe1f9508 --- /dev/null +++ b/build/azure-pipelines/upload-nlsmetadata.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * 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 path from 'path'; +import * as es from 'event-stream'; +import * as Vinyl from 'vinyl'; +import * as vfs from 'vinyl-fs'; +import * as util from '../lib/util'; +import * as merge from 'gulp-merge-json'; +import * as gzip from 'gulp-gzip'; +const azure = require('gulp-azure-storage'); + +const root = path.dirname(path.dirname(__dirname)); +const commit = util.getVersion(root); + +interface NlsMetadata { + keys: { [module: string]: string }, + messages: { [module: string]: string }, + bundles: { [bundle: string]: string[] }, +} + +function main() { + return es.merge( + vfs.src('out-vscode-web-min/nls.metadata.json', { base: 'out-vscode-web-min' }), + vfs.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), + vfs.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), + vfs.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })) + .pipe(merge({ + fileName: 'combined.nls.metadata.json', + jsonSpace: '', + edit: (parsedJson, file) => { + let key; + if (file.base === 'out-vscode-web-min') { + return { vscode: parsedJson }; + } + + // Handle extensions and follow the same structure as the Core nls file. + switch (file.basename) { + case 'package.nls.json': + // put package.nls.json content in Core NlsMetadata format + // language packs use the key "package" to specify that + // translations are for the package.json file + parsedJson = { + messages: { + package: Object.values(parsedJson) + }, + keys: { + package: Object.keys(parsedJson) + }, + bundles: { + main: ['package'] + } + }; + break; + + case 'nls.metadata.header.json': + parsedJson = { header: parsedJson }; + break; + + case 'nls.metadata.json': + // put nls.metadata.json content in Core NlsMetadata format + const modules = Object.keys(parsedJson); + + const json: NlsMetadata = { + keys: {}, + messages: {}, + bundles: { + main: [] + } + }; + for (const module of modules) { + json.messages[module] = parsedJson[module].messages; + json.keys[module] = parsedJson[module].keys; + json.bundles.main.push(module); + } + parsedJson = json; + break; + } + key = 'vscode.' + file.relative.split('/')[0]; + return { [key]: parsedJson }; + }, + })) + .pipe(gzip({ append: false })) + .pipe(vfs.dest('./nlsMetadata')) + .pipe(es.through(function (data: Vinyl) { + console.log(`Uploading ${data.path}`); + // trigger artifact upload + console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=combined.nls.metadata.json]${data.path}`); + this.emit('data', data); + })) + .pipe(azure.upload({ + account: process.env.AZURE_STORAGE_ACCOUNT, + key: process.env.AZURE_STORAGE_ACCESS_KEY, + container: 'nlsmetadata', + prefix: commit + '/', + contentSettings: { + contentEncoding: 'gzip', + cacheControl: 'max-age=31536000, public' + } + })); +} + +main(); diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 2d81b8c48c..4977207b89 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -119,6 +119,13 @@ steps: node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.api.js.map displayName: Upload sourcemaps (Web) + - script: | + set -e + AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ + node build/azure-pipelines/upload-nlsmetadata + displayName: Upload NLS Metadata + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + - script: | set -e REPO="$(pwd)" diff --git a/build/azure-pipelines/web/sql-product-build-web.yml b/build/azure-pipelines/web/sql-product-build-web.yml index 9008208783..b8ea9737c3 100644 --- a/build/azure-pipelines/web/sql-product-build-web.yml +++ b/build/azure-pipelines/web/sql-product-build-web.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 29ddba0b55..d365c165f8 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -79,7 +79,6 @@ steps: . build/azure-pipelines/win32/retry.ps1 $ErrorActionPreference = "Stop" $env:npm_config_arch="$(VSCODE_ARCH)" - $env:npm_config_build_from_source="true" $env:CHILD_CONCURRENCY="1" retry { exec { yarn --frozen-lockfile } } env: @@ -104,6 +103,14 @@ steps: exec { node build/azure-pipelines/mixin } displayName: Mix in quality + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" + exec { yarn npm-run-all -lp "electron $(VSCODE_ARCH)" } + displayName: Download Electron + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -116,7 +123,6 @@ steps: . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" - exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-code-helper" } exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-inno-updater" } displayName: Prepare Package condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) @@ -135,8 +141,8 @@ steps: . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" - exec { yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" } - displayName: Download Electron and Playwright + exec { yarn npm-run-all -lp "playwright-install" } + displayName: Download Playwright condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - powershell: | @@ -200,20 +206,20 @@ steps: . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --build "$AppRoot" --screenshots .build\logs\smoke-tests } + exec { yarn smoketest-no-compile --build "$AppRoot" --screenshots $(Build.SourcesDirectory)\.build\logs\smoke-tests } displayName: Run smoke tests (Electron) timeoutInMinutes: 5 condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - # - powershell: | - # . build/azure-pipelines/win32/exec.ps1 - # $ErrorActionPreference = "Stop" - # $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - # $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)" - # exec { yarn smoketest-no-compile --build "$AppRoot" --remote } - # displayName: Run smoke tests (Remote) - # timeoutInMinutes: 5 - # condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)" + exec { yarn smoketest-no-compile --build "$AppRoot" --remote } + displayName: Run smoke tests (Remote) + timeoutInMinutes: 5 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -249,7 +255,7 @@ steps: - task: UseDotNet@2 inputs: - version: 2.x + version: 3.x condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: EsrpClientTool@1 diff --git a/build/azure-pipelines/win32/sql-product-build-win32.yml b/build/azure-pipelines/win32/sql-product-build-win32.yml index 30ad4bafd7..25d697050d 100644 --- a/build/azure-pipelines/win32/sql-product-build-win32.yml +++ b/build/azure-pipelines/win32/sql-product-build-win32.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: @@ -103,7 +103,6 @@ steps: $ErrorActionPreference = "Stop" exec { yarn gulp "package-rebuild-extensions" } exec { yarn gulp "vscode-win32-x64-min-ci" } - exec { yarn gulp "vscode-win32-x64-code-helper" } exec { yarn gulp "vscode-win32-x64-inno-updater" } displayName: Build env: @@ -141,18 +140,19 @@ steps: # displayName: Run unit tests (Electron) # condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) - - powershell: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\azuredatastudio-win32-x64" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json - $AppNameShort = $AppProductJson.nameShort - # exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\azuredatastudio-reh-win32-x64"; .\scripts\test-integration.bat --build --tfs "Integration Tests" } - displayName: Run integration tests (Electron) - condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) +# skip while fixing + # - powershell: | + # # Figure out the full absolute path of the product we just built + # # including the remote server and configure the integration tests + # # to run with these builds instead of running out of sources. + # . build/azure-pipelines/win32/exec.ps1 + # $ErrorActionPreference = "Stop" + # $AppRoot = "$(agent.builddirectory)\azuredatastudio-win32-x64" + # $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + # $AppNameShort = $AppProductJson.nameShort + # # exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\azuredatastudio-reh-win32-x64"; .\scripts\test-integration.bat --build --tfs "Integration Tests" } + # displayName: Run integration tests (Electron) + # condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) # - powershell: | # . build/azure-pipelines/win32/exec.ps1 diff --git a/build/azure-pipelines/win32/sql-product-test-win32.yml b/build/azure-pipelines/win32/sql-product-test-win32.yml index d18db41d90..50b29aeea0 100644 --- a/build/azure-pipelines/win32/sql-product-test-win32.yml +++ b/build/azure-pipelines/win32/sql-product-test-win32.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: diff --git a/build/filters.js b/build/filters.js index be1990667b..17facc5eef 100644 --- a/build/filters.js +++ b/build/filters.js @@ -41,7 +41,7 @@ module.exports.indentationFilter = [ '!src/vs/css.js', '!src/vs/css.build.js', '!src/vs/loader.js', - '!src/vs/base/common/insane/insane.js', + '!src/vs/base/browser/dompurify/*', '!src/vs/base/common/marked/marked.js', '!src/vs/base/common/semver/semver.js', '!src/vs/base/node/terminateProcess.sh', @@ -209,7 +209,7 @@ module.exports.jsHygieneFilter = [ '!src/vs/nls.js', '!src/vs/css.build.js', '!src/vs/nls.build.js', - '!src/**/insane.js', + '!src/**/dompurify.js', '!src/**/marked.js', '!src/**/semver.js', '!**/test/**', diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 1706287c84..dd7fe45436 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -191,7 +191,7 @@ const compileEditorESMTask = task.define('compile-editor-esm', () => { } } - console.log(`Open in VS Code the folder at '${destPath}' and you can alayze the compilation error`); + console.log(`Open in VS Code the folder at '${destPath}' and you can analyze the compilation error`); throw new Error('Standalone Editor compilation failed. If this is the build machine, simply launch `yarn run gulp editor-distro` on your machine to further analyze the compilation problem.'); }); } @@ -231,7 +231,10 @@ function toExternalDTS(contents) { if (line.indexOf('declare let MonacoEnvironment') === 0) { lines[i] = `declare global {\n let MonacoEnvironment: Environment | undefined;\n}`; - // lines[i] = line.replace('declare namespace monaco.', 'export namespace '); + } + + if (line.indexOf('\tMonacoEnvironment?') === 0) { + lines[i] = ` MonacoEnvironment?: Environment | undefined;`; } } return lines.join('\n').replace(/\n\n\n+/g, '\n\n'); diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index d16742c5ea..c2e76bfc93 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -14,6 +14,10 @@ function checkPackageJSON(actualPath) { const rootPackageJSON = require('../package.json'); const checkIncluded = (set1, set2) => { for (let depName in set1) { + if (depName === 'typescript') { + continue; + } + const depVersion = set1[depName]; const rootDepVersion = set2[depName]; if (!rootDepVersion) { diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index ff0880ff44..01d7569e78 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -38,14 +38,17 @@ const REMOTE_FOLDER = path.join(REPO_ROOT, 'remote'); // Targets const BUILD_TARGETS = [ - { platform: 'win32', arch: 'ia32', pkgTarget: 'node8-win-x86' }, - { platform: 'win32', arch: 'x64', pkgTarget: 'node8-win-x64' }, - { platform: 'darwin', arch: null, pkgTarget: 'node8-macos-x64' }, - { platform: 'linux', arch: 'ia32', pkgTarget: 'node8-linux-x86' }, - { platform: 'linux', arch: 'x64', pkgTarget: 'node8-linux-x64' }, - { platform: 'linux', arch: 'armhf', pkgTarget: 'node8-linux-armv7' }, - { platform: 'linux', arch: 'arm64', pkgTarget: 'node8-linux-arm64' }, - { platform: 'linux', arch: 'alpine', pkgTarget: 'node8-linux-alpine' }, + { platform: 'win32', arch: 'ia32' }, + { platform: 'win32', arch: 'x64' }, + { platform: 'darwin', arch: null }, + { platform: 'linux', arch: 'ia32' }, + { platform: 'linux', arch: 'x64' }, + { platform: 'linux', arch: 'armhf' }, + { platform: 'linux', arch: 'arm64' }, + { platform: 'alpine', arch: 'arm64' }, + // legacy: we use to ship only one alpine so it was put in the arch, but now we ship + // multiple alpine images and moved to a better model (alpine as the platform) + { platform: 'linux', arch: 'alpine' }, ]; const serverResources = [ @@ -108,10 +111,6 @@ const serverEntryPoints = [ name: 'vs/server/remoteExtensionHostProcess', exclude: ['vs/css', 'vs/nls'] }, - { - name: 'vs/platform/files/node/watcher/unix/watcherApp', - exclude: ['vs/css', 'vs/nls'] - }, { name: 'vs/platform/files/node/watcher/nsfw/watcherApp', exclude: ['vs/css', 'vs/nls'] @@ -186,8 +185,9 @@ function nodejs(platform, arch) { .pipe(rename('node.exe')); } - if (arch === 'alpine') { - const contents = cp.execSync(`docker run --rm node:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' }); + if (arch === 'alpine' || platform === 'alpine') { + const imageName = arch === 'arm64' ? 'arm64v8/node' : 'node'; + const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' }); return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } })]); } @@ -508,32 +508,198 @@ function packagePkgTask(platform, arch, pkgTarget) { }); }); -function mixinServer(watch) { - const packageJSONPath = path.join(path.dirname(__dirname), 'package.json'); - function exec(cmdLine) { - console.log(cmdLine); - cp.execSync(cmdLine, { stdio: 'inherit' }); - } - function checkout() { - const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString()); - exec('git fetch distro'); - exec(`git checkout ${packageJSON['distro']} -- src/vs/server resources/server`); - exec('git reset HEAD src/vs/server resources/server'); - } - checkout(); - if (watch) { - console.log('Enter watch mode (observing package.json)'); - const watcher = fs.watch(packageJSONPath); - watcher.addListener('change', () => { - try { - checkout(); - } catch (e) { - console.log(e); +function packageTask(type, platform, arch, sourceFolderName, destinationFolderName) { + const destination = path.join(BUILD_ROOT, destinationFolderName); + + return () => { + const json = require('gulp-json-editor'); + + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })) + .pipe(util.setExecutableBit(['**/*.sh'])) + .pipe(filter(['**', '!**/*.js.map'])); + + const workspaceExtensionPoints = ['debuggers', 'jsonValidation']; + const isUIExtension = (manifest) => { + switch (manifest.extensionKind) { + case 'ui': return true; + case 'workspace': return false; + default: { + if (manifest.main) { + return false; + } + if (manifest.contributes && Object.keys(manifest.contributes).some(key => workspaceExtensionPoints.indexOf(key) !== -1)) { + return false; + } + // Default is UI Extension + return true; + } } - }); - } - return Promise.resolve(); + }; + const localWorkspaceExtensions = glob.sync('extensions/*/package.json') + .filter((extensionPath) => { + if (type === 'reh-web') { + return true; // web: ship all extensions for now + } + + const manifest = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, extensionPath)).toString()); + return !isUIExtension(manifest); + }).map((extensionPath) => path.basename(path.dirname(extensionPath))) + .filter(name => name !== 'vscode-api-tests' && name !== 'vscode-test-resolver'); // Do not ship the test extensions + const marketplaceExtensions = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'product.json'), 'utf8')).builtInExtensions + .filter(entry => !entry.platforms || new Set(entry.platforms).has(platform)) + .filter(entry => !entry.clientOnly) + .map(entry => entry.name); + const extensionPaths = [...localWorkspaceExtensions, ...marketplaceExtensions] + .map(name => `.build/extensions/${name}/**`); + + const extensions = gulp.src(extensionPaths, { base: '.build', dot: true }); + const extensionsCommonDependencies = gulp.src('.build/extensions/node_modules/**', { base: '.build', dot: true }); + const sources = es.merge(src, extensions, extensionsCommonDependencies) + .pipe(filter(['**', '!**/*.js.map'], { dot: true })); + + let version = packageJson.version; + const quality = product.quality; + + if (quality && quality !== 'stable') { + version += '-' + quality; + } + + const name = product.nameShort; + const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) + .pipe(json({ name, version })); + + const date = new Date().toISOString(); + + const productJsonStream = gulp.src(['product.json'], { base: '.' }) + .pipe(json({ commit, date })); + + const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); + + const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); + + const productionDependencies = getProductionDependencies(REMOTE_FOLDER); + const dependenciesSrc = _.flatten(productionDependencies.map(d => path.relative(REPO_ROOT, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`])); + const deps = gulp.src(dependenciesSrc, { base: 'remote', dot: true }) + // filter out unnecessary files, no source maps in server build + .pipe(filter(['**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.js.map'])) + .pipe(util.cleanNodeModules(path.join(__dirname, '.moduleignore'))) + .pipe(jsFilter) + .pipe(util.stripSourceMappingURL()) + .pipe(jsFilter.restore); + + const nodePath = `.build/node/v${nodeVersion}/${platform}-${platform === 'darwin' ? 'x64' : arch}`; + const node = gulp.src(`${nodePath}/**`, { base: nodePath, dot: true }); + + let web = []; + if (type === 'reh-web') { + web = [ + 'resources/server/favicon.ico', + 'resources/server/code-192.png', + 'resources/server/code-512.png', + 'resources/server/manifest.json' + ].map(resource => gulp.src(resource, { base: '.' }).pipe(rename(resource))); + } + + let all = es.merge( + packageJsonStream, + productJsonStream, + license, + sources, + deps, + node, + ...web + ); + + let result = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()); + + if (platform === 'win32') { + result = es.merge(result, + gulp.src('resources/server/bin/code.cmd', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/${product.applicationName}.cmd`)), + gulp.src('resources/server/bin/helpers/browser.cmd', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/helpers/browser.cmd`)), + gulp.src('resources/server/bin/server.cmd', { base: '.' }) + .pipe(rename(`server.cmd`)) + ); + } else if (platform === 'linux' || platform === 'alpine' || platform === 'darwin') { + result = es.merge(result, + gulp.src('resources/server/bin/code.sh', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/${product.applicationName}`)) + .pipe(util.setExecutableBit()), + gulp.src('resources/server/bin/helpers/browser.sh', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/helpers/browser.sh`)) + .pipe(util.setExecutableBit()), + gulp.src('resources/server/bin/server.sh', { base: '.' }) + .pipe(rename(`server.sh`)) + .pipe(util.setExecutableBit()) + ); + } + + return result.pipe(vfs.dest(destination)); + }; } -gulp.task(task.define('mixin-server', () => mixinServer(false))); -gulp.task(task.define('mixin-server-watch', () => mixinServer(true))); +['reh', 'reh-web'].forEach(type => { + const optimizeTask = task.define(`optimize-vscode-${type}`, task.series( + util.rimraf(`out-vscode-${type}`), + common.optimizeTask({ + src: 'out-build', + entryPoints: _.flatten(type === 'reh' ? serverEntryPoints : serverWithWebEntryPoints), + otherSources: [], + resources: type === 'reh' ? serverResources : serverWithWebResources, + loaderConfig: common.loaderConfig(), + out: `out-vscode-${type}`, + inlineAmdImages: true, + bundleInfo: undefined, + fileContentMapper: createVSCodeWebFileContentMapper('.build/extensions') + }) + )); + + const minifyTask = task.define(`minify-vscode-${type}`, task.series( + optimizeTask, + util.rimraf(`out-vscode-${type}-min`), + common.minifyTask(`out-vscode-${type}`, `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + )); + gulp.task(minifyTask); + + BUILD_TARGETS.forEach(buildTarget => { + const dashed = (str) => (str ? `-${str}` : ``); + const platform = buildTarget.platform; + const arch = buildTarget.arch; + + ['', 'min'].forEach(minified => { + const sourceFolderName = `out-vscode-${type}${dashed(minified)}`; + const destinationFolderName = `vscode-${type}${dashed(platform)}${dashed(arch)}`; + + const serverTaskCI = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series( + gulp.task(`node-${platform}-${platform === 'darwin' ? 'x64' : arch}`), + util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), + packageTask(type, platform, arch, sourceFolderName, destinationFolderName) + )); + gulp.task(serverTaskCI); + + const serverTask = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( + compileBuildTask, + compileExtensionsBuildTask, + minified ? minifyTask : optimizeTask, + serverTaskCI + )); + gulp.task(serverTask); + }); + }); +}); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 94f132c995..a756efda6d 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -39,6 +39,8 @@ const vscodeEntryPoints = _.flatten([ buildfile.workerExtensionHost, buildfile.workerNotebook, buildfile.workerLanguageDetection, + buildfile.workerSharedProcess, + buildfile.workerLocalFileSearch, buildfile.workbenchDesktop, buildfile.code ]); diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index e654979844..209eacc43a 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -6,11 +6,212 @@ 'use strict'; const gulp = require('gulp'); +const path = require('path'); +const es = require('event-stream'); +const util = require('./lib/util'); +const task = require('./lib/task'); +const common = require('./lib/optimize'); +const product = require('../product.json'); +const rename = require('gulp-rename'); +const filter = require('gulp-filter'); +const _ = require('underscore'); +const { getProductionDependencies } = require('./lib/dependencies'); +const vfs = require('vinyl-fs'); +const fs = require('fs'); +const packageJson = require('../package.json'); +const { compileBuildTask } = require('./gulpfile.compile'); +const extensions = require('./lib/extensions'); -const noop = () => { return Promise.resolve(); }; +const REPO_ROOT = path.dirname(__dirname); +const BUILD_ROOT = path.dirname(REPO_ROOT); +const WEB_FOLDER = path.join(REPO_ROOT, 'remote', 'web'); -gulp.task('minify-vscode-web', noop); -gulp.task('vscode-web', noop); -gulp.task('vscode-web-min', noop); -gulp.task('vscode-web-ci', noop); -gulp.task('vscode-web-min-ci', noop); +const commit = util.getVersion(REPO_ROOT); +const quality = product.quality; +const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; + +const vscodeWebResourceIncludes = [ + // Workbench + 'out-build/vs/{base,platform,editor,workbench}/**/*.{svg,png,jpg}', + 'out-build/vs/code/browser/workbench/*.html', + 'out-build/vs/base/browser/ui/codicons/codicon/**/*.ttf', + 'out-build/vs/**/markdown.css', + + // Webview + 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', + 'out-build/vs/workbench/contrib/webview/browser/pre/*.html', + + // Extension Worker + 'out-build/vs/workbench/services/extensions/worker/httpsWebWorkerExtensionHostIframe.html', + 'out-build/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html', + + // Web node paths (needed for integration tests) + 'out-build/vs/webPackagePaths.js', +]; +exports.vscodeWebResourceIncludes = vscodeWebResourceIncludes; + +const vscodeWebResources = [ + + // Includes + ...vscodeWebResourceIncludes, + + // Excludes + '!out-build/vs/**/{node,electron-browser,electron-main}/**', + '!out-build/vs/editor/standalone/**', + '!out-build/vs/workbench/**/*-tb.png', + '!**/test/**' +]; + +const buildfile = require('../src/buildfile'); + +const vscodeWebEntryPoints = _.flatten([ + buildfile.entrypoint('vs/workbench/workbench.web.api'), + buildfile.base, + buildfile.workerExtensionHost, + buildfile.workerNotebook, + buildfile.workerLanguageDetection, + buildfile.workerLocalFileSearch, + buildfile.keyboardMaps, + buildfile.workbenchWeb +]); +exports.vscodeWebEntryPoints = vscodeWebEntryPoints; + +const buildDate = new Date().toISOString(); + +/** + * @param extensionsRoot {string} The location where extension will be read from + */ +const createVSCodeWebFileContentMapper = (extensionsRoot) => { + /** + * @param content {string} The contens of the file + * @param path {string} The absolute file path, always using `/`, even on Windows + */ + const result = (content, path) => { + // (1) Patch product configuration + if (path.endsWith('vs/platform/product/common/product.js')) { + const productConfiguration = JSON.stringify({ + ...product, + extensionAllowedProposedApi: [...product.extensionAllowedProposedApi], + version, + commit, + date: buildDate + }); + return content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', productConfiguration.substr(1, productConfiguration.length - 2) /* without { and }*/); + } + + // (2) Patch builtin extensions + if (path.endsWith('vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.js')) { + // Do not inline `vscode-web-playground` even if it has been packed! + const builtinExtensions = JSON.stringify(extensions.scanBuiltinExtensions(extensionsRoot, ['vscode-web-playground'])); + return content.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', builtinExtensions.substr(1, builtinExtensions.length - 2) /* without [ and ]*/); + } + + return content; + }; + return result; +}; +exports.createVSCodeWebFileContentMapper = createVSCodeWebFileContentMapper; + +const optimizeVSCodeWebTask = task.define('optimize-vscode-web', task.series( + util.rimraf('out-vscode-web'), + common.optimizeTask({ + src: 'out-build', + entryPoints: _.flatten(vscodeWebEntryPoints), + otherSources: [], + resources: vscodeWebResources, + loaderConfig: common.loaderConfig(), + externalLoaderInfo: util.createExternalLoaderConfig(product.webEndpointUrl, commit, quality), + out: 'out-vscode-web', + inlineAmdImages: true, + bundleInfo: undefined, + fileContentMapper: createVSCodeWebFileContentMapper('.build/web/extensions') + }) +)); + +const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( + optimizeVSCodeWebTask, + util.rimraf('out-vscode-web-min'), + common.minifyTask('out-vscode-web', `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) +)); +gulp.task(minifyVSCodeWebTask); + +function packageTask(sourceFolderName, destinationFolderName) { + const destination = path.join(BUILD_ROOT, destinationFolderName); + + return () => { + const json = require('gulp-json-editor'); + + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })); + + const extensions = gulp.src('.build/web/extensions/**', { base: '.build/web', dot: true }); + + const sources = es.merge(src, extensions) + .pipe(filter(['**', '!**/*.js.map'], { dot: true })); + + const name = product.nameShort; + const packageJsonStream = gulp.src(['remote/web/package.json'], { base: 'remote/web' }) + .pipe(json({ name, version })); + + const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); + + const productionDependencies = getProductionDependencies(WEB_FOLDER); + const dependenciesSrc = _.flatten(productionDependencies.map(d => path.relative(REPO_ROOT, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`])); + + const deps = gulp.src(dependenciesSrc, { base: 'remote/web', dot: true }) + .pipe(filter(['**', '!**/package-lock.json'])) + .pipe(util.cleanNodeModules(path.join(__dirname, '.webignore'))); + + const favicon = gulp.src('resources/server/favicon.ico', { base: 'resources/server' }); + const manifest = gulp.src('resources/server/manifest.json', { base: 'resources/server' }); + const pwaicons = es.merge( + gulp.src('resources/server/code-192.png', { base: 'resources/server' }), + gulp.src('resources/server/code-512.png', { base: 'resources/server' }) + ); + + let all = es.merge( + packageJsonStream, + license, + sources, + deps, + favicon, + manifest, + pwaicons + ); + + let result = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()); + + return result.pipe(vfs.dest(destination)); + }; +} + +const compileWebExtensionsBuildTask = task.define('compile-web-extensions-build', task.series( + task.define('clean-web-extensions-build', util.rimraf('.build/web/extensions')), + task.define('bundle-web-extensions-build', () => extensions.packageLocalExtensionsStream(true).pipe(gulp.dest('.build/web'))), + task.define('bundle-marketplace-web-extensions-build', () => extensions.packageMarketplaceExtensionsStream(true).pipe(gulp.dest('.build/web'))), + task.define('bundle-web-extension-media-build', () => extensions.buildExtensionMedia(false, '.build/web/extensions')), +)); +gulp.task(compileWebExtensionsBuildTask); + +const dashed = (str) => (str ? `-${str}` : ``); + +['', 'min'].forEach(minified => { + const sourceFolderName = `out-vscode-web${dashed(minified)}`; + const destinationFolderName = `vscode-web`; + + const vscodeWebTaskCI = task.define(`vscode-web${dashed(minified)}-ci`, task.series( + compileWebExtensionsBuildTask, + minified ? minifyVSCodeWebTask : optimizeVSCodeWebTask, + util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), + packageTask(sourceFolderName, destinationFolderName) + )); + gulp.task(vscodeWebTaskCI); + + const vscodeWebTask = task.define(`vscode-web${dashed(minified)}`, task.series( + compileBuildTask, + vscodeWebTaskCI + )); + gulp.task(vscodeWebTask); +}); diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index e8d01e83b9..949aa4a254 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -156,9 +156,3 @@ function updateIcon(executablePath) { gulp.task(task.define('vscode-win32-ia32-inno-updater', task.series(copyInnoUpdater('ia32'), updateIcon(path.join(buildPath('ia32'), 'tools', 'inno_updater.exe'))))); gulp.task(task.define('vscode-win32-x64-inno-updater', task.series(copyInnoUpdater('x64'), updateIcon(path.join(buildPath('x64'), 'tools', 'inno_updater.exe'))))); gulp.task(task.define('vscode-win32-arm64-inno-updater', task.series(copyInnoUpdater('arm64'), updateIcon(path.join(buildPath('arm64'), 'tools', 'inno_updater.exe'))))); - -// CodeHelper.exe icon - -gulp.task(task.define('vscode-win32-ia32-code-helper', task.series(updateIcon(path.join(buildPath('ia32'), 'resources', 'app', 'out', 'vs', 'platform', 'files', 'node', 'watcher', 'win32', 'CodeHelper.exe'))))); -gulp.task(task.define('vscode-win32-x64-code-helper', task.series(updateIcon(path.join(buildPath('x64'), 'resources', 'app', 'out', 'vs', 'platform', 'files', 'node', 'watcher', 'win32', 'CodeHelper.exe'))))); -gulp.task(task.define('vscode-win32-arm64-code-helper', task.series(updateIcon(path.join(buildPath('arm64'), 'resources', 'app', 'out', 'vs', 'platform', 'files', 'node', 'watcher', 'win32', 'CodeHelper.exe'))))); diff --git a/build/lib/asar.ts b/build/lib/asar.ts index 66ea414964..0b83f86a74 100644 --- a/build/lib/asar.ts +++ b/build/lib/asar.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as es from 'event-stream'; -const pickle = require('chromium-pickle-js'); +const pickle = require('chromium-pickle-js'); const Filesystem = require('asar/lib/filesystem'); import * as VinylFile from 'vinyl'; import * as minimatch from 'minimatch'; diff --git a/build/lib/compilation.js b/build/lib/compilation.js index d668d11cae..585101a46b 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -37,17 +37,9 @@ function createCompile(src, build, emitError) { const sourcemaps = require('gulp-sourcemaps'); const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); const overrideOptions = Object.assign(Object.assign({}, getTypeScriptCompilerOptions(src)), { inlineSources: Boolean(build) }); - if (!build && !process.env['SQL_NO_INLINE_SOURCEMAP']) { + if (!build) { overrideOptions.inlineSourceMap = true; } - else if (!build) { - console.warn('********************************************************************************************'); - console.warn('* Inlining of source maps is DISABLED, which will prevent debugging from working properly, *'); - console.warn('* but is required to generate code coverage reports. *'); - console.warn('* To re-enable inlining of source maps clear the SQL_NO_INLINE_SOURCEMAP environment var *'); - console.warn('* and re-run the build/watch task *'); - console.warn('********************************************************************************************'); - } const compilation = tsb.create(projectPath, overrideOptions, false, err => reporter(err)); function pipeline(token) { const bom = require('gulp-bom'); diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 9bc909bbe4..66ca3c5e4f 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -44,15 +44,8 @@ function createCompile(src: string, build: boolean, emitError?: boolean) { const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; - if (!build && !process.env['SQL_NO_INLINE_SOURCEMAP']) { + if (!build) { overrideOptions.inlineSourceMap = true; - } else if (!build) { - console.warn('********************************************************************************************'); - console.warn('* Inlining of source maps is DISABLED, which will prevent debugging from working properly, *'); - console.warn('* but is required to generate code coverage reports. *'); - console.warn('* To re-enable inlining of source maps clear the SQL_NO_INLINE_SOURCEMAP environment var *'); - console.warn('* and re-run the build/watch task *'); - console.warn('********************************************************************************************'); } const compilation = tsb.create(projectPath, overrideOptions, false, err => reporter(err)); @@ -94,6 +87,7 @@ function createCompile(src: string, build: boolean, emitError?: boolean) { export function compileTask(src: string, out: string, build: boolean): () => NodeJS.ReadWriteStream { return function () { + if (os.totalmem() < 4_000_000_000) { throw new Error('compilation requires 4GB of RAM'); } diff --git a/build/lib/eslint/vscode-dts-literal-or-types.js b/build/lib/eslint/vscode-dts-literal-or-types.js index a259a9c318..2717cfabe2 100644 --- a/build/lib/eslint/vscode-dts-literal-or-types.js +++ b/build/lib/eslint/vscode-dts-literal-or-types.js @@ -12,15 +12,13 @@ module.exports = new class ApiLiteralOrTypes { } create(context) { return { - ['TSTypeAnnotation TSUnionType TSLiteralType']: (node) => { - var _a; - if (((_a = node.literal) === null || _a === void 0 ? void 0 : _a.type) === 'TSNullKeyword') { - return; + ['TSTypeAnnotation TSUnionType']: (node) => { + if (node.types.every(value => value.type === 'TSLiteralType')) { + context.report({ + node: node, + messageId: 'useEnum' + }); } - context.report({ - node: node, - messageId: 'useEnum' - }); } }; } diff --git a/build/lib/eslint/vscode-dts-literal-or-types.ts b/build/lib/eslint/vscode-dts-literal-or-types.ts index 0d7130aaa0..bb6e816bee 100644 --- a/build/lib/eslint/vscode-dts-literal-or-types.ts +++ b/build/lib/eslint/vscode-dts-literal-or-types.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { @@ -14,14 +15,13 @@ export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['TSTypeAnnotation TSUnionType TSLiteralType']: (node: any) => { - if (node.literal?.type === 'TSNullKeyword') { - return; + ['TSTypeAnnotation TSUnionType']: (node: any) => { + if ((node).types.every(value => value.type === 'TSLiteralType')) { + context.report({ + node: node, + messageId: 'useEnum' + }); } - context.report({ - node: node, - messageId: 'useEnum' - }); } }; } diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 8258c723fe..0b85293440 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -102,6 +102,10 @@ "name": "vs/workbench/contrib/interactive", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/languageStatus", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/keybindings", "project": "vscode-workbench" diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index a500bb3136..7439ac9d0c 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -49,7 +49,8 @@ const CORE_TYPES = [ 'decode', 'self', 'trimLeft', - 'trimRight' + 'trimRight', + 'queueMicrotask' ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 446b2fd410..bd9e3af4fa 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -50,7 +50,8 @@ const CORE_TYPES = [ 'decode', 'self', 'trimLeft', - 'trimRight' + 'trimRight', + 'queueMicrotask' ]; // Types that are defined in a common layer but are known to be only diff --git a/build/lib/monaco-api.js b/build/lib/monaco-api.js index 86cf8a6174..a968aba204 100644 --- a/build/lib/monaco-api.js +++ b/build/lib/monaco-api.js @@ -149,26 +149,6 @@ function getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importN } }); } - else if (declaration.kind === ts.SyntaxKind.VariableStatement) { - const jsDoc = result.substr(0, declaration.getLeadingTriviaWidth(sourceFile)); - if (jsDoc.indexOf('@monacodtsreplace') >= 0) { - const jsDocLines = jsDoc.split(/\r\n|\r|\n/); - let directives = []; - for (const jsDocLine of jsDocLines) { - const m = jsDocLine.match(/^\s*\* \/([^/]+)\/([^/]+)\/$/); - if (m) { - directives.push([new RegExp(m[1], 'g'), m[2]]); - } - } - // remove the jsdoc - result = result.substr(jsDoc.length); - if (directives.length > 0) { - // apply replace directives - const replacer = createReplacerFromDirectives(directives); - result = replacer(result); - } - } - } result = result.replace(/export default /g, 'export '); result = result.replace(/export declare /g, 'export '); result = result.replace(/declare /g, ''); diff --git a/build/lib/monaco-api.ts b/build/lib/monaco-api.ts index 126153ef8f..9b35bbfd0e 100644 --- a/build/lib/monaco-api.ts +++ b/build/lib/monaco-api.ts @@ -178,25 +178,6 @@ function getMassagedTopLevelDeclarationText(ts: typeof import('typescript'), sou // life.. } }); - } else if (declaration.kind === ts.SyntaxKind.VariableStatement) { - const jsDoc = result.substr(0, declaration.getLeadingTriviaWidth(sourceFile)); - if (jsDoc.indexOf('@monacodtsreplace') >= 0) { - const jsDocLines = jsDoc.split(/\r\n|\r|\n/); - let directives: [RegExp, string][] = []; - for (const jsDocLine of jsDocLines) { - const m = jsDocLine.match(/^\s*\* \/([^/]+)\/([^/]+)\/$/); - if (m) { - directives.push([new RegExp(m[1], 'g'), m[2]]); - } - } - // remove the jsdoc - result = result.substr(jsDoc.length); - if (directives.length > 0) { - // apply replace directives - const replacer = createReplacerFromDirectives(directives); - result = replacer(result); - } - } } result = result.replace(/export default /g, 'export '); result = result.replace(/export declare /g, 'export '); diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 57e6139b12..72f6e5e869 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -37,7 +37,7 @@ function loaderConfig() { } exports.loaderConfig = loaderConfig; const IS_OUR_COPYRIGHT_REGEXP = /Copyright \(C\) Microsoft Corporation/i; -function loader(src, bundledFileHeader, bundleLoader) { +function loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo) { let sources = [ `${src}/vs/loader.js` ]; @@ -63,6 +63,15 @@ function loader(src, bundledFileHeader, bundleLoader) { else { this.emit('data', data); } + }, function () { + if (externalLoaderInfo !== undefined) { + this.emit('data', new VinylFile({ + path: 'fake2', + base: '.', + contents: Buffer.from(`require.config(${JSON.stringify(externalLoaderInfo, undefined, 2)});`) + })); + } + this.emit('end'); })) .pipe(concat('vs/loader.js'))); } @@ -148,7 +157,7 @@ function optimizeTask(opts) { } es.readArray(bundleInfoArray).pipe(bundleInfoStream); }); - const result = es.merge(loader(src, bundledFileHeader, bundleLoader), bundlesStream, resourcesStream, bundleInfoStream); + const result = es.merge(loader(src, bundledFileHeader, bundleLoader, opts.externalLoaderInfo), bundlesStream, resourcesStream, bundleInfoStream); return result .pipe(sourcemaps.write('./', { sourceRoot: undefined, diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index a24726e647..401540d7b3 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -43,7 +43,7 @@ export function loaderConfig() { const IS_OUR_COPYRIGHT_REGEXP = /Copyright \(C\) Microsoft Corporation/i; -function loader(src: string, bundledFileHeader: string, bundleLoader: boolean): NodeJS.ReadWriteStream { +function loader(src: string, bundledFileHeader: string, bundleLoader: boolean, externalLoaderInfo?: any): NodeJS.ReadWriteStream { let sources = [ `${src}/vs/loader.js` ]; @@ -70,6 +70,15 @@ function loader(src: string, bundledFileHeader: string, bundleLoader: boolean): } else { this.emit('data', data); } + }, function () { + if (externalLoaderInfo !== undefined) { + this.emit('data', new VinylFile({ + path: 'fake2', + base: '.', + contents: Buffer.from(`require.config(${JSON.stringify(externalLoaderInfo, undefined, 2)});`) + })); + } + this.emit('end'); })) .pipe(concat('vs/loader.js')) ); @@ -135,6 +144,10 @@ export interface IOptimizeTaskOpts { */ resources: string[]; loaderConfig: any; + /** + * Additional info we append to the end of the loader + */ + externalLoaderInfo?: any; /** * (true by default - append css and nls to loader) */ @@ -213,7 +226,7 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr }); const result = es.merge( - loader(src, bundledFileHeader, bundleLoader), + loader(src, bundledFileHeader, bundleLoader, opts.externalLoaderInfo), bundlesStream, resourcesStream, bundleInfoStream diff --git a/build/lib/typings/vinyl.d.ts b/build/lib/typings/vinyl.d.ts index 6be30a1eeb..5062c5154f 100644 --- a/build/lib/typings/vinyl.d.ts +++ b/build/lib/typings/vinyl.d.ts @@ -47,6 +47,29 @@ declare module "vinyl" { * Used for relative pathing. Typically where a glob starts. */ public base: string; + /** + * Gets and sets the basename of `file.path`. + * + * Throws when `file.path` is not set. + * + * Example: + * + * ```js + * var file = new File({ + * cwd: '/', + * base: '/test/', + * path: '/test/file.js' + * }); + * + * console.log(file.basename); // file.js + * + * file.basename = 'file.txt'; + * + * console.log(file.basename); // file.txt + * console.log(file.path); // /test/file.txt + * ``` + */ + basename: string; /** * Full path to the file. */ @@ -105,7 +128,7 @@ declare module "vinyl" { * This is required as per: * https://github.com/microsoft/TypeScript/issues/5073 */ - namespace File {} + namespace File { } export = File; diff --git a/build/lib/util.js b/build/lib/util.js index 2379e56ed6..899c5984ff 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.buildWebNodePaths = exports.acquireWebNodePaths = exports.getElectronVersion = exports.streamToPromise = exports.versionStringToNumber = exports.filter = exports.rebase = exports.getVersion = exports.ensureDir = exports.rreddir = exports.rimraf = exports.rewriteSourceMappingURL = exports.stripSourceMappingURL = exports.loadSourcemaps = exports.cleanNodeModules = exports.skipDirectories = exports.toFileUri = exports.setExecutableBit = exports.fixWin32DirectoryPermissions = exports.incremental = void 0; +exports.buildWebNodePaths = exports.createExternalLoaderConfig = exports.acquireWebNodePaths = exports.getElectronVersion = exports.streamToPromise = exports.versionStringToNumber = exports.filter = exports.rebase = exports.getVersion = exports.ensureDir = exports.rreddir = exports.rimraf = exports.rewriteSourceMappingURL = exports.stripSourceMappingURL = exports.loadSourcemaps = exports.cleanNodeModules = exports.skipDirectories = exports.toFileUri = exports.setExecutableBit = exports.fixWin32DirectoryPermissions = exports.incremental = void 0; const es = require("event-stream"); const debounce = require("debounce"); const _filter = require("gulp-filter"); @@ -301,6 +301,23 @@ function acquireWebNodePaths() { return nodePaths; } exports.acquireWebNodePaths = acquireWebNodePaths; +function createExternalLoaderConfig(webEndpoint, commit, quality) { + if (!webEndpoint || !commit || !quality) { + return undefined; + } + webEndpoint = webEndpoint + `/${quality}/${commit}`; + let nodePaths = acquireWebNodePaths(); + Object.keys(nodePaths).map(function (key, _) { + nodePaths[key] = `${webEndpoint}/node_modules/${key}/${nodePaths[key]}`; + }); + const externalLoaderConfig = { + baseUrl: `${webEndpoint}/out`, + recordStats: true, + paths: nodePaths + }; + return externalLoaderConfig; +} +exports.createExternalLoaderConfig = createExternalLoaderConfig; function buildWebNodePaths(outDir) { const result = () => new Promise((resolve, _) => { const root = path.join(__dirname, '..', '..'); diff --git a/build/lib/util.ts b/build/lib/util.ts index 4cd41557ea..6322c09d8b 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -345,7 +345,7 @@ export function acquireWebNodePaths() { const root = path.join(__dirname, '..', '..'); const webPackageJSON = path.join(root, '/remote/web', 'package.json'); const webPackages = JSON.parse(fs.readFileSync(webPackageJSON, 'utf8')).dependencies; - const nodePaths: { [key: string]: string } = {}; + const nodePaths: { [key: string]: string } = { }; for (const key of Object.keys(webPackages)) { const packageJSON = path.join(root, 'node_modules', key, 'package.json'); const packageData = JSON.parse(fs.readFileSync(packageJSON, 'utf8')); @@ -366,6 +366,23 @@ export function acquireWebNodePaths() { return nodePaths; } +export function createExternalLoaderConfig(webEndpoint?: string, commit?: string, quality?: string) { + if (!webEndpoint || !commit || !quality) { + return undefined; + } + webEndpoint = webEndpoint + `/${quality}/${commit}`; + let nodePaths = acquireWebNodePaths(); + Object.keys(nodePaths).map(function (key, _) { + nodePaths[key] = `${webEndpoint}/node_modules/${key}/${nodePaths[key]}`; + }); + const externalLoaderConfig = { + baseUrl: `${webEndpoint}/out`, + recordStats: true, + paths: nodePaths + }; + return externalLoaderConfig; +} + export function buildWebNodePaths(outDir: string) { const result = () => new Promise((resolve, _) => { const root = path.join(__dirname, '..', '..'); diff --git a/build/monaco/ThirdPartyNotices.txt b/build/monaco/ThirdPartyNotices.txt index 8b488daf19..30a0afa4b6 100644 --- a/build/monaco/ThirdPartyNotices.txt +++ b/build/monaco/ThirdPartyNotices.txt @@ -35,39 +35,12 @@ END OF nodejs path library NOTICES AND INFORMATION -%% string_scorer version 0.1.20 (https://github.com/joshaven/string_score) -========================================= -This software is released under the MIT license: - -Copyright (c) Joshaven Potter - -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 string_scorer NOTICES AND INFORMATION - - - - -%% chjj-marked NOTICES AND INFORMATION BEGIN HERE +%% markedjs NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) -Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/) +Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) +Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -87,4 +60,4 @@ 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 chjj-marked NOTICES AND INFORMATION +END OF markedjs NOTICES AND INFORMATION diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index 641bed28fb..a201265b48 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -59,7 +59,7 @@ declare namespace monaco.editor { #include(vs/editor/standalone/common/standaloneThemeService): BuiltinTheme, IStandaloneThemeData, IColors #include(vs/editor/common/modes/supports/tokenization): ITokenThemeRule #include(vs/editor/common/services/webWorker): MonacoWebWorker, IWebWorkerOptions -#include(vs/editor/standalone/browser/standaloneCodeEditor): IActionDescriptor, IGlobalEditorOptions, IStandaloneEditorConstructionOptions, IDiffEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor +#include(vs/editor/standalone/browser/standaloneCodeEditor): IActionDescriptor, IGlobalEditorOptions, IStandaloneEditorConstructionOptions, IStandaloneDiffEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor export interface ICommandHandler { (...args: any[]): void; } @@ -69,7 +69,7 @@ export interface ICommandHandler { #include(vs/editor/standalone/browser/colorizer): IColorizerOptions, IColorizerElementOptions #include(vs/base/common/scrollable): ScrollbarVisibility #include(vs/platform/theme/common/themeService): ThemeColor -#includeAll(vs/editor/common/model;LanguageIdentifier=>languages.LanguageIdentifier): IScrollEvent +#includeAll(vs/editor/common/model): IScrollEvent #includeAll(vs/editor/common/editorCommon;editorOptions.=>): IScrollEvent #includeAll(vs/editor/common/model/textModelEvents): #includeAll(vs/editor/common/controller/cursorEvents): diff --git a/build/monaco/package.json b/build/monaco/package.json index 9a6e05c123..b987a610d7 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -1,7 +1,7 @@ { "name": "monaco-editor-core", "private": true, - "version": "0.23.0", + "version": "0.29.2", "description": "A browser based code editor", "author": "Microsoft Corporation", "license": "MIT", diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index b80f1198e7..59aac3b73a 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -30,7 +30,7 @@ if (!/yarn[\w-.]*\.js$|yarnpkg$/.test(process.env['npm_execpath'])) { if (process.platform === 'win32') { if (!hasSupportedVisualStudioVersion()) { - console.error('\033[1;31m*** Invalid C/C++ Compiler Toolchain. Please check https://github.com/microsoft/vscode/wiki/How-to-Contribute.\033[0;0m'); + console.error('\033[1;31m*** Invalid C/C++ Compiler Toolchain. Please check https://github.com/microsoft/vscode/wiki/How-to-Contribute#prerequisites.\033[0;0m'); err = true; } } diff --git a/build/package.json b/build/package.json index 64b914b395..0369cfadff 100644 --- a/build/package.json +++ b/build/package.json @@ -54,7 +54,8 @@ "extract-zip": "^2.0.1", "fs-extra": "^9.1.0", "documentdb": "1.13.0", - "got": "11.8.5", + "got": "11.8.1", + "gulp-merge-json": "^2.1.1", "iconv-lite-umd": "0.6.8", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", @@ -66,7 +67,7 @@ "rollup-plugin-node-resolve": "^5.2.0", "source-map": "0.6.1", "tmp": "^0.2.1", - "typescript": "^4.5.0-dev.20210817", + "typescript": "^4.5.0-dev.20211029", "vsce": "2.8.0", "vscode-universal-bundler": "^0.0.2" }, diff --git a/build/yarn.lock b/build/yarn.lock index 350763419f..20417bb662 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -686,10 +686,17 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" + integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== + dependencies: + ansi-wrap "^0.1.0" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= ansi-regex@^5.0.1: version "5.0.1" @@ -703,6 +710,11 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-wrap@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + anymatch@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -738,6 +750,16 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + asar@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/asar/-/asar-3.0.3.tgz#1fef03c2d6d2de0cbad138788e4f7ae03b129c7b" @@ -762,6 +784,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -931,7 +958,7 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -cacheable-request@^7.0.2: +cacheable-request@^7.0.1: version "7.0.2" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== @@ -966,31 +993,29 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -cheerio-select@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" - integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== +cheerio-select@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696" + integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== dependencies: - boolbase "^1.0.0" - css-select "^5.1.0" - css-what "^6.1.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" + css-select "^4.3.0" + css-what "^6.0.1" + domelementtype "^2.2.0" + domhandler "^4.3.1" + domutils "^2.8.0" cheerio@^1.0.0-rc.9: - version "1.0.0-rc.11" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.11.tgz#1be84be1a126958366bcc57a11648cd9b30a60c2" - integrity sha512-bQwNaDIBKID5ts/DsdhxrjqFXYfLw4ste+wMKqWA8DyKcS4qwsPP4Bk8ZNaTJjvpiX/qW3BT4sU7d6Bh5i+dag== + version "1.0.0-rc.10" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" + integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== dependencies: - cheerio-select "^2.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.3" - domutils "^3.0.1" - htmlparser2 "^8.0.1" - parse5 "^7.0.0" - parse5-htmlparser2-tree-adapter "^7.0.0" - tslib "^2.4.0" + cheerio-select "^1.5.0" + dom-serializer "^1.3.2" + domhandler "^4.2.0" + htmlparser2 "^6.1.0" + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter "^6.0.1" + tslib "^2.2.0" chownr@^1.1.1: version "1.1.4" @@ -1002,6 +1027,11 @@ chromium-pickle-js@^0.2.0: resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" integrity sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= + clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" @@ -1009,6 +1039,25 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= + +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + +cloneable-readable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec" + integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ== + dependencies: + inherits "^2.0.1" + process-nextick-args "^2.0.0" + readable-stream "^2.3.5" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -1107,18 +1156,18 @@ cross-spawn@^7.0.1: shebang-command "^2.0.0" which "^2.0.1" -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== +css-select@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== dependencies: boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" nth-check "^2.0.1" -css-what@^6.1.0: +css-what@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== @@ -1239,35 +1288,56 @@ documentdb@1.13.0: semaphore "1.0.5" underscore "1.8.3" -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== +dom-serializer@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" + integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" + domelementtype "^2.0.1" + domhandler "^4.0.0" + entities "^2.0.0" -domelementtype@^2.3.0: +dom-serializer@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" + integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== + +domelementtype@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== -domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== +domhandler@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" + integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== dependencies: - domelementtype "^2.3.0" + domelementtype "^2.1.0" -domutils@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" - integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== +domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.1" + domelementtype "^2.2.0" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" duplexer3@^0.1.4: version "0.1.4" @@ -1311,10 +1381,10 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -entities@^4.2.0, entities@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.0.tgz#62915f08d67353bb4eb67e3d62641a4059aec656" - integrity sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg== +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== entities@~2.1.0: version "2.1.0" @@ -1398,6 +1468,14 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -1601,17 +1679,17 @@ globalthis@^1.0.1: dependencies: define-properties "^1.1.3" -got@11.8.5: - version "11.8.5" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" - integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== +got@11.8.1: + version "11.8.1" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.1.tgz#df04adfaf2e782babb3daabc79139feec2f7e85d" + integrity sha512-9aYdZL+6nHmvJwHALLwKSUZ0hMwGaJGYv3hoPLPgnT8BoBXm1SjnZeky+91tfwJaDzun2s4RsBRy48IEYv2q2Q== dependencies: "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" "@types/cacheable-request" "^6.0.1" "@types/responselike" "^1.0.0" cacheable-lookup "^5.0.3" - cacheable-request "^7.0.2" + cacheable-request "^7.0.1" decompress-response "^6.0.0" http2-wrapper "^1.0.0-beta.5.2" lowercase-keys "^2.0.0" @@ -1645,6 +1723,17 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +gulp-merge-json@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/gulp-merge-json/-/gulp-merge-json-2.1.2.tgz#7810dbf8f1f1a8638c18d46bad165bd44d18ea79" + integrity sha512-FysBAdHdnQvZzigVJJzlrt6TEosHxVb0mR2h/8eSnd+eJyBvb1LQF1EIrovrOCfj4HGE5p/95wGEjXsJk9qomw== + dependencies: + json5 "^2.2.1" + lodash.mergewith "^4.6.1" + plugin-error "^1.0.1" + through "^2.3.8" + vinyl "^2.2.1" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -1696,15 +1785,15 @@ hosted-git-info@^4.0.2: dependencies: lru-cache "^6.0.0" -htmlparser2@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010" - integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA== +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - domutils "^3.0.1" - entities "^4.3.0" + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" http-cache-semantics@^4.0.0: version "4.1.0" @@ -1763,6 +1852,13 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1792,6 +1888,13 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + is-reference@^1.1.2: version "1.2.1" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" @@ -1821,6 +1924,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -1868,6 +1976,11 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + jsonc-parser@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" @@ -1938,6 +2051,11 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" +lodash.mergewith@^4.6.1: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash.unescape@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" @@ -2074,9 +2192,9 @@ napi-build-utils@^1.0.1: integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== node-abi@^3.3.0: - version "3.22.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.22.0.tgz#00b8250e86a0816576258227edbce7bbe0039362" - integrity sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w== + version "3.15.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.15.0.tgz#cd9ac8c58328129b49998cc6fa16aa5506152716" + integrity sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA== dependencies: semver "^7.3.5" @@ -2131,9 +2249,9 @@ npmlog@^4.0.1: set-blocking "~2.0.0" nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== dependencies: boolbase "^1.0.0" @@ -2153,9 +2271,9 @@ object-assign@^4.1.0: integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= object-inspect@^1.9.0: - version "1.12.1" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.1.tgz#28a661153bad7e470e4b01479ef1cb91ce511191" - integrity sha512-Y/jF6vnvEtOPGiKD1+q+X0CiUYRQtEHp89MLLUJ7TUivtH8Ugn2+3A7Rynqk7BRsAoqeOQWnFnjpDrKSxDgIGA== + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== object-keys@^1.0.12: version "1.1.1" @@ -2193,20 +2311,17 @@ parse-semver@^1.1.1: dependencies: semver "^5.1.0" -parse5-htmlparser2-tree-adapter@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" - integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== +parse5-htmlparser2-tree-adapter@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== dependencies: - domhandler "^5.0.2" - parse5 "^7.0.0" + parse5 "^6.0.1" -parse5@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a" - integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g== - dependencies: - entities "^4.3.0" +parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== path-is-absolute@^1.0.0: version "1.0.1" @@ -2251,6 +2366,16 @@ plist@^3.0.1, plist@^3.0.5: base64-js "^1.5.1" xmlbuilder "^9.0.7" +plugin-error@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" + integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA== + dependencies: + ansi-colors "^1.0.1" + arr-diff "^4.0.0" + arr-union "^3.1.0" + extend-shallow "^3.0.2" + "postcss@5 - 7": version "7.0.36" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb" @@ -2289,7 +2414,7 @@ priorityqueuejs@1.0.0, priorityqueuejs@^1.0.0: resolved "https://registry.yarnpkg.com/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz#2ee4f23c2560913e08c07ce5ccdd6de3df2c5af8" integrity sha1-LuTyPCVgkT4IwHzlzN1t498sWvg= -process-nextick-args@~2.0.0: +process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== @@ -2361,7 +2486,7 @@ read@^1.0.7: dependencies: mute-stream "~0.0.4" -readable-stream@^2.0.0, readable-stream@^2.0.6: +readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.3.5: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -2383,6 +2508,16 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +replace-ext@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== + request@^2.86.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -2737,6 +2872,11 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + tmp@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -2781,7 +2921,7 @@ tslib@^2.0.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== -tslib@^2.4.0: +tslib@^2.2.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== @@ -2816,18 +2956,18 @@ type-fest@^0.13.1: integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== typed-rest-client@^1.8.4: - version "1.8.9" - resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.8.9.tgz#e560226bcadfe71b0fb5c416b587f8da3b8f92d8" - integrity sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g== + version "1.8.6" + resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.8.6.tgz#d8facd6abd98cbd8ad14cccf056ca5cc306919d7" + integrity sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA== dependencies: qs "^6.9.1" tunnel "0.0.6" underscore "^1.12.1" -typescript@^4.5.0-dev.20210817: - version "4.5.0-dev.20210817" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.0-dev.20210817.tgz#6b0d0ce68c2381cc85fd0d609817cb3576eb9480" - integrity sha512-G427tdOZrQKSEUcLF+dq57gK7D6CzxhbZggpEwqZP1HDuBhIk2bu+br9QvR5uoubR2P6lHhWhUZaCDmkIpnnDQ== +typescript@^4.5.0-dev.20211029: + version "4.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" + integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -2907,6 +3047,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vinyl@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" + integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + vsce@2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/vsce/-/vsce-2.8.0.tgz#3d40d369d8d774fa221318a2a39ed1d6b353c3d7" diff --git a/cglicenses.json b/cglicenses.json index 6973e9aec1..2398fe5c40 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -102,22 +102,6 @@ "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." ] }, - { - // Reason: 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", - "Copyright (C) 2012-2018 by various contributors (see AUTHORS)", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - ] - }, { // Reason: Repository lacks license text. // https://github.com/othiym23/emitter-listener/blob/master/package.json declares BSD-2-Clause. diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 096e3014ae..aabe3c0021 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -51,7 +51,8 @@ ".devcontainer.json" ], "filenamePatterns": [ - "**/.devcontainer/devcontainer.json" + "**/.devcontainer/devcontainer.json", + "**/User/snippets/*.json" ] } ], diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index 9fbc5d0fec..48d51d78f0 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -14,9 +14,19 @@ }, "forwardPorts": { "type": "array", - "description": "Ports that are forwarded from the container to the local machine.", + "description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".", "items": { - "type": "integer" + "oneOf": [ + { + "type": "integer", + "maximum": 65535, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([a-z0-9\\-]+):(\\d{1,5})$" + } + ] } }, "portsAttributes": { diff --git a/extensions/configuration-editing/schemas/devContainer.schema.generated.json b/extensions/configuration-editing/schemas/devContainer.schema.generated.json index bd10964f3e..19e769e95b 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.generated.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.generated.json @@ -124,13 +124,26 @@ "$ref": "vscode://schemas/settings/machine", "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." }, + "features": { + "type": "object", + "description": "Features to add to the dev container.", + "additionalProperties": true + }, "forwardPorts": { "type": "array", - "description": "Ports that are forwarded from the container to the local machine.", + "description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".", "items": { - "type": "integer", - "maximum": 65535, - "minimum": 0 + "oneOf": [ + { + "type": "integer", + "maximum": 65535, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([a-z0-9\\-]+):(\\d{1,5})$" + } + ] } }, "portsAttributes": { @@ -263,7 +276,7 @@ }, "updateRemoteUserUID": { "type": "boolean", - "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default." + "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder." }, "remoteEnv": { "type": "object", @@ -514,13 +527,26 @@ "$ref": "vscode://schemas/settings/machine", "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." }, + "features": { + "type": "object", + "description": "Features to add to the dev container.", + "additionalProperties": true + }, "forwardPorts": { "type": "array", - "description": "Ports that are forwarded from the container to the local machine.", + "description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".", "items": { - "type": "integer", - "maximum": 65535, - "minimum": 0 + "oneOf": [ + { + "type": "integer", + "maximum": 65535, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([a-z0-9\\-]+):(\\d{1,5})$" + } + ] } }, "portsAttributes": { @@ -653,7 +679,7 @@ }, "updateRemoteUserUID": { "type": "boolean", - "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default." + "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder." }, "remoteEnv": { "type": "object", @@ -870,13 +896,26 @@ "$ref": "vscode://schemas/settings/machine", "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." }, + "features": { + "type": "object", + "description": "Features to add to the dev container.", + "additionalProperties": true + }, "forwardPorts": { "type": "array", - "description": "Ports that are forwarded from the container to the local machine.", + "description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".", "items": { - "type": "integer", - "maximum": 65535, - "minimum": 0 + "oneOf": [ + { + "type": "integer", + "maximum": 65535, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([a-z0-9\\-]+):(\\d{1,5})$" + } + ] } }, "portsAttributes": { @@ -1009,7 +1048,7 @@ }, "updateRemoteUserUID": { "type": "boolean", - "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default." + "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder." }, "remoteEnv": { "type": "object", @@ -1200,13 +1239,26 @@ "$ref": "vscode://schemas/settings/machine", "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." }, + "features": { + "type": "object", + "description": "Features to add to the dev container.", + "additionalProperties": true + }, "forwardPorts": { "type": "array", - "description": "Ports that are forwarded from the container to the local machine.", + "description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".", "items": { - "type": "integer", - "maximum": 65535, - "minimum": 0 + "oneOf": [ + { + "type": "integer", + "maximum": 65535, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([a-z0-9\\-]+):(\\d{1,5})$" + } + ] } }, "portsAttributes": { @@ -1339,7 +1391,7 @@ }, "updateRemoteUserUID": { "type": "boolean", - "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default." + "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder." }, "remoteEnv": { "type": "object", @@ -1495,13 +1547,26 @@ "$ref": "vscode://schemas/settings/machine", "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." }, + "features": { + "type": "object", + "description": "Features to add to the dev container.", + "additionalProperties": true + }, "forwardPorts": { "type": "array", - "description": "Ports that are forwarded from the container to the local machine.", + "description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".", "items": { - "type": "integer", - "maximum": 65535, - "minimum": 0 + "oneOf": [ + { + "type": "integer", + "maximum": 65535, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([a-z0-9\\-]+):(\\d{1,5})$" + } + ] } }, "portsAttributes": { @@ -1634,7 +1699,7 @@ }, "updateRemoteUserUID": { "type": "boolean", - "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default." + "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder." }, "remoteEnv": { "type": "object", diff --git a/extensions/configuration-editing/schemas/devContainer.schema.src.json b/extensions/configuration-editing/schemas/devContainer.schema.src.json index 9c6da0adcb..e42d3655d2 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.src.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.src.json @@ -24,13 +24,26 @@ "$ref": "vscode://schemas/settings/machine", "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." }, + "features": { + "type": "object", + "description": "Features to add to the dev container.", + "additionalProperties": true + }, "forwardPorts": { "type": "array", - "description": "Ports that are forwarded from the container to the local machine.", + "description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".", "items": { - "type": "integer", - "maximum": 65535, - "minimum": 0 + "oneOf": [ + { + "type": "integer", + "maximum": 65535, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([a-z0-9\\-]+):(\\d{1,5})$" + } + ] } }, "portsAttributes": { @@ -78,7 +91,10 @@ }, "protocol": { "type": "string", - "enum": ["http", "https"], + "enum": [ + "http", + "https" + ], "description": "The protocol to use when forwarding this port." } }, @@ -140,7 +156,10 @@ }, "protocol": { "type": "string", - "enum": ["http", "https"], + "enum": [ + "http", + "https" + ], "description": "The protocol to use when forwarding this port." } }, @@ -156,7 +175,7 @@ }, "updateRemoteUserUID": { "type": "boolean", - "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default." + "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder." }, "remoteEnv": { "type": "object", diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index f788d11b6d..96beef5d0a 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -83,7 +83,7 @@ export class SettingsDocument { completions.push(this.newSimpleCompletionItem('${folderPath}', range, localize('folderPath', "file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder)"))); completions.push(this.newSimpleCompletionItem('${appName}', range, localize('appName', "e.g. VS Code"))); completions.push(this.newSimpleCompletionItem('${remoteName}', range, localize('remoteName', "e.g. SSH"))); - completions.push(this.newSimpleCompletionItem('${dirty}', range, localize('dirty', "a dirty indicator if the active editor is dirty"))); + completions.push(this.newSimpleCompletionItem('${dirty}', range, localize('dirty', "an indicator for when the active editor has unsaved changes"))); completions.push(this.newSimpleCompletionItem('${separator}', range, localize('separator', "a conditional separator (' - ') that only shows when surrounded by variables with values"))); return Promise.resolve(completions); diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index 8b65e5000f..87e298088b 100644 --- a/extensions/csharp/cgmanifest.json +++ b/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "16612717ccd557383c0c821d7b6ae6662492ffde" + "commitHash": "5426265f1be3f8056a984b709fadf56b9ce4c400" } }, "license": "MIT", diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index f6317202d2..15384e0f58 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.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/dotnet/csharp-tmLanguage/commit/16612717ccd557383c0c821d7b6ae6662492ffde", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/5426265f1be3f8056a984b709fadf56b9ce4c400", "name": "C#", "scopeName": "source.cs", "patterns": [ diff --git a/extensions/git/package.json b/extensions/git/package.json index cc69e1f7fc..5a222f7647 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2128,6 +2128,7 @@ "deprecationMessage": "This setting is now deprecated, please use `github.gitAuthentication` instead." }, "git.timeline.date": { + "type": "string", "enum": [ "committed", "authored" @@ -2145,6 +2146,28 @@ "default": true, "description": "%config.timeline.showAuthor%", "scope": "window" + }, + "git.showUnpublishedCommitsButton": { + "type": "string", + "enum": [ + "always", + "whenEmpty", + "never" + ], + "enumDescriptions": [ + "%config.showUnpublishedCommitsButton.always%", + "%config.showUnpublishedCommitsButton.whenEmpty%", + "%config.showUnpublishedCommitsButton.never%" + ], + "default": "whenEmpty", + "description": "%config.showUnpublishedCommitsButton%", + "scope": "resource" + }, + "git.statusLimit": { + "type": "number", + "scope": "resource", + "default": 10000, + "description": "%config.statusLimit%" } } }, @@ -2382,8 +2405,8 @@ "byline": "^5.0.0", "file-type": "^7.2.0", "iconv-lite-umd": "0.6.8", - "jschardet": "2.3.0", - "vscode-extension-telemetry": "0.2.8", + "jschardet": "3.0.0", + "vscode-extension-telemetry": "0.4.2", "vscode-nls": "^4.0.0", "vscode-uri": "^2.0.0", "which": "^1.3.0" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 45987d3993..db12d57597 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -185,6 +185,11 @@ "config.timeline.date.committed": "Use the committed date", "config.timeline.date.authored": "Use the authored date", "config.useCommitInputAsStashMessage": "Controls whether to use the message from the commit input box as the default stash message.", + "config.showUnpublishedCommitsButton": "Controls whether to show an action button to sync or publish, if there are unpublished commits.", + "config.showUnpublishedCommitsButton.always": "Always shows the action button, if there are unpublished commits.", + "config.showUnpublishedCommitsButton.whenEmpty": "Only shows the action button if there are no other changes and there are unpublished commits.", + "config.showUnpublishedCommitsButton.never": "Never shows the action button.", + "config.statusLimit": "Controls how to limit the number of changes that can be parsed from Git status command. Can be set to 0 for no limit.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index c465cb5b65..d965fa3215 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -694,27 +694,26 @@ export class CommandCenter { viewColumn: ViewColumn.Active }; - let document; - try { - document = await workspace.openTextDocument(uri); - } catch (error) { - await commands.executeCommand('vscode.open', uri, { - ...opts, - override: arg instanceof Resource && arg.type === Status.BOTH_MODIFIED ? false : undefined - }); + await commands.executeCommand('vscode.open', uri, { + ...opts, + override: arg instanceof Resource && arg.type === Status.BOTH_MODIFIED ? false : undefined + }); + + const document = window.activeTextEditor?.document; + + // If the document doesn't match what we opened then don't attempt to select the range + if (document?.uri.toString() !== uri.toString()) { continue; } // Check if active text editor has same path as other editor. we cannot compare via // URI.toString() here because the schemas can be different. Instead we just go by path. - if (activeTextEditor && activeTextEditor.document.uri.path === uri.path) { + if (activeTextEditor && activeTextEditor.document.uri.path === uri.path && document) { // preserve not only selection but also visible range opts.selection = activeTextEditor.selection; const previousVisibleRanges = activeTextEditor.visibleRanges; const editor = await window.showTextDocument(document, opts); editor.revealRange(previousVisibleRanges[0]); - } else { - await commands.executeCommand('vscode.open', uri, opts); } } } diff --git a/extensions/git/src/fileSystemProvider.ts b/extensions/git/src/fileSystemProvider.ts index 202e8f45f8..c4e70b88c2 100644 --- a/extensions/git/src/fileSystemProvider.ts +++ b/extensions/git/src/fileSystemProvider.ts @@ -48,13 +48,6 @@ export class GitFileSystemProvider implements FileSystemProvider { model.onDidChangeRepository(this.onDidChangeRepository, this), model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this), workspace.registerFileSystemProvider('git', this, { isReadonly: true, isCaseSensitive: true }), - workspace.registerResourceLabelFormatter({ - scheme: 'git', - formatting: { - label: '${path} (git)', - separator: '/' - } - }) ); setInterval(() => this.cleanup(), FIVE_MINUTES); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 5a93d09152..c956822568 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1842,7 +1842,7 @@ export class Repository { const onStdoutData = (raw: string) => { parser.update(raw); - if (parser.status.length > limit) { + if (limit !== 0 && parser.status.length > limit) { child.removeListener('exit', onExit); child.stdout!.removeListener('data', onStdoutData); child.kill(); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index f512d3a9e4..58f17f1c4d 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -842,7 +842,7 @@ export class Repository implements Disposable { return this.repository.dotGit; } - private isRepositoryHuge = false; + private isRepositoryHuge: false | { limit: number } = false; private didWarnAboutLimit = false; private resourceCommandResolver = new ResourceCommandResolver(this); @@ -917,6 +917,8 @@ export class Repository implements Disposable { || e.affectsConfiguration('git.untrackedChanges', root) || e.affectsConfiguration('git.ignoreSubmodules', root) || e.affectsConfiguration('git.openDiffOnClick', root) + || e.affectsConfiguration('git.rebaseWhenSync', root) + || e.affectsConfiguration('git.showUnpublishedCommitsButton', root) )(this.updateModelState, this, this.disposables); const updateInputBoxVisibility = () => { @@ -971,6 +973,14 @@ export class Repository implements Disposable { } validateInput(text: string, position: number): SourceControlInputBoxValidation | undefined { + let tooManyChangesWarning: SourceControlInputBoxValidation | undefined; + if (this.isRepositoryHuge) { + tooManyChangesWarning = { + message: localize('tooManyChangesWarning', "Too many changes were detected. Only the first {0} changes will be shown below.", this.isRepositoryHuge.limit), + type: SourceControlInputBoxValidationType.Warning + }; + } + if (this.rebaseCommit) { if (this.rebaseCommit.message !== text) { return { @@ -984,7 +994,7 @@ export class Repository implements Disposable { const setting = config.get<'always' | 'warn' | 'off'>('inputValidation'); if (setting === 'off') { - return; + return tooManyChangesWarning; } if (/^\s+$/.test(text)) { @@ -1020,7 +1030,7 @@ export class Repository implements Disposable { if (line.length <= threshold) { if (setting !== 'always') { - return; + return tooManyChangesWarning; } return { @@ -1790,12 +1800,16 @@ export class Repository implements Disposable { const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root)); const ignoreSubmodules = scopedConfig.get('ignoreSubmodules'); - const { status, didHitLimit } = await this.repository.getStatus({ ignoreSubmodules }); + const limit = scopedConfig.get('statusLimit', 5000); + + const { status, didHitLimit } = await this.repository.getStatus({ limit, ignoreSubmodules }); const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreLimitWarning') === true; const useIcons = !config.get('decorations.enabled', true); - this.isRepositoryHuge = didHitLimit; + this.isRepositoryHuge = didHitLimit ? { limit } : false; + // Triggers or clears any validation warning + this._sourceControl.inputBox.validateInput = this._sourceControl.inputBox.validateInput; if (didHitLimit && !shouldIgnore && !this.didWarnAboutLimit) { const knownHugeFolderPaths = await this.findKnownHugeFolderPathsToIgnore(); @@ -1808,18 +1822,21 @@ export class Repository implements Disposable { const addKnown = localize('add known', "Would you like to add '{0}' to .gitignore?", folderName); const yes = { title: localize('yes', "Yes") }; + const no = { title: localize('no', "No") }; - const result = await window.showWarningMessage(`${gitWarn} ${addKnown}`, yes, neverAgain); - - if (result === neverAgain) { - config.update('ignoreLimitWarning', true, false); - this.didWarnAboutLimit = true; - } else if (result === yes) { + const result = await window.showWarningMessage(`${gitWarn} ${addKnown}`, yes, no, neverAgain); + if (result === yes) { this.ignore([Uri.file(folderPath)]); + } else { + if (result === neverAgain) { + config.update('ignoreLimitWarning', true, false); + } + + this.didWarnAboutLimit = true; } } else { - const result = await window.showWarningMessage(gitWarn, neverAgain); - + const ok = { title: localize('ok', "OK") }; + const result = await window.showWarningMessage(gitWarn, ok, neverAgain); if (result === neverAgain) { config.update('ignoreLimitWarning', true, false); } @@ -1906,6 +1923,37 @@ export class Repository implements Disposable { return undefined; }); + let actionButton: SourceControl['actionButton']; + if (HEAD !== undefined) { + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const showActionButton = config.get('showUnpublishedCommitsButton', 'whenEmpty'); + + if (showActionButton === 'always' || (showActionButton === 'whenEmpty' && workingTree.length === 0 && index.length === 0 && untracked.length === 0 && merge.length === 0)) { + if (HEAD.name && HEAD.commit) { + if (HEAD.upstream) { + if (HEAD.ahead) { + const rebaseWhenSync = config.get('rebaseWhenSync'); + + actionButton = { + command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync', + title: localize('scm button sync title', ' Sync Changes $(sync){0}{1}', HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`), + tooltip: this.syncTooltip, + arguments: [this._sourceControl], + }; + } + } else { + actionButton = { + command: 'git.publish', + title: localize('scm button publish title', "$(cloud-upload) Publish Changes"), + tooltip: localize('scm button publish tooltip', "Publish Changes"), + arguments: [this._sourceControl], + }; + } + } + } + } + this._sourceControl.actionButton = actionButton; + // set resource groups this.mergeGroup.resourceStates = merge; this.indexGroup.resourceStates = index; diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 91efe80257..ce30073eb1 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -56,15 +56,15 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -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== -vscode-extension-telemetry@0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.2.8.tgz#670c625c44791237c040cee2cb9f567ca34784ac" - integrity sha512-Vf52im5qzORRD2K5Ryp8PXo31YXVcJAYRSDDZGegWlt0OATOd83DYabS1U/WIq9nR5g80UQKH3+BsenhpQHUaA== +vscode-extension-telemetry@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.2.tgz#6ef847a80c9cfc207eb15e3a254f235acebb65a5" + integrity sha512-y0f51mVoFxHIzULQNCC26TBFIKdEC7uckS3tFoK++OOOl8mU2LlOxgmbd52T/SXoXNg5aI7xqs+4V2ug5ITvKw== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 2381465dcf..a017a5ae6d 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -61,7 +61,7 @@ "dependencies": { "node-fetch": "^2.6.7", "uuid": "8.1.0", - "vscode-extension-telemetry": "0.2.8", + "vscode-extension-telemetry": "0.4.2", "vscode-nls": "^5.0.0", "vscode-tas-client": "^0.1.42" }, diff --git a/extensions/github-authentication/src/common/keychain.ts b/extensions/github-authentication/src/common/keychain.ts index 516e29de9e..dae357186b 100644 --- a/extensions/github-authentication/src/common/keychain.ts +++ b/extensions/github-authentication/src/common/keychain.ts @@ -5,29 +5,12 @@ // keytar depends on a native module shipped in vscode, so this is // how we load it -import type * as keytarType from 'keytar'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { Log } from './logger'; const localize = nls.loadMessageBundle(); -function getKeytar(): Keytar | undefined { - try { - return require('keytar'); - } catch (err) { - console.log(err); - } - - return undefined; -} - -export type Keytar = { - getPassword: typeof keytarType['getPassword']; - setPassword: typeof keytarType['setPassword']; - deletePassword: typeof keytarType['deletePassword']; -}; - export class Keychain { constructor( private readonly context: vscode.ExtensionContext, @@ -72,25 +55,4 @@ export class Keychain { return Promise.resolve(undefined); } } - - async tryMigrate(): Promise { - try { - const keytar = getKeytar(); - if (!keytar) { - throw new Error('keytar unavailable'); - } - - const oldValue = await keytar.getPassword(`${vscode.env.uriScheme}-github.login`, 'account'); - if (oldValue) { - this.Logger.trace('Attempting to migrate from keytar to secret store...'); - await this.setToken(oldValue); - await keytar.deletePassword(`${vscode.env.uriScheme}-github.login`, 'account'); - } - - return oldValue; - } catch (_) { - // Ignore - return Promise.resolve(undefined); - } - } } diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index e3c15b2de7..5a0120607c 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -117,7 +117,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid let sessionData: SessionData[]; try { this._logger.info('Reading sessions from keychain...'); - const storedSessions = await this._keychain.getToken() || await this._keychain.tryMigrate(); + const storedSessions = await this._keychain.getToken(); if (!storedSessions) { return []; } @@ -195,12 +195,13 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid scopes: JSON.stringify(scopes), }); - const token = await this._githubServer.login(scopes.join(' ')); + const scopeString = scopes.join(' '); + const token = await this._githubServer.login(scopeString); this.afterTokenLoad(token); const session = await this.tokenToSession(token, scopes); const sessions = await this._sessionsPromise; - const sessionIndex = sessions.findIndex(s => s.id === session.id); + const sessionIndex = sessions.findIndex(s => s.id === session.id || s.scopes.join(' ') === scopeString); if (sessionIndex > -1) { sessions.splice(sessionIndex, 1, session); } else { diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 6dbad0c4ca..7fb6a9a0e7 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -122,15 +122,13 @@ export class GitHubServer implements IGitHubServer { // TODO@joaomoreno TODO@TylerLeonhardt private async isNoCorsEnvironment(): Promise { const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); - return (uri.scheme === 'https' && /^(vscode|github)\./.test(uri.authority)) || (uri.scheme === 'http' && /^localhost/.test(uri.authority)); + return (uri.scheme === 'https' && /^((insiders\.)?vscode|github)\./.test(uri.authority)) || (uri.scheme === 'http' && /^localhost/.test(uri.authority)); } public async login(scopes: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); - // TODO@joaomoreno TODO@TylerLeonhardt - const nocors = await this.isNoCorsEnvironment(); - const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate${nocors ? '?nocors=true' : ''}`)); + const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); if (this.isTestEnvironment(callbackUri)) { const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true }); @@ -148,7 +146,7 @@ export class GitHubServer implements IGitHubServer { return tokenScopes.includes(splitScopes); }); })) { - throw new Error(`The provided token is does not match the requested scopes: ${scopes}`); + throw new Error(`The provided token does not match the requested scopes: ${scopes}`); } return token; @@ -160,7 +158,7 @@ export class GitHubServer implements IGitHubServer { const existingStates = this._pendingStates.get(scopes) || []; this._pendingStates.set(scopes, [...existingStates, state]); - const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com${nocors ? '&nocors=true' : ''}`); + const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com`); await vscode.env.openExternal(uri); // Register a single listener for the URI callback, in case the user starts the login process multiple times @@ -208,34 +206,23 @@ export class GitHubServer implements IGitHubServer { const url = `https://${AUTH_RELAY_SERVER}/token?code=${code}&state=${query.state}`; this._logger.info('Exchanging code for token...'); - // TODO@joao: remove - if (query.nocors) { - try { - const json: any = await vscode.commands.executeCommand('_workbench.fetchJSON', url, 'POST'); + try { + const result = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json' + } + }); + + if (result.ok) { + const json = await result.json(); this._logger.info('Token exchange success!'); resolve(json.access_token); - } catch (err) { - reject(err); - } - } else { - try { - const result = await fetch(url, { - method: 'POST', - headers: { - Accept: 'application/json' - } - }); - - if (result.ok) { - const json = await result.json(); - this._logger.info('Token exchange success!'); - resolve(json.access_token); - } else { - reject(result.statusText); - } - } catch (ex) { - reject(ex); + } else { + reject(result.statusText); } + } catch (ex) { + reject(ex); } }; @@ -291,6 +278,9 @@ export class GitHubServer implements IGitHubServer { } public async sendAdditionalTelemetryInfo(token: string): Promise { + if (!vscode.env.isTelemetryEnabled) { + return; + } const nocors = await this.isNoCorsEnvironment(); if (nocors) { @@ -394,7 +384,7 @@ export class GitHubEnterpriseServer implements IGitHubServer { return tokenScopes.includes(splitScopes); }); })) { - throw new Error(`The provided token is does not match the requested scopes: ${scopes}`); + throw new Error(`The provided token does not match the requested scopes: ${scopes}`); } return token; diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 03da2fbec9..c02f4f8ca3 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -99,10 +99,10 @@ uuid@8.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== -vscode-extension-telemetry@0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.2.8.tgz#670c625c44791237c040cee2cb9f567ca34784ac" - integrity sha512-Vf52im5qzORRD2K5Ryp8PXo31YXVcJAYRSDDZGegWlt0OATOd83DYabS1U/WIq9nR5g80UQKH3+BsenhpQHUaA== +vscode-extension-telemetry@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.2.tgz#6ef847a80c9cfc207eb15e3a254f235acebb65a5" + integrity sha512-y0f51mVoFxHIzULQNCC26TBFIKdEC7uckS3tFoK++OOOl8mU2LlOxgmbd52T/SXoXNg5aI7xqs+4V2ug5ITvKw== vscode-nls@^5.0.0: version "5.0.0" diff --git a/extensions/github/src/pushErrorHandler.ts b/extensions/github/src/pushErrorHandler.ts index 561f27a612..0f40a38d19 100644 --- a/extensions/github/src/pushErrorHandler.ts +++ b/extensions/github/src/pushErrorHandler.ts @@ -82,7 +82,7 @@ async function handlePushError(repository: Repository, remote: Remote, refspec: await repository.push('origin', localName, true); - return [octokit, ghRepository]; + return [octokit, ghRepository] as const; }); // yield diff --git a/extensions/image-preview/media/main.js b/extensions/image-preview/media/main.js index 65febc80ee..bfbf7d7ec1 100644 --- a/extensions/image-preview/media/main.js +++ b/extensions/image-preview/media/main.js @@ -59,7 +59,7 @@ ]; const settings = getSettings(); - const isMac = settings.isMac; + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const vscode = acquireVsCodeApi(); diff --git a/extensions/image-preview/package.json b/extensions/image-preview/package.json index ce2751cd9c..de4f79d442 100644 --- a/extensions/image-preview/package.json +++ b/extensions/image-preview/package.json @@ -80,7 +80,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "vscode-extension-telemetry": "0.2.8", + "vscode-extension-telemetry": "0.4.2", "vscode-nls": "^5.0.0" }, "repository": { diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index d021265b9d..a3a7086229 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -93,6 +93,7 @@ class Preview extends Disposable { webviewEditor.webview.options = { enableScripts: true, + enableForms: false, localResourceRoots: [ resourceRoot, extensionRoot, @@ -205,7 +206,6 @@ class Preview extends Disposable { private async getWebviewContents(): Promise { const version = Date.now().toString(); const settings = { - isMac: isMac(), src: await this.getResourcePath(this.webviewEditor, this.resource, version), }; @@ -261,15 +261,6 @@ class Preview extends Disposable { } } -declare const process: undefined | { readonly platform: string }; - -function isMac(): boolean { - if (typeof process === 'undefined') { - return false; - } - return process.platform === 'darwin'; -} - function escapeAttribute(value: string | vscode.Uri): string { return value.toString().replace(/"/g, '"'); } diff --git a/extensions/image-preview/yarn.lock b/extensions/image-preview/yarn.lock index 0f034e4005..cab716b803 100644 --- a/extensions/image-preview/yarn.lock +++ b/extensions/image-preview/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -vscode-extension-telemetry@0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.2.8.tgz#670c625c44791237c040cee2cb9f567ca34784ac" - integrity sha512-Vf52im5qzORRD2K5Ryp8PXo31YXVcJAYRSDDZGegWlt0OATOd83DYabS1U/WIq9nR5g80UQKH3+BsenhpQHUaA== +vscode-extension-telemetry@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.2.tgz#6ef847a80c9cfc207eb15e3a254f235acebb65a5" + integrity sha512-y0f51mVoFxHIzULQNCC26TBFIKdEC7uckS3tFoK++OOOl8mU2LlOxgmbd52T/SXoXNg5aI7xqs+4V2ug5ITvKw== vscode-nls@^5.0.0: version "5.0.0" diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index 8fa9cb9786..d43cc356cb 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -1,103 +1,31 @@ { - // Note that this file should stay in sync with 'typescript-language-basics/language-configuration.json' "comments": { "lineComment": "//", - "blockComment": [ - "/*", - "*/" - ] + "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" - ] - } + { "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": [ - [ - "{", - "}" - ], - [ - "[", - "]" - ], - [ - "(", - ")" - ], - [ - "'", - "'" - ], - [ - "\"", - "\"" - ], - [ - "`", - "`" - ], - [ - "<", - ">" - ] + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["'", "'"], + ["\"", "\""], + ["`", "`"], + ["<", ">"] ], "autoCloseBefore": ";:.,=}])>` \n\t", "folding": { @@ -105,89 +33,5 @@ "start": "^\\s*//\\s*#?region\\b", "end": "^\\s*//\\s*#?endregion\\b" } - }, - "wordPattern": { - "pattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>/\\?\\s]+)", - }, - "indentationRules": { - "decreaseIndentPattern": { - "pattern": "^((?!.*?/\\*).*\\*\/)?\\s*[\\}\\]].*$" - }, - "increaseIndentPattern": { - "pattern": "^((?!//).)*(\\{([^}\"'`/]*|(\\t|[ ])*//.*)|\\([^)\"'`/]*|\\[[^\\]\"'`/]*)$" - }, - // e.g. * ...| or */| or *-----*/| - "unIndentedLinePattern": { - "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$" - } - }, - "onEnterRules": [ - { - // e.g. /** | */ - "beforeText": { - "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" - }, - "afterText": { - "pattern": "^\\s*\\*/$" - }, - "action": { - "indent": "indentOutdent", - "appendText": " * " - } - }, - { - // e.g. /** ...| - "beforeText": { - "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" - }, - "action": { - "indent": "none", - "appendText": " * " - } - }, - { - // e.g. * ...| - "beforeText": { - "pattern": "^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$" - }, - "previousLineText": { - "pattern": "(?=^(\\s*(/\\*\\*|\\*)).*)(?=(?!(\\s*\\*/)))" - }, - "action": { - "indent": "none", - "appendText": "* " - } - }, - { - // e.g. */| - "beforeText": { - "pattern": "^(\\t|[ ])*[ ]\\*/\\s*$" - }, - "action": { - "indent": "none", - "removeText": 1 - }, - }, - { - // e.g. *-----*/| - "beforeText": { - "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$" - }, - "action": { - "indent": "none", - "removeText": 1 - }, - }, - { - "beforeText": { - "pattern": "^\\s*(\\bcase\\s.+:|\\bdefault:)$" - }, - "afterText": { - "pattern": "^(?!\\s*(\\bcase\\b|\\bdefault\\b))" - }, - "action": { - "indent": "indent" - } - } - ] + } } diff --git a/extensions/javascript/tags-language-configuration.json b/extensions/javascript/tags-language-configuration.json index 6616eeca79..1458351437 100644 --- a/extensions/javascript/tags-language-configuration.json +++ b/extensions/javascript/tags-language-configuration.json @@ -37,6 +37,11 @@ ")" ] ], + "colorizedBracketPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], "autoClosingPairs": [ { "open": "{", diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index f4e0fc1e31..92e0c0a382 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -220,7 +220,9 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua */ runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriPath }); } - return runtime.http.getContent(uriPath); + return runtime.http.getContent(uriPath).catch(e => { + return Promise.reject(new ResponseError(4, e.toString())); + }); } else { return Promise.reject(new ResponseError(1, localize('schemaDownloadDisabled', 'Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload))); } @@ -233,7 +235,6 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua } return false; }; - const handleActiveEditorChange = (activeEditor?: TextEditor) => { if (!activeEditor) { return; diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index 2f705bc36e..3f7192bfbd 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -64,12 +64,19 @@ function getPackageInfo(context: ExtensionContext): IPackageInfo { function getHTTPRequestService(): RequestService { return { - getContent(uri: string, _encoding?: string) { + getContent(uri: string, _encoding?: string): Promise { const headers = { 'Accept-Encoding': 'gzip, deflate' }; return xhr({ url: uri, followRedirects: 5, headers }).then(response => { return response.responseText; }, (error: XHRResponse) => { - return Promise.reject(error.responseText || getErrorStatusDescription(error.status) || error.toString()); + let status = getErrorStatusDescription(error.status); + if (status && error.responseText) { + status = `${status}\n${error.responseText.substring(0, 200)}`; + } + if (!status) { + status = error.toString(); + } + return Promise.reject(status); }); } }; diff --git a/extensions/json-language-features/client/src/requests.ts b/extensions/json-language-features/client/src/requests.ts index f4ad69adc4..71366d8c6c 100644 --- a/extensions/json-language-features/client/src/requests.ts +++ b/extensions/json-language-features/client/src/requests.ts @@ -6,7 +6,7 @@ import { Uri } from 'vscode'; export interface RequestService { - getContent(uri: string, encoding?: string): Thenable; + getContent(uri: string, encoding?: string): Promise; } export function getScheme(uri: string) { diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 1428705ead..b92282c62b 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -135,7 +135,7 @@ }, "dependencies": { "request-light": "^0.5.4", - "vscode-extension-telemetry": "0.2.8", + "vscode-extension-telemetry": "0.4.2", "vscode-languageclient": "^7.0.0", "vscode-nls": "^5.0.0" }, diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 8be0e38b69..cfbbe5e5ab 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -14,7 +14,7 @@ "dependencies": { "jsonc-parser": "^3.0.0", "request-light": "^0.5.4", - "vscode-json-languageservice": "^4.1.6", + "vscode-json-languageservice": "^4.1.9", "vscode-languageserver": "^7.0.0", "vscode-uri": "^3.0.2" }, diff --git a/extensions/json-language-features/server/src/utils/runner.ts b/extensions/json-language-features/server/src/utils/runner.ts index 5db8e8b9da..dc7f16415c 100644 --- a/extensions/json-language-features/server/src/utils/runner.ts +++ b/extensions/json-language-features/server/src/utils/runner.ts @@ -23,6 +23,7 @@ export function runSafeAsync(runtime: RuntimeEnvironment, func: () => Thenabl runtime.timer.setImmediate(() => { if (token.isCancellationRequested) { resolve(cancelValue()); + return; } return func().then(result => { if (token.isCancellationRequested) { diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 45be6f5853..06fd41ef11 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -22,10 +22,10 @@ request-light@^0.5.4: resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.4.tgz#497a98c6d8ae49536417a5e2d7f383b934f3e38c" integrity sha512-t3566CMweOFlUk7Y1DJMu5OrtpoZEb6aSTsLQVT3wtrIEJ5NhcY9G/Oqxvjllzl4a15zXfFlcr9q40LbLVQJqw== -vscode-json-languageservice@^4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.6.tgz#4275e8daf1cba80273c318f33fbf7a2ede307053" - integrity sha512-DIKb3tcfRtb3tIE6g9SLOl5E9tNSt6kljH08Wa5RwFlVshtXGrDDzttchze4CYy9pJpE9mBtCbRHmLvY1Z1ZXA== +vscode-json-languageservice@^4.1.9: + version "4.1.9" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.9.tgz#fb48edc69e37167c3cafd447c3fa898052d87b61" + integrity sha512-kxNHitUy2fCxmP6vAp0SRLrUSuecUYzzxlC+85cC3jJlFHWmvtCJOzikC+kcUnIdls9fQSB8n0yHs8Sl6taxJw== dependencies: jsonc-parser "^3.0.0" vscode-languageserver-textdocument "^1.0.1" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index feab12153b..4ae959a91a 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -51,10 +51,10 @@ semver@^7.3.4: dependencies: lru-cache "^6.0.0" -vscode-extension-telemetry@0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.2.8.tgz#670c625c44791237c040cee2cb9f567ca34784ac" - integrity sha512-Vf52im5qzORRD2K5Ryp8PXo31YXVcJAYRSDDZGegWlt0OATOd83DYabS1U/WIq9nR5g80UQKH3+BsenhpQHUaA== +vscode-extension-telemetry@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.2.tgz#6ef847a80c9cfc207eb15e3a254f235acebb65a5" + integrity sha512-y0f51mVoFxHIzULQNCC26TBFIKdEC7uckS3tFoK++OOOl8mU2LlOxgmbd52T/SXoXNg5aI7xqs+4V2ug5ITvKw== vscode-jsonrpc@6.0.0: version "6.0.0" diff --git a/extensions/markdown-basics/language-configuration.json b/extensions/markdown-basics/language-configuration.json index d5d64a80ae..6d59777e02 100644 --- a/extensions/markdown-basics/language-configuration.json +++ b/extensions/markdown-basics/language-configuration.json @@ -12,6 +12,8 @@ ["[", "]"], ["(", ")"] ], + "colorizedBracketPairs": [ + ], "autoClosingPairs": [ { "open": "{", diff --git a/extensions/markdown-basics/snippets/markdown.code-snippets b/extensions/markdown-basics/snippets/markdown.code-snippets index f0753507b6..6c67eb86a8 100644 --- a/extensions/markdown-basics/snippets/markdown.code-snippets +++ b/extensions/markdown-basics/snippets/markdown.code-snippets @@ -21,7 +21,7 @@ }, "Insert fenced code block": { "prefix": "fenced codeblock", - "body": ["```${1:language}", "${TM_SELECTED_TEXT}$0", "```"], + "body": ["```${1|python,c,c++,c#,ruby,go,java,php,htm,css,javascript,json,markdown,console|}", "${TM_SELECTED_TEXT}$0", "```"], "description": "Insert fenced code block" }, "Insert heading level 1": { @@ -64,11 +64,6 @@ "body": ["1. ${1:first}", "2. ${2:second}", "3. ${3:third}", "$0"], "description": "Insert ordered list" }, - "Insert definition list": { - "prefix": "definition list", - "body": ["${1:term}", ": ${2:definition}", "$0"], - "description": "Insert definition list" - }, "Insert horizontal rule": { "prefix": "horizontal rule", "body": "----------\n", @@ -88,5 +83,5 @@ "prefix": "strikethrough", "body": "~~${1:${TM_SELECTED_TEXT}}~~", "description": "Insert strikethrough" - }, + }, } diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index 4200d7193a..e8a34e4394 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -55,8 +55,8 @@ body.showEditorSelection .code-line { position: relative; } -body.showEditorSelection .code-active-line:before, -body.showEditorSelection .code-line:hover:before { +body.showEditorSelection :not(tr).code-active-line:before, +body.showEditorSelection :not(tr).code-line:hover:before { content: ""; display: block; position: absolute; diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index 33cc93cb6c..611338f568 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -3,22 +3,22 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const MarkdownIt = require('markdown-it'); +const MarkdownIt: typeof import('markdown-it') = require('markdown-it'); import * as DOMPurify from 'dompurify'; -import type * as markdownIt from 'markdown-it'; +import type * as MarkdownItToken from 'markdown-it/lib/token'; +import type { ActivationFunction } from 'vscode-notebook-renderer'; const sanitizerOptions: DOMPurify.Config = { ALLOWED_TAGS: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], }; -export function activate(ctx: { workspace: { isTrusted: boolean } }) { +export const activate: ActivationFunction = (ctx) => { let markdownIt = new MarkdownIt({ html: true }); addNamedHeaderRendering(markdownIt); const style = document.createElement('style'); - style.classList.add('markdown-style'); style.textContent = ` .emptyMarkdownCell::before { content: "${document.documentElement.style.getPropertyValue('--notebook-cell-markup-empty-content')}"; @@ -54,16 +54,19 @@ export function activate(ctx: { workspace: { isTrusted: boolean } }) { } h1 { - font-size: 26px; - line-height: 31px; - margin: 0; - margin-bottom: 13px; + font-size: 2.25em; } h2 { - font-size: 19px; - margin: 0; - margin-bottom: 10px; + font-size: 1.9em; + } + + h3 { + font-size: 1.6em; + } + + p { + font-size: 1.1em; } h1, @@ -141,10 +144,13 @@ export function activate(ctx: { workspace: { isTrusted: boolean } }) { white-space: pre-wrap; } `; - document.head.append(style); + const template = document.createElement('template'); + template.classList.add('markdown-style'); + template.content.appendChild(style); + document.head.appendChild(template); return { - renderOutputItem: (outputInfo: { text(): string }, element: HTMLElement) => { + renderOutputItem: (outputInfo, element) => { let previewNode: HTMLElement; if (!element.shadowRoot) { const previewRoot = element.attachShadow({ mode: 'open' }); @@ -155,15 +161,19 @@ export function activate(ctx: { workspace: { isTrusted: boolean } }) { previewRoot.appendChild(defaultStyles.cloneNode(true)); // And then contributed styles - for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) { - previewRoot.appendChild(markdownStyleNode.cloneNode(true)); + for (const element of document.getElementsByClassName('markdown-style')) { + if (element instanceof HTMLTemplateElement) { + previewRoot.appendChild(element.content.cloneNode(true)); + } else { + previewRoot.appendChild(element.cloneNode(true)); + } } previewNode = document.createElement('div'); previewNode.id = 'preview'; previewRoot.appendChild(previewNode); } else { - previewNode = element.shadowRoot.getElementById('preview')! as HTMLElement; // {{SQL CARBON EDIT}} Cast to fix compilation error + previewNode = element.shadowRoot.getElementById('preview')!; } const text = outputInfo.text(); @@ -174,24 +184,24 @@ export function activate(ctx: { workspace: { isTrusted: boolean } }) { previewNode.classList.remove('emptyMarkdownCell'); const unsanitizedRenderedMarkdown = markdownIt.render(text); - previewNode.innerHTML = ctx.workspace.isTrusted + previewNode.innerHTML = (ctx.workspace.isTrusted ? unsanitizedRenderedMarkdown - : DOMPurify.sanitize(unsanitizedRenderedMarkdown, sanitizerOptions); + : DOMPurify.sanitize(unsanitizedRenderedMarkdown, sanitizerOptions)); } }, extendMarkdownIt: (f: (md: typeof markdownIt) => void) => { f(markdownIt); } }; -} +}; -function addNamedHeaderRendering(md: markdownIt.MarkdownIt): void { +function addNamedHeaderRendering(md: InstanceType): void { const slugCounter = new Map(); const originalHeaderOpen = md.renderer.rules.heading_open; - md.renderer.rules.heading_open = (tokens: markdownIt.Token[], idx: number, options: any, env: any, self: any) => { - const title = tokens[idx + 1].children.reduce((acc: string, t: any) => acc + t.content, ''); + md.renderer.rules.heading_open = (tokens: MarkdownItToken[], idx: number, options, env, self) => { + const title = tokens[idx + 1].children!.reduce((acc, t) => acc + t.content, ''); let slug = slugFromHeading(title); if (slugCounter.has(slug)) { @@ -202,13 +212,12 @@ function addNamedHeaderRendering(md: markdownIt.MarkdownIt): void { slugCounter.set(slug, 0); } - tokens[idx].attrs = tokens[idx].attrs || []; - tokens[idx].attrs.push(['id', slug]); + tokens[idx].attrSet('id', slug); if (originalHeaderOpen) { return originalHeaderOpen(tokens, idx, options, env, self); } else { - return self.renderToken(tokens, idx, options, env, self); + return self.renderToken(tokens, idx, options); } }; diff --git a/extensions/markdown-language-features/notebook/tsconfig.json b/extensions/markdown-language-features/notebook/tsconfig.json index 2d704f32de..b90051ec35 100644 --- a/extensions/markdown-language-features/notebook/tsconfig.json +++ b/extensions/markdown-language-features/notebook/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "./dist/", "jsx": "react", + "moduleResolution": "Node", "module": "es2020", "lib": [ "es2018", diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index f2ed79f7d6..c74ed11305 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -356,14 +356,14 @@ "highlight.js": "^10.4.1", "markdown-it": "^12.3.2", "markdown-it-front-matter": "^0.2.1", - "vscode-extension-telemetry": "0.2.8", + "vscode-extension-telemetry": "0.4.2", "vscode-nls": "^5.0.0" }, "devDependencies": { - "@types/dompurify": "^2.2.3", - "@types/highlight.js": "10.1.0", + "@types/dompurify": "^2.3.1", "@types/lodash.throttle": "^4.1.3", - "@types/markdown-it": "0.0.2", + "@types/markdown-it": "12.2.3", + "@types/vscode-notebook-renderer": "^1.60.0", "@types/vscode-webview": "^1.57.0", "lodash.throttle": "^4.1.1" }, diff --git a/extensions/markdown-language-features/src/commands/openDocumentLink.ts b/extensions/markdown-language-features/src/commands/openDocumentLink.ts index 32b29ba427..9445fdbf43 100644 --- a/extensions/markdown-language-features/src/commands/openDocumentLink.ts +++ b/extensions/markdown-language-features/src/commands/openDocumentLink.ts @@ -6,10 +6,7 @@ import * as vscode from 'vscode'; import { Command } from '../commandManager'; import { MarkdownEngine } from '../markdownEngine'; -import { TableOfContentsProvider } from '../tableOfContentsProvider'; -import { isMarkdownFile } from '../util/file'; -import { extname } from '../util/path'; - +import { openDocumentLink } from '../util/openDocumentLink'; type UriComponents = { readonly scheme?: string; @@ -25,11 +22,6 @@ export interface OpenDocumentLinkArgs { readonly fromResource: UriComponents; } -enum OpenMarkdownLinks { - beside = 'beside', - currentGroup = 'currentGroup', -} - export class OpenDocumentLinkCommand implements Command { private static readonly id = '_markdown.openDocumentLink'; public readonly id = OpenDocumentLinkCommand.id; @@ -60,95 +52,9 @@ export class OpenDocumentLinkCommand implements Command { ) { } public async execute(args: OpenDocumentLinkArgs) { - return OpenDocumentLinkCommand.execute(this.engine, args); - } - - public static async execute(engine: MarkdownEngine, args: OpenDocumentLinkArgs): Promise { const fromResource = vscode.Uri.parse('').with(args.fromResource); - - const targetResource = reviveUri(args.parts); - - const column = this.getViewColumn(fromResource); - - const didOpen = await this.tryOpen(engine, targetResource, args, column); - if (didOpen) { - return; - } - - if (extname(targetResource.path) === '') { - await this.tryOpen(engine, targetResource.with({ path: targetResource.path + '.md' }), args, column); - return; - } - } - - private static async tryOpen(engine: MarkdownEngine, resource: vscode.Uri, args: OpenDocumentLinkArgs, column: vscode.ViewColumn): Promise { - const tryUpdateForActiveFile = async (): Promise => { - if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) { - if (vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) { - await this.tryRevealLine(engine, vscode.window.activeTextEditor, args.fragment); - return true; - } - } - return false; - }; - - if (await tryUpdateForActiveFile()) { - return true; - } - - let stat: vscode.FileStat; - try { - stat = await vscode.workspace.fs.stat(resource); - if (stat.type === vscode.FileType.Directory) { - await vscode.commands.executeCommand('revealInExplorer', resource); - return true; - } - } catch { - // noop - // If resource doesn't exist, execute `vscode.open` either way so an error - // notification is shown to the user with a create file action #113475 - } - - try { - await vscode.commands.executeCommand('vscode.open', resource, column); - } catch { - return false; - } - - return tryUpdateForActiveFile(); - } - - private static getViewColumn(resource: vscode.Uri): vscode.ViewColumn { - const config = vscode.workspace.getConfiguration('markdown', resource); - const openLinks = config.get('links.openLocation', OpenMarkdownLinks.currentGroup); - switch (openLinks) { - case OpenMarkdownLinks.beside: - return vscode.ViewColumn.Beside; - case OpenMarkdownLinks.currentGroup: - default: - return vscode.ViewColumn.Active; - } - } - - private static async tryRevealLine(engine: MarkdownEngine, editor: vscode.TextEditor, fragment?: string) { - if (fragment) { - const toc = new TableOfContentsProvider(engine, editor.document); - const entry = await toc.lookup(fragment); - if (entry) { - const lineStart = new vscode.Range(entry.line, 0, entry.line, 0); - editor.selection = new vscode.Selection(lineStart.start, lineStart.end); - return editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop); - } - const lineNumberFragment = fragment.match(/^L(\d+)$/i); - if (lineNumberFragment) { - const line = +lineNumberFragment[1] - 1; - if (!isNaN(line)) { - const lineStart = new vscode.Range(line, 0, line, 0); - editor.selection = new vscode.Selection(lineStart.start, lineStart.end); - return editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop); - } - } - } + const targetResource = reviveUri(args.parts).with({ fragment: args.fragment }); + return openDocumentLink(this.engine, targetResource, fromResource); } } @@ -158,36 +64,3 @@ function reviveUri(parts: any) { } return vscode.Uri.parse('').with(parts); } - -export async function resolveLinkToMarkdownFile(path: string): Promise { - try { - const standardLink = await tryResolveLinkToMarkdownFile(path); - if (standardLink) { - return standardLink; - } - } catch { - // Noop - } - - // If no extension, try with `.md` extension - if (extname(path) === '') { - return tryResolveLinkToMarkdownFile(path + '.md'); - } - - return undefined; -} - -async function tryResolveLinkToMarkdownFile(path: string): Promise { - const resource = vscode.Uri.file(path); - - let document: vscode.TextDocument; - try { - document = await vscode.workspace.openTextDocument(resource); - } catch { - return undefined; - } - if (isMarkdownFile(document)) { - return document.uri; - } - return undefined; -} diff --git a/extensions/markdown-language-features/src/commands/showPreview.ts b/extensions/markdown-language-features/src/commands/showPreview.ts index 25798360bb..63c80e4f7f 100644 --- a/extensions/markdown-language-features/src/commands/showPreview.ts +++ b/extensions/markdown-language-features/src/commands/showPreview.ts @@ -40,7 +40,7 @@ async function showPreview( const resourceColumn = (vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One; webviewManager.openDynamicPreview(resource, { resourceColumn: resourceColumn, - previewColumn: previewSettings.sideBySide ? resourceColumn + 1 : resourceColumn, + previewColumn: previewSettings.sideBySide ? vscode.ViewColumn.Beside : resourceColumn, locked: !!previewSettings.locked }); diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 1b12b24164..f5c32bcb38 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -83,3 +83,4 @@ function registerMarkdownCommands( commandManager.register(new commands.ReloadPlugins(previewManager, engine)); return commandManager; } + diff --git a/extensions/markdown-language-features/src/features/documentLinkProvider.ts b/extensions/markdown-language-features/src/features/documentLinkProvider.ts index 51d65e1db2..455b9b893c 100644 --- a/extensions/markdown-language-features/src/features/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/features/documentLinkProvider.ts @@ -15,7 +15,9 @@ function parseLink( document: vscode.TextDocument, link: string, ): { uri: vscode.Uri, tooltip?: string } | undefined { - const externalSchemeUri = getUriForLinkWithKnownExternalScheme(link); + + const cleanLink = stripAngleBrackets(link); + const externalSchemeUri = getUriForLinkWithKnownExternalScheme(cleanLink); if (externalSchemeUri) { // Normalize VS Code links to target currently running version if (isOfScheme(Schemes.vscode, link) || isOfScheme(Schemes['vscode-insiders'], link)) { @@ -89,6 +91,15 @@ function extractDocumentLink( } } +/* Used to strip brackets from the markdown link + will be transformed to + http://example.com +*/ +export function stripAngleBrackets(link: string) { + const bracketMatcher = /^<(.*)>$/; + return link.replace(bracketMatcher, '$1'); +} + export default class LinkProvider implements vscode.DocumentLinkProvider { private readonly linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g; private readonly referenceLinkPattern = /(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]/g; diff --git a/extensions/markdown-language-features/src/features/foldingProvider.ts b/extensions/markdown-language-features/src/features/foldingProvider.ts index 1b305bf8a3..7a5aa9fe90 100644 --- a/extensions/markdown-language-features/src/features/foldingProvider.ts +++ b/extensions/markdown-language-features/src/features/foldingProvider.ts @@ -3,13 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Token } from 'markdown-it'; +import Token = require('markdown-it/lib/token'); import * as vscode from 'vscode'; import { MarkdownEngine } from '../markdownEngine'; import { TableOfContentsProvider } from '../tableOfContentsProvider'; const rangeLimit = 5000; +interface MarkdownItTokenWithMap extends Token { + map: [number, number]; +} + export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvider { constructor( @@ -84,10 +88,14 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi const isStartRegion = (t: string) => /^\s*/.test(t); const isEndRegion = (t: string) => /^\s*/.test(t); -const isRegionMarker = (token: Token) => - token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content)); +const isRegionMarker = (token: Token): token is MarkdownItTokenWithMap => + !!token.map && token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content)); + +const isFoldableToken = (token: Token): token is MarkdownItTokenWithMap => { + if (!token.map) { + return false; + } -const isFoldableToken = (token: Token): boolean => { switch (token.type) { case 'fence': case 'list_item_open': diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index f8d715c869..15bdf56b90 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -5,12 +5,12 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { OpenDocumentLinkCommand, resolveLinkToMarkdownFile } from '../commands/openDocumentLink'; import { Logger } from '../logger'; import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; +import { openDocumentLink, resolveDocumentLink, resolveLinkToMarkdownFile } from '../util/openDocumentLink'; import * as path from '../util/path'; import { WebviewResourceProvider } from '../util/resources'; import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; @@ -355,7 +355,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } } - vscode.workspace.openTextDocument(this._resource) + await vscode.workspace.openTextDocument(this._resource) .then(vscode.window.showTextDocument) .then(undefined, () => { vscode.window.showErrorMessage(localize('preview.clickOpenFailed', 'Could not open {0}', this._resource.toString())); @@ -406,6 +406,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private getWebviewOptions(): vscode.WebviewOptions { return { enableScripts: true, + enableForms: false, localResourceRoots: this.getLocalResourceRoots() }; } @@ -428,29 +429,19 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private async onDidClickPreviewLink(href: string) { - let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c)); - - if (hrefPath[0] !== '/') { - // We perviously already resolve absolute paths. - // Now make sure we handle relative file paths - const dirnameUri = vscode.Uri.parse(path.dirname(this.resource.path)); - hrefPath = vscode.Uri.joinPath(dirnameUri, hrefPath).path; - } else { - // Handle any normalized file paths - hrefPath = vscode.Uri.parse(hrefPath.replace('/file', '')).path; - } + const targetResource = resolveDocumentLink(href, this.resource); const config = vscode.workspace.getConfiguration('markdown', this.resource); const openLinks = config.get('preview.openMarkdownLinks', 'inPreview'); if (openLinks === 'inPreview') { - const markdownLink = await resolveLinkToMarkdownFile(hrefPath); + const markdownLink = await resolveLinkToMarkdownFile(targetResource); if (markdownLink) { - this.delegate.openPreviewLinkToMarkdownFile(markdownLink, fragment); + this.delegate.openPreviewLinkToMarkdownFile(markdownLink, targetResource.fragment); return; } } - OpenDocumentLinkCommand.execute(this.engine, { parts: { path: hrefPath }, fragment, fromResource: this.resource.toJSON() }); + return openDocumentLink(this.engine, targetResource, this.resource); } //#region WebviewResourceProvider @@ -531,7 +522,7 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown })); this._register(this.preview.onScroll((scrollInfo) => { - topmostLineMonitor.setPreviousEditorLine(scrollInfo); + topmostLineMonitor.setPreviousStaticEditorLine(scrollInfo); })); this._register(topmostLineMonitor.onDidChanged(event => { diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index b4883178c2..da6044bd40 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -81,7 +81,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview // When at a markdown file, apply existing scroll settings if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) { - const line = this._topmostLineMonitor.getPreviousEditorLineByUri(textEditor.document.uri); + const line = this._topmostLineMonitor.getPreviousStaticEditorLineByUri(textEditor.document.uri); if (line) { scrollEditorToLine(line, textEditor); } @@ -172,7 +172,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview document: vscode.TextDocument, webview: vscode.WebviewPanel ): Promise { - const lineNumber = this._topmostLineMonitor.getPreviousEditorLineByUri(document.uri); + const lineNumber = this._topmostLineMonitor.getPreviousTextEditorLineByUri(document.uri); const preview = StaticMarkdownPreview.revive( document.uri, webview, diff --git a/extensions/markdown-language-features/src/features/smartSelect.ts b/extensions/markdown-language-features/src/features/smartSelect.ts index 0ad3e90f60..27fa662ac9 100644 --- a/extensions/markdown-language-features/src/features/smartSelect.ts +++ b/extensions/markdown-language-features/src/features/smartSelect.ts @@ -2,11 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Token } from 'markdown-it'; +import Token = require('markdown-it/lib/token'); import * as vscode from 'vscode'; import { MarkdownEngine } from '../markdownEngine'; import { TableOfContentsProvider, TocEntry } from '../tableOfContentsProvider'; +interface MarkdownItTokenWithMap extends Token { + map: [number, number]; +} + export default class MarkdownSmartSelect implements vscode.SelectionRangeProvider { constructor( @@ -96,8 +100,8 @@ function createHeaderRange(header: TocEntry, isClosestHeaderToPosition: boolean, } } -function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, parent?: vscode.SelectionRange): Token[] { - const enclosingTokens = tokens.filter(token => token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token)); +function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, parent?: vscode.SelectionRange): MarkdownItTokenWithMap[] { + const enclosingTokens = tokens.filter((token): token is MarkdownItTokenWithMap => !!token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token)); if (enclosingTokens.length === 0) { return []; } @@ -105,7 +109,7 @@ function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, p return sortedTokens; } -function createBlockRange(block: Token, document: vscode.TextDocument, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined { +function createBlockRange(block: MarkdownItTokenWithMap, document: vscode.TextDocument, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined { if (block.type === 'fence') { return createFencedRange(block, cursorLine, document, parent); } else { @@ -144,7 +148,7 @@ function createInlineRange(document: vscode.TextDocument, cursorPosition: vscode return inlineCodeBlockSelection || linkSelection || comboSelection || boldSelection || italicSelection; } -function createFencedRange(token: Token, cursorLine: number, document: vscode.TextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange { +function createFencedRange(token: MarkdownItTokenWithMap, cursorLine: number, document: vscode.TextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange { const startLine = token.map[0]; const endLine = token.map[1] - 1; const onFenceLine = cursorLine === startLine || cursorLine === endLine; diff --git a/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts b/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts index 8ac6cdde0f..77dee4c171 100644 --- a/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts +++ b/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts @@ -41,7 +41,7 @@ class VSCodeWorkspaceMarkdownDocumentProvider extends Disposable implements Work for (let i = 0; i < resources.length; i += maxConcurrent) { const resourceBatch = resources.slice(i, i + maxConcurrent); - const documentBatch = (await Promise.all(resourceBatch.map(this.getMarkdownDocument))).filter((doc) => !!doc) as SkinnyTextDocument[]; + const documentBatch = (await Promise.all(resourceBatch.map(x => this.getMarkdownDocument(x)))).filter((doc) => !!doc) as SkinnyTextDocument[]; docList.push(...documentBatch); } return docList; diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index e491392f8a..3c43820651 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MarkdownIt, Token } from 'markdown-it'; +import MarkdownIt = require('markdown-it'); +import Token = require('markdown-it/lib/token'); import * as vscode from 'vscode'; import { MarkdownContributionProvider as MarkdownContributionProvider } from './markdownExtensions'; import { Slugifier } from './slugify'; @@ -14,11 +15,34 @@ import { WebviewResourceProvider } from './util/resources'; const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g; -interface MarkdownItConfig { - readonly breaks: boolean; - readonly linkify: boolean; - readonly typographer: boolean; -} +/** + * Adds begin line index to the output via the 'data-line' data attribute. + */ +const pluginSourceMap: MarkdownIt.PluginSimple = (md): void => { + // Set the attribute on every possible token. + md.core.ruler.push('source_map_data_attribute', (state): void => { + for (const token of state.tokens) { + if (token.map && token.type !== 'inline') { + token.attrSet('data-line', String(token.map[0])); + token.attrJoin('class', 'code-line'); + } + } + }); + + // The 'html_block' renderer doesn't respect `attrs`. We need to insert a marker. + const originalHtmlBlockRenderer = md.renderer.rules['html_block']; + if (originalHtmlBlockRenderer) { + md.renderer.rules['html_block'] = (tokens, idx, options, env, self) => ( + `
\n` + + originalHtmlBlockRenderer(tokens, idx, options, env, self) + ); + } +}; + +/** + * The markdown-it options that we expose in the settings. + */ +type MarkdownItConfig = Readonly>>; class TokenCache { private cachedDocument?: { @@ -85,14 +109,15 @@ export class MarkdownEngine { private async getEngine(config: MarkdownItConfig): Promise { if (!this.md) { - this.md = import('markdown-it').then(async markdownIt => { + this.md = (async () => { + const markdownIt = await import('markdown-it'); let md: MarkdownIt = markdownIt(await getMarkdownOptions(() => md)); for (const plugin of this.contributionProvider.contributions.markdownItPlugins.values()) { try { md = (await plugin)(md); - } catch { - // noop + } catch (e) { + console.error('Could not load markdown it plugin', e); } } @@ -111,18 +136,15 @@ export class MarkdownEngine { alt: ['paragraph', 'reference', 'blockquote', 'list'] }); - for (const renderName of ['paragraph_open', 'heading_open', 'image', 'code_block', 'fence', 'blockquote_open', 'list_item_open']) { - this.addLineNumberRenderer(md, renderName); - } - this.addImageRenderer(md); this.addFencedRenderer(md); this.addLinkNormalizer(md); this.addLinkValidator(md); this.addNamedHeaders(md); this.addLinkRenderer(md); + md.use(pluginSourceMap); return md; - }); + })(); } const md = await this.md!; @@ -170,7 +192,7 @@ export class MarkdownEngine { }; const html = engine.renderer.render(tokens, { - ...(engine as any).options, + ...engine.options, ...config }, env); @@ -199,26 +221,9 @@ export class MarkdownEngine { }; } - private addLineNumberRenderer(md: MarkdownIt, ruleName: string): void { - const original = md.renderer.rules[ruleName]; - 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.attrJoin('class', 'code-line'); - } - - if (original) { - return original(tokens, idx, options, env, self); - } else { - return self.renderToken(tokens, idx, options, env, self); - } - }; - } - private addImageRenderer(md: MarkdownIt): void { const original = md.renderer.rules.image; - md.renderer.rules.image = (tokens: Token[], idx: number, options: any, env: RenderEnv, self: any) => { + md.renderer.rules.image = (tokens: Token[], idx: number, options, env: RenderEnv, self) => { const token = tokens[idx]; token.attrJoin('class', 'loading'); @@ -237,20 +242,24 @@ export class MarkdownEngine { if (original) { return original(tokens, idx, options, env, self); } else { - return self.renderToken(tokens, idx, options, env, self); + return self.renderToken(tokens, idx, options); } }; } private addFencedRenderer(md: MarkdownIt): void { const original = md.renderer.rules['fenced']; - md.renderer.rules['fenced'] = (tokens: Token[], idx: number, options: any, env: any, self: any) => { + md.renderer.rules['fenced'] = (tokens: Token[], idx: number, options, env, self) => { const token = tokens[idx]; if (token.map && token.map.length) { token.attrJoin('class', 'hljs'); } - return original(tokens, idx, options, env, self); + if (original) { + return original(tokens, idx, options, env, self); + } else { + return self.renderToken(tokens, idx, options); + } }; } @@ -282,8 +291,8 @@ export class MarkdownEngine { private addNamedHeaders(md: MarkdownIt): void { const original = md.renderer.rules.heading_open; - 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, ''); + md.renderer.rules.heading_open = (tokens: Token[], idx: number, options, env, self) => { + const title = tokens[idx + 1].children!.reduce((acc, t) => acc + t.content, ''); let slug = this.slugifier.fromHeading(title); if (this._slugCount.has(slug.value)) { @@ -294,30 +303,31 @@ export class MarkdownEngine { this._slugCount.set(slug.value, 0); } - tokens[idx].attrs = tokens[idx].attrs || []; - tokens[idx].attrs.push(['id', slug.value]); + tokens[idx].attrSet('id', slug.value); if (original) { return original(tokens, idx, options, env, self); } else { - return self.renderToken(tokens, idx, options, env, self); + return self.renderToken(tokens, idx, options); } }; } 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); - }); + const original = md.renderer.rules.link_open; - md.renderer.rules.link_open = (tokens: Token[], idx: number, options: any, env: any, self: any) => { + md.renderer.rules.link_open = (tokens: Token[], idx: number, options, env, self) => { const token = tokens[idx]; - const hrefIndex = token.attrIndex('href'); - if (hrefIndex >= 0) { - const href = token.attrs[hrefIndex][1]; - token.attrPush(['data-href', href]); + const href = token.attrGet('href'); + // A string, including empty string, may be `href`. + if (typeof href === 'string') { + token.attrSet('data-href', href); + } + if (original) { + return original(tokens, idx, options, env, self); + } else { + return self.renderToken(tokens, idx, options); } - return old_render(tokens, idx, options, env, self); }; } @@ -366,7 +376,7 @@ export class MarkdownEngine { } } -async function getMarkdownOptions(md: () => MarkdownIt) { +async function getMarkdownOptions(md: () => MarkdownIt): Promise { const hljs = await import('highlight.js'); return { html: true, diff --git a/extensions/markdown-language-features/src/tableOfContentsProvider.ts b/extensions/markdown-language-features/src/tableOfContentsProvider.ts index 85c68fddf1..1374a012ca 100644 --- a/extensions/markdown-language-features/src/tableOfContentsProvider.ts +++ b/extensions/markdown-language-features/src/tableOfContentsProvider.ts @@ -60,6 +60,10 @@ export class TableOfContentsProvider { const existingSlugEntries = new Map(); for (const heading of tokens.filter(token => token.type === 'heading_open')) { + if (!heading.map) { + continue; + } + const lineNumber = heading.map[0]; const line = document.lineAt(lineNumber); diff --git a/extensions/markdown-language-features/src/test/documentLink.test.ts b/extensions/markdown-language-features/src/test/documentLink.test.ts index 57e011c85d..46bc554c45 100644 --- a/extensions/markdown-language-features/src/test/documentLink.test.ts +++ b/extensions/markdown-language-features/src/test/documentLink.test.ts @@ -94,6 +94,17 @@ suite('Markdown Document links', () => { assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1); }); + + test('Should navigate to line number within non-md file', async () => { + await withFileContents(testFileA, '[b](sub/foo.txt#L3)'); + + const [link] = await getLinksForFile(testFileA); + await executeLink(link); + + assertActiveDocumentUri(workspaceFile('sub', 'foo.txt')); + assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 2); + }); + test('Should navigate to fragment within current file', async () => { await withFileContents(testFileA, joinLines( '[](a#header)', diff --git a/extensions/markdown-language-features/src/test/engine.test.ts b/extensions/markdown-language-features/src/test/engine.test.ts index 8fd5338ada..035e75a846 100644 --- a/extensions/markdown-language-features/src/test/engine.test.ts +++ b/extensions/markdown-language-features/src/test/engine.test.ts @@ -15,7 +15,7 @@ const testFileName = vscode.Uri.file('test.md'); suite('markdown.engine', () => { suite('rendering', () => { const input = '# hello\n\nworld!'; - const output = '

hello

\n' + const output = '

hello

\n' + '

world!

\n'; test('Renders a document', async () => { diff --git a/extensions/markdown-language-features/src/util/openDocumentLink.ts b/extensions/markdown-language-features/src/util/openDocumentLink.ts new file mode 100644 index 0000000000..dc07ea4e37 --- /dev/null +++ b/extensions/markdown-language-features/src/util/openDocumentLink.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { MarkdownEngine } from '../markdownEngine'; +import { TableOfContentsProvider } from '../tableOfContentsProvider'; +import { isMarkdownFile } from './file'; +import { extname } from './path'; + +export interface OpenDocumentLinkArgs { + readonly parts: vscode.Uri; + readonly fragment: string; + readonly fromResource: vscode.Uri; +} + +enum OpenMarkdownLinks { + beside = 'beside', + currentGroup = 'currentGroup', +} + +export function resolveDocumentLink(href: string, markdownFile: vscode.Uri): vscode.Uri { + let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c)); + + if (hrefPath[0] === '/') { + // Absolute path. Try to resolve relative to the workspace + const workspace = vscode.workspace.getWorkspaceFolder(markdownFile); + if (workspace) { + return vscode.Uri.joinPath(workspace.uri, hrefPath.slice(1)).with({ fragment }); + } + } + + // Relative path. Resolve relative to the md file + const dirnameUri = markdownFile.with({ path: path.dirname(markdownFile.path) }); + return vscode.Uri.joinPath(dirnameUri, hrefPath).with({ fragment }); +} + +export async function openDocumentLink(engine: MarkdownEngine, targetResource: vscode.Uri, fromResource: vscode.Uri): Promise { + const column = getViewColumn(fromResource); + + if (await tryNavigateToFragmentInActiveEditor(engine, targetResource)) { + return; + } + + let targetResourceStat: vscode.FileStat | undefined; + try { + targetResourceStat = await vscode.workspace.fs.stat(targetResource); + } catch { + // noop + } + + if (typeof targetResourceStat === 'undefined') { + // We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead + if (extname(targetResource.path) === '') { + const dotMdResource = targetResource.with({ path: targetResource.path + '.md' }); + try { + const stat = await vscode.workspace.fs.stat(dotMdResource); + if (stat.type === vscode.FileType.File) { + await tryOpenMdFile(engine, dotMdResource, column); + return; + } + } catch { + // noop + } + } + } else if (targetResourceStat.type === vscode.FileType.Directory) { + return vscode.commands.executeCommand('revealInExplorer', targetResource); + } + + await tryOpenMdFile(engine, targetResource, column); +} + +async function tryOpenMdFile(engine: MarkdownEngine, resource: vscode.Uri, column: vscode.ViewColumn): Promise { + await vscode.commands.executeCommand('vscode.open', resource.with({ fragment: '' }), column); + return tryNavigateToFragmentInActiveEditor(engine, resource); +} + +async function tryNavigateToFragmentInActiveEditor(engine: MarkdownEngine, resource: vscode.Uri): Promise { + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor?.document.uri.fsPath === resource.fsPath) { + if (isMarkdownFile(activeEditor.document)) { + if (await tryRevealLineUsingTocFragment(engine, activeEditor, resource.fragment)) { + return true; + } + } + tryRevealLineUsingLineFragment(activeEditor, resource.fragment); + return true; + } + return false; +} + +function getViewColumn(resource: vscode.Uri): vscode.ViewColumn { + const config = vscode.workspace.getConfiguration('markdown', resource); + const openLinks = config.get('links.openLocation', OpenMarkdownLinks.currentGroup); + switch (openLinks) { + case OpenMarkdownLinks.beside: + return vscode.ViewColumn.Beside; + case OpenMarkdownLinks.currentGroup: + default: + return vscode.ViewColumn.Active; + } +} + +async function tryRevealLineUsingTocFragment(engine: MarkdownEngine, editor: vscode.TextEditor, fragment: string): Promise { + const toc = new TableOfContentsProvider(engine, editor.document); + const entry = await toc.lookup(fragment); + if (entry) { + const lineStart = new vscode.Range(entry.line, 0, entry.line, 0); + editor.selection = new vscode.Selection(lineStart.start, lineStart.end); + editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop); + return true; + } + return false; +} + +function tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: string): boolean { + const lineNumberFragment = fragment.match(/^L(\d+)$/i); + if (lineNumberFragment) { + const line = +lineNumberFragment[1] - 1; + if (!isNaN(line)) { + const lineStart = new vscode.Range(line, 0, line, 0); + editor.selection = new vscode.Selection(lineStart.start, lineStart.end); + editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop); + return true; + } + } + return false; +} + +export async function resolveLinkToMarkdownFile(resource: vscode.Uri): Promise { + try { + const standardLink = await tryResolveLinkToMarkdownFile(resource); + if (standardLink) { + return standardLink; + } + } catch { + // Noop + } + + // If no extension, try with `.md` extension + if (extname(resource.path) === '') { + return tryResolveLinkToMarkdownFile(resource.with({ path: resource.path + '.md' })); + } + + return undefined; +} + +async function tryResolveLinkToMarkdownFile(resource: vscode.Uri): Promise { + let document: vscode.TextDocument; + try { + document = await vscode.workspace.openTextDocument(resource); + } catch { + return undefined; + } + if (isMarkdownFile(document)) { + return document.uri; + } + return undefined; +} diff --git a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index abba94ddc6..97086ea2ac 100644 --- a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -16,15 +16,15 @@ export class TopmostLineMonitor extends Disposable { private readonly pendingUpdates = new Map(); private readonly throttle = 50; - private previousEditorInfo = new Map(); - public isPrevEditorCustom = false; + private previousTextEditorInfo = new Map(); + private previousStaticEditorInfo = new Map(); constructor() { super(); if (vscode.window.activeTextEditor) { const line = getVisibleLine(vscode.window.activeTextEditor); - this.setPreviousEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 }); + this.setPreviousTextEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 }); } this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => { @@ -32,7 +32,7 @@ export class TopmostLineMonitor extends Disposable { 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 }); + this.setPreviousTextEditorLine({ uri: event.textEditor.document.uri, line: line }); } } })); @@ -41,12 +41,24 @@ 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; - public setPreviousEditorLine(scrollLocation: LastScrollLocation): void { - this.previousEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); + public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation): void { + this.previousStaticEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); } - public getPreviousEditorLineByUri(resource: vscode.Uri): number | undefined { - const scrollLoc = this.previousEditorInfo.get(resource.toString()); + public getPreviousStaticEditorLineByUri(resource: vscode.Uri): number | undefined { + const scrollLoc = this.previousStaticEditorInfo.get(resource.toString()); + this.previousStaticEditorInfo.delete(resource.toString()); + return scrollLoc?.line; + } + + + public setPreviousTextEditorLine(scrollLocation: LastScrollLocation): void { + this.previousTextEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); + } + + public getPreviousTextEditorLineByUri(resource: vscode.Uri): number | undefined { + const scrollLoc = this.previousTextEditorInfo.get(resource.toString()); + this.previousTextEditorInfo.delete(resource.toString()); return scrollLoc?.line; } diff --git a/extensions/markdown-language-features/test-workspace/sub/foo.txt b/extensions/markdown-language-features/test-workspace/sub/foo.txt new file mode 100644 index 0000000000..85954eabcf --- /dev/null +++ b/extensions/markdown-language-features/test-workspace/sub/foo.txt @@ -0,0 +1,5 @@ +1 +2 +3 +4 +5 \ No newline at end of file diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 41dd553713..56f4c68ebf 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -2,19 +2,17 @@ # yarn lockfile v1 -"@types/dompurify@^2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.2.3.tgz#6e89677a07902ac1b6821c345f34bd85da239b08" - integrity sha512-CLtc2mZK8+axmrz1JqtpklO/Kvn38arGc8o1l3UVopZaXXuer9ONdZwJ/9f226GrhRLtUmLr9WrvZsRSNpS8og== +"@types/dompurify@^2.3.1": + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.3.tgz#c24c92f698f77ed9cc9d9fa7888f90cf2bfaa23f" + integrity sha512-nnVQSgRVuZ/843oAfhA25eRSNzUFcBPk/LOiw5gm8mD9/X7CNcbRkQu/OsjCewO8+VIYfPxUnXvPEVGenw14+w== dependencies: "@types/trusted-types" "*" -"@types/highlight.js@10.1.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-10.1.0.tgz#89bb0c202997d7a90a07bd2ec1f7d00c56bb90b4" - integrity sha512-77hF2dGBsOgnvZll1vymYiNUtqJ8cJfXPD6GG/2M0aLRc29PkvB7Au6sIDjIEFcSICBhCh2+Pyq6WSRS7LUm6A== - dependencies: - highlight.js "*" +"@types/linkify-it@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" + integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== "@types/lodash.throttle@^4.1.3": version "4.1.3" @@ -28,16 +26,29 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" integrity sha512-ufQcVg4daO8xQ5kopxRHanqFdL4AI7ondQkV+2f+7mz3gvp0LkBx2zBRC6hfs3T87mzQFmf5Fck7Fi145Ul6NQ== -"@types/markdown-it@0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" - integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= +"@types/markdown-it@12.2.3": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" + +"@types/mdurl@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" + integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== "@types/trusted-types@*": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== +"@types/vscode-notebook-renderer@^1.60.0": + version "1.60.0" + resolved "https://registry.yarnpkg.com/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.60.0.tgz#8a67d561f48ddf46a95dfa9f712a79c72c7b8f7a" + integrity sha512-u7TD2uuEZTVuitx0iijOJdKI0JLiQP6PsSBSRy2XmHXUOXcp5p1S56NrjOEDoF+PIHd3NL3eO6KTRSf5nukDqQ== + "@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" @@ -58,7 +69,7 @@ entities@~2.1.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -highlight.js@*, highlight.js@^10.4.1: +highlight.js@^10.4.1: version "10.7.1" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.1.tgz#a8ec4152db24ea630c90927d6cae2a45f8ecb955" integrity sha512-S6G97tHGqJ/U8DsXcEdnACbirtbx58Bx9CzIVeYli8OuswCfYI/LsXH2EiGcoGio1KAC3x4mmUwulOllJ2ZyRA== @@ -101,10 +112,10 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376" integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg== -vscode-extension-telemetry@0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.2.8.tgz#670c625c44791237c040cee2cb9f567ca34784ac" - integrity sha512-Vf52im5qzORRD2K5Ryp8PXo31YXVcJAYRSDDZGegWlt0OATOd83DYabS1U/WIq9nR5g80UQKH3+BsenhpQHUaA== +vscode-extension-telemetry@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.2.tgz#6ef847a80c9cfc207eb15e3a254f235acebb65a5" + integrity sha512-y0f51mVoFxHIzULQNCC26TBFIKdEC7uckS3tFoK++OOOl8mU2LlOxgmbd52T/SXoXNg5aI7xqs+4V2ug5ITvKw== vscode-nls@^5.0.0: version "5.0.0" diff --git a/extensions/markdown-math/notebook/katex.ts b/extensions/markdown-math/notebook/katex.ts index f08bb8ee52..21dd6f8c64 100644 --- a/extensions/markdown-math/notebook/katex.ts +++ b/extensions/markdown-math/notebook/katex.ts @@ -3,13 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type * as markdownIt from 'markdown-it'; +import type { RendererContext } from 'vscode-notebook-renderer'; 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'); +export async function activate(ctx: RendererContext) { + const markdownItRenderer = (await ctx.getRenderer('markdownItRenderer')) as undefined | any; if (!markdownItRenderer) { throw new Error('Could not load markdownItRenderer'); } @@ -41,6 +40,9 @@ export async function activate(ctx: { const katex = require('@iktakahiro/markdown-it-katex'); markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { - return md.use(katex, { globalGroup: true }); + return md.use(katex, { + globalGroup: true, + enableBareBlocks: true, + }); }); } diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index e767cb1cef..62400fbd6e 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -60,7 +60,8 @@ "@iktakahiro/markdown-it-katex": "https://github.com/mjbvz/markdown-it-katex.git" }, "devDependencies": { - "@types/markdown-it": "^0.0.0" + "@types/markdown-it": "^0.0.0", + "@types/vscode-notebook-renderer": "^1.60.0" }, "repository": { "type": "git", diff --git a/extensions/markdown-math/yarn.lock b/extensions/markdown-math/yarn.lock index f402ca890b..afbc51221a 100644 --- a/extensions/markdown-math/yarn.lock +++ b/extensions/markdown-math/yarn.lock @@ -4,7 +4,7 @@ "@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" + resolved "https://github.com/mjbvz/markdown-it-katex.git#820d9025ad84937eb3f9f7efbc1be7595e20b19f" dependencies: katex "^0.13.0" @@ -13,6 +13,11 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.0.tgz#8f6acaa5e3245e275f684e95deb3e518d1c6ab16" integrity sha1-j2rKpeMkXidfaE6V3rPlGNHGqxY= +"@types/vscode-notebook-renderer@^1.60.0": + version "1.60.0" + resolved "https://registry.yarnpkg.com/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.60.0.tgz#8a67d561f48ddf46a95dfa9f712a79c72c7b8f7a" + integrity sha512-u7TD2uuEZTVuitx0iijOJdKI0JLiQP6PsSBSRy2XmHXUOXcp5p1S56NrjOEDoF+PIHd3NL3eO6KTRSf5nukDqQ== + commander@^6.0.0: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" diff --git a/extensions/microsoft-authentication/extension-browser.webpack.config.js b/extensions/microsoft-authentication/extension-browser.webpack.config.js index 4f362dc3d6..96b4444763 100644 --- a/extensions/microsoft-authentication/extension-browser.webpack.config.js +++ b/extensions/microsoft-authentication/extension-browser.webpack.config.js @@ -12,7 +12,11 @@ const withBrowserDefaults = require('../shared.webpack.config').browser; module.exports = withBrowserDefaults({ context: __dirname, - node: false, + node: { + global: true, + __filename: false, + __dirname: false, + }, entry: { extension: './src/extension.ts', }, @@ -23,11 +27,6 @@ module.exports = withBrowserDefaults({ alias: { './env/node': path.resolve(__dirname, 'src/env/browser'), './authServer': path.resolve(__dirname, 'src/env/browser/authServer'), - 'buffer': path.resolve(__dirname, 'node_modules/buffer/index.js'), - 'node-fetch': path.resolve(__dirname, 'node_modules/node-fetch/browser.js'), - 'randombytes': path.resolve(__dirname, 'node_modules/randombytes/browser.js'), - 'stream': path.resolve(__dirname, 'node_modules/stream/index.js'), - 'uuid': path.resolve(__dirname, 'node_modules/uuid/dist/esm-browser/index.js') } } }); diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 74a417655f..af6de0bdc7 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -54,11 +54,11 @@ "dependencies": { "buffer": "^5.6.0", "node-fetch": "^2.6.7", - "randombytes": "github:rmacfarlane/randombytes#b28d4ecee46262801ea09f15fa1f1513a05c5971", + "randombytes": "~2.1.0", "sha.js": "2.4.11", "stream": "0.0.2", "uuid": "^8.2.0", - "vscode-extension-telemetry": "0.2.8", + "vscode-extension-telemetry": "0.4.2", "vscode-nls": "^5.0.0" }, "repository": { diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 73f9375210..db59487803 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -100,7 +100,7 @@ export class AzureActiveDirectoryService { private _tokens: IToken[] = []; private _refreshTimeouts: Map = new Map(); private _uriHandler: UriEventHandler; - private _disposables: vscode.Disposable[] = []; + private _disposable: vscode.Disposable; // Used to keep track of current requests when not using the local server approach. private _pendingStates = new Map(); @@ -112,51 +112,59 @@ export class AzureActiveDirectoryService { constructor(private _context: vscode.ExtensionContext) { this._keychain = new Keychain(_context); this._uriHandler = new UriEventHandler(); - this._disposables.push(vscode.window.registerUriHandler(this._uriHandler)); + this._disposable = vscode.Disposable.from( + vscode.window.registerUriHandler(this._uriHandler), + this._context.secrets.onDidChange(() => this.checkForUpdates())); } public async initialize(): Promise { - const storedData = await this._keychain.getToken() || await this._keychain.tryMigrate(); - if (storedData) { - try { - const sessions = this.parseStoredData(storedData); - const refreshes = sessions.map(async session => { - if (!session.refreshToken) { - return Promise.resolve(); - } - - try { - await this.refreshToken(session.refreshToken, session.scope, session.id); - } catch (e) { - if (e.message === REFRESH_NETWORK_FAILURE) { - const didSucceedOnRetry = await this.handleRefreshNetworkError(session.id, session.refreshToken, session.scope); - if (!didSucceedOnRetry) { - this._tokens.push({ - accessToken: undefined, - refreshToken: session.refreshToken, - account: { - label: session.account.label ?? session.account.displayName!, - id: session.account.id - }, - scope: session.scope, - sessionId: session.id - }); - this.pollForReconnect(session.id, session.refreshToken, session.scope); - } - } else { - await this.removeSession(session.id); - } - } - }); - - await Promise.all(refreshes); - } catch (e) { - Logger.info('Failed to initialize stored data'); - await this.clearSessions(); - } + Logger.info('Reading sessions from keychain...'); + const storedData = await this._keychain.getToken(); + if (!storedData) { + Logger.info('No stored sessions found.'); + return; } + Logger.info('Got stored sessions!'); - this._disposables.push(this._context.secrets.onDidChange(() => this.checkForUpdates)); + try { + const sessions = this.parseStoredData(storedData); + const refreshes = sessions.map(async session => { + Logger.trace(`Read the following session from the keychain with the following scopes: ${session.scope}`); + if (!session.refreshToken) { + Logger.trace(`Session with the following scopes does not have a refresh token so we will not try to refresh it: ${session.scope}`); + return Promise.resolve(); + } + + try { + await this.refreshToken(session.refreshToken, session.scope, session.id); + } catch (e) { + // If we aren't connected to the internet, then wait and try to refresh again later. + if (e.message === REFRESH_NETWORK_FAILURE) { + const didSucceedOnRetry = await this.handleRefreshNetworkError(session.id, session.refreshToken, session.scope); + if (!didSucceedOnRetry) { + this._tokens.push({ + accessToken: undefined, + refreshToken: session.refreshToken, + account: { + label: session.account.label ?? session.account.displayName!, + id: session.account.id + }, + scope: session.scope, + sessionId: session.id + }); + this.pollForReconnect(session.id, session.refreshToken, session.scope); + } + } else { + await this.removeSession(session.id); + } + } + }); + + await Promise.all(refreshes); + } catch (e) { + Logger.error(`Failed to initialize stored data: ${e}`); + await this.clearSessions(); + } } private parseStoredData(data: string): IStoredSession[] { @@ -263,8 +271,8 @@ export class AzureActiveDirectoryService { private async resolveAccessAndIdTokens(token: IToken): Promise { if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { token.expiresAt - ? Logger.info(`Token available from cache, expires in ${token.expiresAt - Date.now()} milliseconds`) - : Logger.info('Token available from cache'); + ? Logger.info(`Token available from cache (for scopes ${token.scope}), expires in ${token.expiresAt - Date.now()} milliseconds`) + : Logger.info('Token available from cache (for scopes ${token.scope})'); return Promise.resolve({ accessToken: token.accessToken, idToken: token.idToken @@ -272,7 +280,7 @@ export class AzureActiveDirectoryService { } try { - Logger.info('Token expired or unavailable, trying refresh'); + Logger.info(`Token expired or unavailable (for scopes ${token.scope}), trying refresh`); const refreshedToken = await this.refreshToken(token.refreshToken, token.scope, token.sessionId); if (refreshedToken.accessToken) { return { @@ -301,98 +309,97 @@ export class AzureActiveDirectoryService { } async getSessions(scopes?: string[]): Promise { + Logger.info(`Getting sessions for ${scopes?.join(',') ?? 'all scopes'}...`); if (!scopes) { - return this.sessions; + const sessions = await this.sessions; + Logger.info(`Got ${sessions.length} sessions for all scopes...`); + return sessions; } const orderedScopes = scopes.sort().join(' '); const matchingTokens = this._tokens.filter(token => token.scope === orderedScopes); + Logger.info(`Got ${matchingTokens.length} sessions for ${scopes?.join(',')}...`); return Promise.all(matchingTokens.map(token => this.convertToSession(token))); } public async createSession(scope: string): Promise { - Logger.info('Logging in...'); + Logger.info(`Logging in for the following scopes: ${scope}`); if (!scope.includes('offline_access')) { Logger.info('Warning: The \'offline_access\' scope was not included, so the generated token will not be able to be refreshed.'); } - return new Promise(async (resolve, reject) => { - const runsRemote = vscode.env.remoteName !== undefined; - const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; + const runsRemote = vscode.env.remoteName !== undefined; + const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; + if (runsRemote || runsServerless) { + return this.loginWithoutLocalServer(scope); + } - if (runsRemote || runsServerless) { - resolve(this.loginWithoutLocalServer(scope)); - return; + const nonce = randomBytes(16).toString('base64'); + const { server, redirectPromise, codePromise } = createServer(nonce); + + let token: IToken | undefined; + try { + const port = await startServer(server); + vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`)); + + const redirectReq = await redirectPromise; + if ('err' in redirectReq) { + const { err, res } = redirectReq; + res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); + res.end(); + throw err; } - const nonce = randomBytes(16).toString('base64'); - const { server, redirectPromise, codePromise } = createServer(nonce); + const host = redirectReq.req.headers.host || ''; + const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1]; + const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port; + + const state = `${updatedPort},${encodeURIComponent(nonce)}`; + + const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64')); + const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier)); + const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`; + + redirectReq.res.writeHead(302, { Location: loginUrl }); + redirectReq.res.end(); + + const codeRes = await codePromise; + const res = codeRes.res; - let token: IToken | undefined; try { - const port = await startServer(server); - vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`)); - - const redirectReq = await redirectPromise; - if ('err' in redirectReq) { - const { err, res } = redirectReq; - res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); - res.end(); - throw err; + if ('err' in codeRes) { + throw codeRes.err; } - - const host = redirectReq.req.headers.host || ''; - const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1]; - const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port; - - const state = `${updatedPort},${encodeURIComponent(nonce)}`; - - const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64')); - const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier)); - const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`; - - await redirectReq.res.writeHead(302, { Location: loginUrl }); - redirectReq.res.end(); - - const codeRes = await codePromise; - const res = codeRes.res; - - try { - if ('err' in codeRes) { - throw codeRes.err; - } - token = await this.exchangeCodeForToken(codeRes.code, codeVerifier, scope); - this.setToken(token, scope); - Logger.info('Login successful'); - res.writeHead(302, { Location: '/' }); - const session = await this.convertToSession(token); - resolve(session); - res.end(); - } catch (err) { - res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); - res.end(); - reject(err.message); - } - } catch (e) { - Logger.error(e.message); - - // If the error was about starting the server, try directly hitting the login endpoint instead - if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { - await this.loginWithoutLocalServer(scope); - } - - reject(e.message); + token = await this.exchangeCodeForToken(codeRes.code, codeVerifier, scope); + this.setToken(token, scope); + Logger.info(`Login successful for scopes: ${scope}`); + res.writeHead(302, { Location: '/' }); + const session = await this.convertToSession(token); + return session; + } catch (err) { + res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); + throw err; } finally { - setTimeout(() => { - server.close(); - }, 5000); + res.end(); } - }); + } catch (e) { + Logger.error(`Error creating session for scopes: ${scope} Error: ${e}`); + + // If the error was about starting the server, try directly hitting the login endpoint instead + if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { + return this.loginWithoutLocalServer(scope); + } + + throw e; + } finally { + setTimeout(() => { + server.close(); + }, 5000); + } } public dispose(): void { - this._disposables.forEach(disposable => disposable.dispose()); - this._disposables = []; + this._disposable.dispose(); } private getCallbackEnvironment(callbackUri: vscode.Uri): string { @@ -554,7 +561,7 @@ export class AzureActiveDirectoryService { } private async exchangeCodeForToken(code: string, codeVerifier: string, scope: string): Promise { - Logger.info('Exchanging login code for token'); + Logger.info(`Exchanging login code for token for scopes: ${scope}`); try { const postData = querystring.stringify({ grant_type: 'authorization_code', @@ -579,21 +586,21 @@ export class AzureActiveDirectoryService { }); if (result.ok) { - Logger.info('Exchanging login code for token success'); const json = await result.json(); + Logger.info(`Exchanging login code for token (for scopes: ${scope}) succeeded!`); return this.getTokenFromResponse(json, scope); } else { - Logger.error('Exchanging login code for token failed'); + Logger.error(`Exchanging login code for token (for scopes: ${scope}) failed: ${await result.text()}`); throw new Error('Unable to login.'); } } catch (e) { - Logger.error(e.message); + Logger.error(`Error exchanging code for token (for scopes ${scope}): ${e}`); throw e; } } private async refreshToken(refreshToken: string, scope: string, sessionId: string): Promise { - Logger.info('Refreshing token...'); + Logger.info(`Refreshing token for scopes: ${scope}`); const postData = querystring.stringify({ refresh_token: refreshToken, client_id: clientId, @@ -615,7 +622,7 @@ export class AzureActiveDirectoryService { body: postData }); } catch (e) { - Logger.error('Refreshing token failed'); + Logger.error(`Refreshing token failed (for scopes: ${scope}) Error: ${e}`); throw new Error(REFRESH_NETWORK_FAILURE); } @@ -624,14 +631,14 @@ export class AzureActiveDirectoryService { const json = await result.json(); const token = this.getTokenFromResponse(json, scope, sessionId); this.setToken(token, scope); - Logger.info('Token refresh success'); + Logger.info(`Token refresh success for scopes: ${token.scope}`); return token; } else { throw new Error('Bad request.'); } } catch (e) { vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); - Logger.error(`Refreshing token failed: ${result.statusText}`); + Logger.error(`Refreshing token failed (for scopes: ${scope}): ${result.statusText}`); throw new Error('Refreshing token failed'); } } @@ -672,7 +679,7 @@ export class AzureActiveDirectoryService { private handleRefreshNetworkError(sessionId: string, refreshToken: string, scope: string, attempts: number = 1): Promise { return new Promise((resolve, _) => { if (attempts === 3) { - Logger.error('Token refresh failed after 3 attempts'); + Logger.error(`Token refresh (for scopes: ${scope}) failed after 3 attempts`); return resolve(false); } diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 589b8d38e2..57f723f963 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -7,8 +7,6 @@ import * as vscode from 'vscode'; import { AzureActiveDirectoryService, onDidChangeSessions } from './AADHelper'; import TelemetryReporter from 'vscode-extension-telemetry'; -export const DEFAULT_SCOPES = 'https://management.core.windows.net/.default offline_access'; - export async function activate(context: vscode.ExtensionContext) { const { name, version, aiKey } = context.extension.packageJSON as { name: string, version: string, aiKey: string }; const telemetryReporter = new TelemetryReporter(name, version, aiKey); diff --git a/extensions/microsoft-authentication/src/keychain.ts b/extensions/microsoft-authentication/src/keychain.ts index 1217c7d55e..802965e439 100644 --- a/extensions/microsoft-authentication/src/keychain.ts +++ b/extensions/microsoft-authentication/src/keychain.ts @@ -3,47 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// keytar depends on a native module shipped in vscode, so this is -// how we load it -import * as keytarType from 'keytar'; import * as vscode from 'vscode'; import Logger from './logger'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -function getKeytar(): Keytar | undefined { - try { - return require('keytar'); - } catch (err) { - console.log(err); - } - - return undefined; -} - -export type Keytar = { - getPassword: typeof keytarType['getPassword']; - setPassword: typeof keytarType['setPassword']; - deletePassword: typeof keytarType['deletePassword']; -}; - -const OLD_SERVICE_ID = `${vscode.env.uriScheme}-microsoft.login`; const SERVICE_ID = `microsoft.login`; -const ACCOUNT_ID = 'account'; export class Keychain { - private keytar: Keytar; - - constructor(private context: vscode.ExtensionContext) { - const keytar = getKeytar(); - if (!keytar) { - throw new Error('System keychain unavailable'); - } - - this.keytar = keytar; - } + constructor(private context: vscode.ExtensionContext) { } async setToken(token: string): Promise { @@ -87,19 +57,4 @@ export class Keychain { return Promise.resolve(undefined); } } - - async tryMigrate(): Promise { - try { - const oldValue = await this.keytar.getPassword(OLD_SERVICE_ID, ACCOUNT_ID); - if (oldValue) { - await this.setToken(oldValue); - await this.keytar.deletePassword(OLD_SERVICE_ID, ACCOUNT_ID); - } - - return oldValue; - } catch (_) { - // Ignore - return Promise.resolve(null); - } - } } diff --git a/extensions/microsoft-authentication/src/logger.ts b/extensions/microsoft-authentication/src/logger.ts index a44e4bdd35..e37b41d22d 100644 --- a/extensions/microsoft-authentication/src/logger.ts +++ b/extensions/microsoft-authentication/src/logger.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; -type LogLevel = 'Info' | 'Error'; +type LogLevel = 'Trace' | 'Info' | 'Error'; class Log { private output: vscode.OutputChannel; @@ -24,6 +24,10 @@ class Log { return data.toString(); } + public trace(message: string, data?: any): void { + this.logLevel('Trace', message, data); + } + public info(message: string, data?: any): void { this.logLevel('Info', message, data); } diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index cd0504e074..c31a15a229 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -112,9 +112,10 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -"randombytes@github:rmacfarlane/randombytes#b28d4ecee46262801ea09f15fa1f1513a05c5971": +randombytes@~2.1.0: version "2.1.0" - resolved "https://codeload.github.com/rmacfarlane/randombytes/tar.gz/b28d4ecee46262801ea09f15fa1f1513a05c5971" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" @@ -153,10 +154,10 @@ uuid@^8.2.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.2.0.tgz#cb10dd6b118e2dada7d0cd9730ba7417c93d920e" integrity sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q== -vscode-extension-telemetry@0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.2.8.tgz#670c625c44791237c040cee2cb9f567ca34784ac" - integrity sha512-Vf52im5qzORRD2K5Ryp8PXo31YXVcJAYRSDDZGegWlt0OATOd83DYabS1U/WIq9nR5g80UQKH3+BsenhpQHUaA== +vscode-extension-telemetry@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.2.tgz#6ef847a80c9cfc207eb15e3a254f235acebb65a5" + integrity sha512-y0f51mVoFxHIzULQNCC26TBFIKdEC7uckS3tFoK++OOOl8mU2LlOxgmbd52T/SXoXNg5aI7xqs+4V2ug5ITvKw== vscode-nls@^5.0.0: version "5.0.0" diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index e800ff2941..5db0c5b411 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -114,7 +114,7 @@ export class AccountFeature implements StaticFeature { } // find tenant - const tenant = account.properties.tenants.find(tenant => tenant.id === request.tenantId); + const tenant = account.properties.tenants.find((tenant: any) => tenant.id === request.tenantId); if (!tenant) { console.log(`Failed to find tenant ${request.tenantId} in account ${account.displayInfo.displayName} when refreshing security token`); throw Error(localizedConstants.failedToFindTenants(request.tenantId, account.displayInfo.displayName)); diff --git a/extensions/package.json b/extensions/package.json index 86bfd54f1a..3163e6a239 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^4.4.1-rc" + "typescript": "^4.8.0-dev.20220614" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/python/package.json b/extensions/python/package.json index 91068a5033..b1b86500f9 100644 --- a/extensions/python/package.json +++ b/extensions/python/package.json @@ -24,7 +24,8 @@ ".gyp", ".gypi", ".pyi", - ".ipy" + ".ipy", + ".pyt" ], "aliases": [ "Python", diff --git a/extensions/r/cgmanifest.json b/extensions/r/cgmanifest.json index 05f8a237fd..9a2c139825 100644 --- a/extensions/r/cgmanifest.json +++ b/extensions/r/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "Ikuyadeu/vscode-R", "repositoryUrl": "https://github.com/Ikuyadeu/vscode-R", - "commitHash": "f98bd30417c203876969408440f656f56eba80d8" + "commitHash": "c6a9803fbda262ea68c427a2339bddafed41a9d5" } }, "license": "MIT", - "version": "2.0.0" + "version": "2.1.0" } ], "version": 1 diff --git a/extensions/r/syntaxes/r.tmLanguage.json b/extensions/r/syntaxes/r.tmLanguage.json index 9e7500c59c..d2186b9c5f 100644 --- a/extensions/r/syntaxes/r.tmLanguage.json +++ b/extensions/r/syntaxes/r.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/Ikuyadeu/vscode-R/commit/f98bd30417c203876969408440f656f56eba80d8", + "version": "https://github.com/Ikuyadeu/vscode-R/commit/c6a9803fbda262ea68c427a2339bddafed41a9d5", "name": "R", "scopeName": "source.r", "patterns": [ @@ -332,8 +332,8 @@ "function-declarations": { "patterns": [ { - "begin": "^\\s*([a-zA-Z._][\\w.:]*)\\s*(< implements cp.ChildProcessPromise { } resolve!: (value: T | PromiseLike) => void; reject!: (reason?: any) => void; - then(onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null): Promise { + + + then(onFulfilled?: ((value: any) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null): Promise { return this._promise.then(onFulfilled, onRejected); } - catch(onRejected?: ((reason: any) => TResult | PromiseLike) | null): Promise { + + catch(onRejected?: ((reason: any) => TResult | PromiseLike) | null): Promise { return this._promise.catch(onRejected); } - [Symbol.toStringTag]: string; - finally(onFinally?: (() => void) | null): Promise { + [Symbol.toStringTag]: string = ''; + finally(onFinally?: (() => void) | null): Promise { return this._promise.finally(onFinally); } stdin: any = this._event; diff --git a/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts b/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts index b5c899e5fe..c115dbdfed 100644 --- a/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts +++ b/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts @@ -25,7 +25,7 @@ export class RadioGroupLoadingComponentBuilder implements azdata.ComponentBuilde } withProperties(properties: U): azdata.ComponentBuilder { - return this._optionsLoadingBuilder.withProps(properties); + return this._optionsLoadingBuilder.withProps(properties); } withProps(properties: azdata.LoadingComponentProperties): azdata.ComponentBuilder { diff --git a/extensions/search-result/images/icon.png b/extensions/search-result/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..90a56780f58b70460ff92d6594fd5b47346be262 GIT binary patch literal 2901 zcmXArdpy(oAICr6%`oPY+;VFxx#Tt^G{;6Fkz3?eEF3AFOjKytEFobj$*+~261i>_ zrQ9`9q;ZO=*a(GO5}I80(>agF`|)`_-`B_c^VjE-;o;^aBc&z<0FZIs@8AUhw1E(i zgl|Y>g`f9^40*VE@7qw2&;=-OLBbnwq3bs%bOLhcZ#S+TP};$T4c-X80hHIjA@1KA z^1qSzF0f(bjm9^>Q}La@C;WGA3`uzXZ|`e5XM%8R;O+P#^UXK@%rZLjAMW@Pz3ppG z+gJXK@a_0g)6l#~B*I?n^CG=Z)Q=aw`9@&yk{iZ6?tQ{u?_<81 zBGrtLsz=!GXR+7bv-)P)eX}`j6TuaH?Dh8>I}j`Ql!h?^V*s7mE1&XqE-b!h}-SLDAQY(&b_veQXp-FBTJL5BzNJxjie>9p~s-6Mn|9#KOP4=A~R^P0fiv zl=8C@0(J5&j4tB23T{0Of#;s;W+nT#&U9V4Xfl<8Gm2zL;FoN@}+r+ozxw355#T|tJ+N-iK+j+ zx7xgEZMD1DdCg)cHIjshy(0Bu+GN543k^zNI<}U2M9d$at1FPCF&_j8QmEg$`tf)Z z8@wR(i$wk0Dn;ea*16Un@J72NXfqGX&P?#*a2W|)S5>t?yG48->Q3j%ztjCy}5tK6Z29WGWA7TlN%?)%JqQM!8)fR zktl7??eE$u2I72!X|=IsIJ03b&i8rXRrI+hdX52!ZzF1wj^C+JmEC`3fK;TVZ7}5D z5~3Fj!cz}_^vk>WPg3IxB=^(FMZHI@9XhT3+o}r7)kcFQM=h9?wi_lPDTmOIq~TU3 z*6al34~XZ2IO8HIfhz{%0WD$rQ0z$r_#U!5p{em#p}>tR0V4>%2P*;hoUyp76v5qVFs49`qXL456~B)Ddc!MmO?7&nX| zzw|g5faAiA7T3nyjsB1d3?Ir6EWEO$-ca*j*9@PGq@FV$=vHr8{9@rt?6xtQQ|q9R zr}|^c4i+r?Gey_yh>o=NSgWi@ zAIYDoTtq#>#3%qu#fwTBVi)FRJ$%@ErD&CEXEhz9tJ9H(68%O;3MFe~n26`wws1YL zmujUuo>0x5K$;xvuLk$7LKiE7BgXldN`#KFiMB>FyWnsjf34>d0jZ~FD1MfDZiJ-m z2}2>mD;v*D7l`vE)0 z>Tn`BYWV4jqxDHUNAnw7z;Zd4v*@}jz8c{J}d#WEr3MQ9CI~o$I69ouJ#=3*J>h{Nn*M%7(HMMzU}_sRQog$fn|$4Nh@W+C1e*Es;c40xr=oR`57<_@taMTo80of^>n zE2Q?Ic28Mk$cs?^Lz!3@-qMs>Dot|y*!3i zn$j5Z_$|H|EFIpV&h+NrBqe6%y;cLFFwVT{ZrxAdu0J6$90~p;!8>XJP1Uc3&+Sxi zj?fKgcqa41;!H0ZX=^R&%YTN;g88J3*wG7`ZS$^HK&v7)5|_gfsxtQtb-2^9TeQGT z5=DM*JB->FOwWgct|~q=cR+>}q&^;tp3f~}7nsY+g%rab51CBRRb!6RRsBTehOaWS zW3bz-oUsOwWkBhykdTDR-QKc@t8hK~^KENY;qzA*Wb_PPcOa;&d8#C-S9^;lkmG4RRfT(_CR+~->ZIK$%%!<+BAURT zst4r9Pg`{yka`t(DnZN`Fa!r=aO_2`S5$&Bs`0#U)f1Mj ziBW0^GSMnSnvzyfnDvf!MCOo-h6XlyPvl#l)E`Ef?1_Z4e5nL)!|@xnUrpWR{h@}; z+eKrV6FdbI-TKYDPE!{&_3O3Lxf#&r-pq zvj6ES0U>etp9csK1hNAxp)^tA|3ILrfRT`Nb7}^*gC22wQDW`$7DttRq0kf6|v;2Y&INTBgUZ z)xw2!&G>AP-UE?T{3efX2Exj6nORue-c8n&PV^awf&*0u4M_ezTo54W-2^~~MMXs+%+#G>lsLy*rrVtWX8=B& z%k$EZ0^`20^wTOR_lrH+1V*B=bVn3yc;O?5LWY_ryxuXihivYpFSAznxMrz}wPIJT zzp4%pD(DnL^>J*2sNfgtN2vb&>5~>eJ_&q|+wTmbe3?F8FQz{2d#XD4VNX(<BXs46Qa|B6u@me##Ds5rOBTea#YgP;IC$giGlP64HPgz0PcQ2%fsS^1~dav-PH>n|H56-hI=P?xaPl=d!MU3lQ8W7&G!qN~Tt zAvY[] = []; @@ -233,12 +232,12 @@ function parseSearchResults(document: vscode.TextDocument, token?: vscode.Cancel targetRange, targetSelectionRange: new vscode.Range(lineNumber, 0, lineNumber, 1), targetUri: currentTarget, - originSelectionRange: new vscode.Range(i, 0, i, resultStart), + originSelectionRange: new vscode.Range(i, 0, i, metadataOffset - 1), }); - let lastEnd = resultStart; + let lastEnd = metadataOffset; let offset = 0; - ELISION_REGEX.lastIndex = resultStart; + ELISION_REGEX.lastIndex = metadataOffset; for (let match: RegExpExecArray | null; (match = ELISION_REGEX.exec(line));) { locations.push({ targetRange, @@ -261,7 +260,7 @@ function parseSearchResults(document: vscode.TextDocument, token?: vscode.Cancel } currentTargetLocations?.push(...locations); - links[i] = { type: 'result', locations, isContext: seperator === ' ', prefixRange: new vscode.Range(i, 0, i, metadataOffset) }; + links[i] = { type: 'result', locations, isContext: separator === ' ', prefixRange: new vscode.Range(i, 0, i, metadataOffset) }; } } diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 97627a395a..c1382be241 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -24,7 +24,8 @@ "onCommand:simpleBrowser.show", "onCommand:simpleBrowser.api.open", "onOpenExternalUri:http", - "onOpenExternalUri:https" + "onOpenExternalUri:https", + "onWebviewPanel:simpleBrowser.view" ], "capabilities": { "virtualWorkspaces": true, @@ -65,7 +66,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "vscode-extension-telemetry": "0.2.8", + "vscode-extension-telemetry": "0.4.2", "vscode-nls": "^5.0.0" }, "devDependencies": { diff --git a/extensions/simple-browser/preview-src/index.ts b/extensions/simple-browser/preview-src/index.ts index 8c27186e9b..b94a5bd6d4 100644 --- a/extensions/simple-browser/preview-src/index.ts +++ b/extensions/simple-browser/preview-src/index.ts @@ -101,6 +101,8 @@ onceDocumentLoaded(() => { } catch { iframe.src = rawUrl; } + + vscode.setState({ url: rawUrl }); } }); diff --git a/extensions/simple-browser/src/extension.ts b/extensions/simple-browser/src/extension.ts index 1e4d518cd1..909aa88055 100644 --- a/extensions/simple-browser/src/extension.ts +++ b/extensions/simple-browser/src/extension.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { SimpleBrowserManager } from './simpleBrowserManager'; +import { SimpleBrowserView } from './simpleBrowserView'; declare class URL { constructor(input: string, base?: string | URL); @@ -38,6 +39,12 @@ export function activate(context: vscode.ExtensionContext) { const manager = new SimpleBrowserManager(context.extensionUri); context.subscriptions.push(manager); + context.subscriptions.push(vscode.window.registerWebviewPanelSerializer(SimpleBrowserView.viewType, { + deserializeWebviewPanel: async (panel, state) => { + manager.restore(panel, state); + } + })); + context.subscriptions.push(vscode.commands.registerCommand(showCommand, async (url?: string) => { if (!url) { url = await vscode.window.showInputBox({ diff --git a/extensions/simple-browser/src/simpleBrowserManager.ts b/extensions/simple-browser/src/simpleBrowserManager.ts index 240e3acc45..5814e6468d 100644 --- a/extensions/simple-browser/src/simpleBrowserManager.ts +++ b/extensions/simple-browser/src/simpleBrowserManager.ts @@ -23,16 +23,27 @@ export class SimpleBrowserManager { if (this._activeView) { this._activeView.show(url, options); } else { - const view = new SimpleBrowserView(this.extensionUri, url, options); - view.onDispose(() => { - if (this._activeView === view) { - this._activeView = undefined; - } - }); + const view = SimpleBrowserView.create(this.extensionUri, url, options); + this.registerWebviewListeners(view); this._activeView = view; } } + + public restore(panel: vscode.WebviewPanel, state: any): void { + const url = state?.url ?? ''; + const view = SimpleBrowserView.restore(this.extensionUri, url, panel); + this.registerWebviewListeners(view); + return; + } + + private registerWebviewListeners(view: SimpleBrowserView) { + view.onDispose(() => { + if (this._activeView === view) { + this._activeView = undefined; + } + }); + } + } - diff --git a/extensions/simple-browser/src/simpleBrowserView.ts b/extensions/simple-browser/src/simpleBrowserView.ts index 2eca1cf86e..8dfadb7acd 100644 --- a/extensions/simple-browser/src/simpleBrowserView.ts +++ b/extensions/simple-browser/src/simpleBrowserView.ts @@ -24,23 +24,41 @@ export class SimpleBrowserView extends Disposable { private readonly _onDidDispose = this._register(new vscode.EventEmitter()); public readonly onDispose = this._onDidDispose.event; - constructor( - private readonly extensionUri: vscode.Uri, + public static create( + extensionUri: vscode.Uri, url: string, showOptions?: ShowOptions - ) { - super(); - - this._webviewPanel = this._register(vscode.window.createWebviewPanel(SimpleBrowserView.viewType, SimpleBrowserView.title, { + ): SimpleBrowserView { + const webview = vscode.window.createWebviewPanel(SimpleBrowserView.viewType, SimpleBrowserView.title, { viewColumn: showOptions?.viewColumn ?? vscode.ViewColumn.Active, preserveFocus: showOptions?.preserveFocus }, { enableScripts: true, + enableForms: true, retainContextWhenHidden: true, localResourceRoots: [ vscode.Uri.joinPath(extensionUri, 'media') ] - })); + }); + return new SimpleBrowserView(extensionUri, url, webview); + } + + public static restore( + extensionUri: vscode.Uri, + url: string, + webview: vscode.WebviewPanel, + ): SimpleBrowserView { + return new SimpleBrowserView(extensionUri, url, webview); + } + + private constructor( + private readonly extensionUri: vscode.Uri, + url: string, + webviewPanel: vscode.WebviewPanel, + ) { + super(); + + this._webviewPanel = this._register(webviewPanel); this._register(this._webviewPanel.webview.onDidReceiveMessage(e => { switch (e.type) { diff --git a/extensions/simple-browser/yarn.lock b/extensions/simple-browser/yarn.lock index 79bcc4467c..66748309e7 100644 --- a/extensions/simple-browser/yarn.lock +++ b/extensions/simple-browser/yarn.lock @@ -12,10 +12,10 @@ vscode-codicons@^0.0.14: resolved "https://registry.yarnpkg.com/vscode-codicons/-/vscode-codicons-0.0.14.tgz#e0d05418e2e195564ff6f6a2199d70415911c18f" integrity sha512-6CEH5KT9ct5WMw7n5dlX7rB8ya4CUI2FSq1Wk36XaW+c5RglFtAanUV0T+gvZVVFhl/WxfjTvFHq06Hz9c1SLA== -vscode-extension-telemetry@0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.2.8.tgz#670c625c44791237c040cee2cb9f567ca34784ac" - integrity sha512-Vf52im5qzORRD2K5Ryp8PXo31YXVcJAYRSDDZGegWlt0OATOd83DYabS1U/WIq9nR5g80UQKH3+BsenhpQHUaA== +vscode-extension-telemetry@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.2.tgz#6ef847a80c9cfc207eb15e3a254f235acebb65a5" + integrity sha512-y0f51mVoFxHIzULQNCC26TBFIKdEC7uckS3tFoK++OOOl8mU2LlOxgmbd52T/SXoXNg5aI7xqs+4V2ug5ITvKw== vscode-nls@^5.0.0: version "5.0.0" diff --git a/extensions/sql-migration/.eslintrc.json b/extensions/sql-migration/.eslintrc.json index 52508f7ce3..98fe0ce7b1 100644 --- a/extensions/sql-migration/.eslintrc.json +++ b/extensions/sql-migration/.eslintrc.json @@ -4,6 +4,7 @@ }, "rules": { // Disabled until the issues can be fixed - "@typescript-eslint/explicit-function-return-type": ["off"] + "@typescript-eslint/explicit-function-return-type": ["off"], + "@typescript-eslint/no-async-promise-executor": ["off"] } } diff --git a/extensions/theme-seti/build/update-icon-theme.js b/extensions/theme-seti/build/update-icon-theme.js index 0b30e6aa27..41d95b81af 100644 --- a/extensions/theme-seti/build/update-icon-theme.js +++ b/extensions/theme-seti/build/update-icon-theme.js @@ -5,14 +5,15 @@ 'use strict'; -let path = require('path'); -let fs = require('fs'); -let https = require('https'); -let url = require('url'); +const path = require('path'); +const fs = require('fs'); +const https = require('https'); +const url = require('url'); +const minimatch = require('minimatch'); // list of languagesId not shipped with VSCode. The information is used to associate an icon with a language association // Please try and keep this list in alphabetical order! Thank you. -let nonBuiltInLanguages = { // { fileNames, extensions } +const nonBuiltInLanguages = { // { fileNames, extensions } "argdown": { extensions: ['ad', 'adown', 'argdown', 'argdn'] }, "bicep": { extensions: ['bicep'] }, "elixir": { extensions: ['ex'] }, @@ -41,13 +42,13 @@ let nonBuiltInLanguages = { // { fileNames, extensions } }; // list of languagesId that inherit the icon from another language -let inheritIconFromLanguage = { +const inheritIconFromLanguage = { "jsonc": 'json', "postcss": 'css', "django-html": 'html' } -let FROM_DISK = true; // set to true to take content from a repo checked out next to the vscode repo +const FROM_DISK = true; // set to true to take content from a repo checked out next to the vscode repo let font, fontMappingsFile, fileAssociationFile, colorsFile; if (!FROM_DISK) { @@ -63,10 +64,10 @@ if (!FROM_DISK) { } function getCommitSha(repoId) { - let commitInfo = 'https://api.github.com/repos/' + repoId + '/commits/master'; + const commitInfo = 'https://api.github.com/repos/' + repoId + '/commits/master'; return download(commitInfo).then(function (content) { try { - let lastCommit = JSON.parse(content); + const lastCommit = JSON.parse(content); return Promise.resolve({ commitSha: lastCommit.sha, commitDate: lastCommit.commit.author.date @@ -86,8 +87,8 @@ function download(source) { return readFile(source); } return new Promise((c, e) => { - let _url = url.parse(source); - let options = { host: _url.host, port: _url.port, path: _url.path, headers: { 'User-Agent': 'NodeJS' } }; + const _url = url.parse(source); + const options = { host: _url.host, port: _url.port, path: _url.path, headers: { 'User-Agent': 'NodeJS' } }; let content = ''; https.get(options, function (response) { response.on('data', function (data) { @@ -122,7 +123,7 @@ function downloadBinary(source, dest) { https.get(source, function (response) { switch (response.statusCode) { case 200: { - let file = fs.createWriteStream(dest); + const file = fs.createWriteStream(dest); response.on('data', function (chunk) { file.write(chunk); }).on('end', function () { @@ -157,9 +158,9 @@ function copyFile(fileName, dest) { cbCalled = true; } } - let rd = fs.createReadStream(fileName); + const rd = fs.createReadStream(fileName); rd.on("error", handleError); - let wr = fs.createWriteStream(dest); + const wr = fs.createWriteStream(dest); wr.on("error", handleError); wr.on("close", function () { if (!cbCalled) { @@ -174,8 +175,8 @@ function copyFile(fileName, dest) { function darkenColor(color) { let res = '#'; for (let i = 1; i < 7; i += 2) { - let newVal = Math.round(parseInt('0x' + color.substr(i, 2), 16) * 0.9); - let hex = newVal.toString(16); + const newVal = Math.round(parseInt('0x' + color.substr(i, 2), 16) * 0.9); + const hex = newVal.toString(16); if (hex.length === 1) { res += '0'; } @@ -195,28 +196,32 @@ function mergeMapping(to, from, property) { } function getLanguageMappings() { - let langMappings = {}; - let allExtensions = fs.readdirSync('..'); + const langMappings = {}; + const allExtensions = fs.readdirSync('..'); for (let i = 0; i < allExtensions.length; i++) { - let dirPath = path.join('..', allExtensions[i], 'package.json'); + const dirPath = path.join('..', allExtensions[i], 'package.json'); if (fs.existsSync(dirPath)) { - let content = fs.readFileSync(dirPath).toString(); - let jsonContent = JSON.parse(content); - let languages = jsonContent.contributes && jsonContent.contributes.languages; + const content = fs.readFileSync(dirPath).toString(); + const jsonContent = JSON.parse(content); + const languages = jsonContent.contributes && jsonContent.contributes.languages; if (Array.isArray(languages)) { for (let k = 0; k < languages.length; k++) { - let languageId = languages[k].id; + const languageId = languages[k].id; if (languageId) { - let extensions = languages[k].extensions; - let mapping = {}; + const extensions = languages[k].extensions; + const mapping = {}; if (Array.isArray(extensions)) { mapping.extensions = extensions.map(function (e) { return e.substr(1).toLowerCase(); }); } - let filenames = languages[k].filenames; + const filenames = languages[k].filenames; if (Array.isArray(filenames)) { mapping.fileNames = filenames.map(function (f) { return f.toLowerCase(); }); } - let existing = langMappings[languageId]; + const filenamePatterns = languages[k].filenamePatterns; + if (Array.isArray(filenamePatterns)) { + mapping.filenamePatterns = filenamePatterns.map(function (f) { return f.toLowerCase(); }); + } + const existing = langMappings[languageId]; if (existing) { // multiple contributions to the same language @@ -224,10 +229,12 @@ function getLanguageMappings() { if (languages[k].configuration) { mergeMapping(mapping, existing, 'extensions'); mergeMapping(mapping, existing, 'fileNames'); + mergeMapping(mapping, existing, 'filenamePatterns'); langMappings[languageId] = mapping; } else { mergeMapping(existing, mapping, 'extensions'); mergeMapping(existing, mapping, 'fileNames'); + mergeMapping(existing, mapping, 'filenamePatterns'); } } else { langMappings[languageId] = mapping; @@ -237,14 +244,12 @@ function getLanguageMappings() { } } } - for (let languageId in nonBuiltInLanguages) { + for (const languageId in nonBuiltInLanguages) { langMappings[languageId] = nonBuiltInLanguages[languageId]; } return langMappings; } - - exports.copyFont = function () { return downloadBinary(font, './icons/seti.woff'); }; @@ -252,27 +257,27 @@ exports.copyFont = function () { exports.update = function () { console.log('Reading from ' + fontMappingsFile); - let def2Content = {}; - let ext2Def = {}; - let fileName2Def = {}; - let def2ColorId = {}; - let colorId2Value = {}; - let lang2Def = {}; + const def2Content = {}; + const ext2Def = {}; + const fileName2Def = {}; + const def2ColorId = {}; + const colorId2Value = {}; + const lang2Def = {}; function writeFileIconContent(info) { - let iconDefinitions = {}; - let allDefs = Object.keys(def2Content).sort(); + const iconDefinitions = {}; + const allDefs = Object.keys(def2Content).sort(); for (let i = 0; i < allDefs.length; i++) { - let def = allDefs[i]; - let entry = { fontCharacter: def2Content[def] }; - let colorId = def2ColorId[def]; + const def = allDefs[i]; + const entry = { fontCharacter: def2Content[def] }; + const colorId = def2ColorId[def]; if (colorId) { - let colorValue = colorId2Value[colorId]; + const colorValue = colorId2Value[colorId]; if (colorValue) { entry.fontColor = colorValue; - let entryInverse = { fontCharacter: entry.fontCharacter, fontColor: darkenColor(colorValue) }; + const entryInverse = { fontCharacter: entry.fontCharacter, fontColor: darkenColor(colorValue) }; iconDefinitions[def + '_light'] = entryInverse; } } @@ -280,9 +285,9 @@ exports.update = function () { } function getInvertSet(input) { - let result = {}; - for (let assoc in input) { - let invertDef = input[assoc] + '_light'; + const result = {}; + for (const assoc in input) { + const invertDef = input[assoc] + '_light'; if (iconDefinitions[invertDef]) { result[assoc] = invertDef; } @@ -290,7 +295,7 @@ exports.update = function () { return result; } - let res = { + const res = { information_for_contributors: [ 'This file has been generated from data in https://github.com/jesseweed/seti-ui', '- icon definitions: https://github.com/jesseweed/seti-ui/blob/master/styles/_fonts/seti.less', @@ -321,7 +326,7 @@ exports.update = function () { version: 'https://github.com/jesseweed/seti-ui/commit/' + info.commitSha, }; - let path = './icons/vs-seti-icon-theme.json'; + const path = './icons/vs-seti-icon-theme.json'; fs.writeFileSync(path, JSON.stringify(res, null, '\t')); console.log('written ' + path); } @@ -330,18 +335,18 @@ exports.update = function () { let match; return download(fontMappingsFile).then(function (content) { - let regex = /@([\w-]+):\s*'(\\E[0-9A-F]+)';/g; - let contents = {}; + const regex = /@([\w-]+):\s*'(\\E[0-9A-F]+)';/g; + const contents = {}; while ((match = regex.exec(content)) !== null) { contents[match[1]] = match[2]; } return download(fileAssociationFile).then(function (content) { - let regex2 = /\.icon-(?:set|partial)\(['"]([\w-\.+]+)['"],\s*['"]([\w-]+)['"],\s*(@[\w-]+)\)/g; + const regex2 = /\.icon-(?:set|partial)\(['"]([\w-\.+]+)['"],\s*['"]([\w-]+)['"],\s*(@[\w-]+)\)/g; while ((match = regex2.exec(content)) !== null) { - let pattern = match[1]; + const pattern = match[1]; let def = '_' + match[2]; - let colorId = match[3]; + const colorId = match[3]; let storedColorId = def2ColorId[def]; let i = 1; while (storedColorId && colorId !== storedColorId) { // different colors for the same def? @@ -364,20 +369,30 @@ exports.update = function () { } } // replace extensions for languageId - let langMappings = getLanguageMappings(); + const langMappings = getLanguageMappings(); for (let lang in langMappings) { - let mappings = langMappings[lang]; - let exts = mappings.extensions || []; - let fileNames = mappings.fileNames || []; + const mappings = langMappings[lang]; + const exts = mappings.extensions || []; + const fileNames = mappings.fileNames || []; + const filenamePatterns = mappings.filenamePatterns || []; let preferredDef = null; - // use the first file association for the preferred definition + // use the first file extension association for the preferred definition for (let i1 = 0; i1 < exts.length && !preferredDef; i1++) { preferredDef = ext2Def[exts[i1]]; } - // use the first file association for the preferred definition + // use the first file name association for the preferred definition, if not availbale for (let i1 = 0; i1 < fileNames.length && !preferredDef; i1++) { preferredDef = fileName2Def[fileNames[i1]]; } + for (let i1 = 0; i1 < filenamePatterns.length && !preferredDef; i1++) { + let pattern = filenamePatterns[i1]; + for (const name in fileName2Def) { + if (minimatch(name, pattern)) { + preferredDef = fileName2Def[name]; + break; + } + } + } if (preferredDef) { lang2Def[lang] = preferredDef; if (!nonBuiltInLanguages[lang] && !inheritIconFromLanguage[lang]) { @@ -393,12 +408,21 @@ exports.update = function () { delete fileName2Def[fileNames[i2]]; } } + for (let i2 = 0; i2 < filenamePatterns.length; i2++) { + let pattern = filenamePatterns[i2]; + // remove the filenamePatterns association, unless it is different from the preferred + for (const name in fileName2Def) { + if (minimatch(name, pattern) && fileName2Def[name] === preferredDef) { + delete fileName2Def[name]; + } + } + } } } } - for (let lang in inheritIconFromLanguage) { - let superLang = inheritIconFromLanguage[lang]; - let def = lang2Def[superLang]; + for (const lang in inheritIconFromLanguage) { + const superLang = inheritIconFromLanguage[lang]; + const def = lang2Def[superLang]; if (def) { lang2Def[lang] = def; } else { @@ -409,7 +433,7 @@ exports.update = function () { return download(colorsFile).then(function (content) { - let regex3 = /(@[\w-]+):\s*(#[0-9a-z]+)/g; + const regex3 = /(@[\w-]+):\s*(#[0-9a-z]+)/g; while ((match = regex3.exec(content)) !== null) { colorId2Value[match[1]] = match[2]; } @@ -417,9 +441,9 @@ exports.update = function () { try { writeFileIconContent(info); - let cgmanifestPath = './cgmanifest.json'; - let cgmanifest = fs.readFileSync(cgmanifestPath).toString(); - let cgmanifestContent = JSON.parse(cgmanifest); + const cgmanifestPath = './cgmanifest.json'; + const cgmanifest = fs.readFileSync(cgmanifestPath).toString(); + const cgmanifestContent = JSON.parse(cgmanifest); cgmanifestContent['registrations'][0]['component']['git']['commitHash'] = info.commitSha; fs.writeFileSync(cgmanifestPath, JSON.stringify(cgmanifestContent, null, '\t')); console.log('updated ' + cgmanifestPath); diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index 842eb5968d..bc693635f3 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": "b484fa778f564e71c81f22ee4c006c4664d66a47" + "commitHash": "8eacb11357be8b79868246f972702b6b2460e9ac" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index 9add6e060ce388057c6d2a91e88f81b802c9b86b..be34169b7687d41abebce82974f7839afeeafc7f 100644 GIT binary patch delta 36731 zcmV)TK(W8To&u<$0u*;oMn(Vu00000kf;C)00000+vJfHKYveSZDDW#00D#m00U?M z01AZrsq8psYf~-v9u3obA=+RvcLvM&V6DLfqZm z-QC^Y-IchzyF1a*+*bdX`^eN=doE*OJ#~uy`s~AkS^zWv>KBF;s-fNws){?R>WWvZ z%Gc}Dq*h{Y^nDa%;SisHJGRjg)Bv94tu>)F6Y zHnEv4Y-JnU*}+bBf3cfA>}4POIlw^A1yK;BQhT=G9M>0AFuL$g39}eBKszZ?3*mIZ;HsisUrKPiR_y$ zvTugSzL_HXW{K>ZEwXQp$iBHE`{s%4n=i6&fylmvBKsDJ>|3nz_p(H!u~ek7Or)_~ zq_IMzu~MY5N~Ez`q_IY%u~ww9PUU;ni}W^#^frq0e>REqHmiL97LoQ=k@hx`_I8o> z4w3dwk@hZ;_HL2(9+CE5k@h~3_I{D}0g?7Wk@g{x_F<9s5s~&$k@hi>_HmJRtw{TX zNc*Hn`;{y-iAejYNc)*c`?*N_g-H9QNc)vY z`?W~>jY#{gNc)}2{}Jy+&iNp6&PS1RK8c)DCvwhbk#oL?oby%WoNprMd>1+AhsZg< ziJbGh$T@$Aob#v1Ie&?q^S8)3KgIWd0SJWxf980cl)VYOTxWGBSl@p4rS4X@_Nx1; z>OH-^)+asbJ?mM!Y{{}EOWp(mgTM>6V<*H0IR+aH0SD8Loj{vq%$S}KgAHj21jnRF zdvIbmBrFLX5&{h!6BFElpA&+6Tr=mpx1MBXVCFZfS5>#}`Yq=>=l?(Fe4lbO2mgqH zf1k-2xngd6ZhtO^nhU7EhPqYcTZl9*v2vMR(!>u zn6ByRmZ|4WXSi*fP?$i-5*!`tUi#Wge|tUn=%V@N>h9H~*0`ft{U*kQQbx$lCpC@m zH=|WVgzETPeOD9o^bjG;gyC`m)x&8W5n0L|&)o?19kT`88-wn|i@ zRICC;_k?~)F%-upSWwjuY{fDOx|;K)1>Yhj;augEa?S-(RmU?*Q|;;4W*#dWn&Gw2 z*Mxv)aaH4la=pRZc~LM!C|jdqe_IHBDXuoE7N?llp{eP@h)tCWOmaxJA^snvk<+0C zdphd5ksFQN5&n%EUVQO}lk@W@(Raqrq8RO59ly8NLpQAEGTB*#g-NzruNwtvfA~_!swnWGL6hca(2GXBAQ_0He-nKE;@OMm zqD#a2&ibuOCEtb?R|BsW*m1n2-P{uA4PDW_e5Yzzaj*5kZQXu%rknK8o!|T3jvbd> z`3G%BVVYuaO;H@r@vC-TnKHQQb463Udgv5Io`3hlAO7wm4}S0ylRC`ff6SG0Yq`r) z`DsFN7cof7pf5#ZFdBjge*~^C64Ba}0T883c3>#9N5gI^GXd1D<8L1A!yu7I2l6y| zhfw$+l?&-b&>m|j9cdwYi7NbLmyo+0W*T?$d)2%k1|evU(t=;5mZ>s^G_(fg?4FFfNv^-wo-wnOxx2U7vyfpy*2kSXwA_O23k7kWV;1jkbwaDTF3zw zb^IiVns6k5MhOxTe?-A>9fv^=eo4vjBY6=FLO9$Ddh$9rw>KKXK$Uu^I~;+`42D@h zETTRPS~=L0$qCRWe3bQ9I%?NZU*2fglSg`k9(?+uIYF=>3i;Z_xRcIv&4M&yWf3}R1x>&&dGt5E9Itg^KgD+)EgxoPz;HP zpz{M?%6@kZ^%oFpw)>!WeSUTWe|Y?_>!-t$wc5$>fAsG1=QU%#@v+CYb*t6x_|0Cq z+$xvRv0|&$pPE9Sd2n%YVezWP9~w<388XMNcDMDa_mtaZ_^VCzlrn)~Xdkw`pa=ZNmc*As~0GWKiaLFiaGcTS5cltx&3& z3dw`Lg!|1Mq;aIjZ97MHTa2$-#)0 zz-GY7Ts1%gfDM3cp;PT-bD_~#s(*olt`Q2R1~!SRVZ}4FWD&D1^%c;4pi?k`e^P?+ z3DsCLRIXAeqqxQ`=lz1W0`(ABRhce_Rc1pkz>mSG7l0oU7*j!G01zS}VANS(3VPIA z0F#=6O;Fk(mDohiq1NZ15_ ztwbcFWYA0BJ(0e__{ewSk2; z8ouXPb-Q5KI@N;*o89KYgV`5u@9Cb^xA=4T$GHf8SSXRhTh=gGfNdC}J!nb%x#r%^ zufF3QzuMW?XzomQ9ykCWO;mQhcl_WT`TT1N;iV5;8W!ZOSlVX$0pqcpnhSH4+$C^d zkk`?GiJ;|i5sfBI6pD67e~bXCv?r!jG@&v<2xcjSi_|P^ z;R?1^8P4mPuR6NR#QEqOjY|(XPtV2QNYMLYbv-VW3k|QJ&pg07eKC^(<5?g;n(a@Y zw?{cVbHl65f**%kf1qJO9ylcU+R1g(ub1=n{LJy!35K?!e9gf^d>4wW&*+s~s)pgI z7EVrqM%B4T3=qqU^Nz1a4DmWOb-RA3W)l+ z5ORWOL@5hEA4cvhf7(K}iz zRUtnQSR{`2qKBAAH`KkQX#Bgygrd6Tqbmwciod;~lG4Wbs`T*WRBwDEoKq~I)9t(_ zuQ-P?xH4nNjVPukRsCkT$gqCtD79Rh5vAEFOnhTna+RSJxPZgYLx_qTa-!Bdrr z?rC1^{TfBLe-ZH)f9g?7c#2svFByOGl9&ZgmCIIM^}IK-Y&fJJHvWs;42*{NN%m;N zP-{b{OY5JS_pU5Q74~-89Ml*5fWA-K3_uJ|+bHZ#r~rH|AQ%+V(n=PQfT;#45`GP5 zya7;8CN4uMVx4q!EvFzNs$w~uvzrY1&6K~kJ5Bi{e<_jJRFoBO)^fwCa!(Mj2gL$Z zv%Jz)!j`J#OEZ4xx?w)9jsH7WNpr>aTg8t@yiB}$0whwk!5YpSTqxdRS%EseAEaoC z6V1$n4Zv2UtHRF*#Bw@9V1;lqwyl7ujjc<&H!Rw00aL`nIM|zgx(;fLS=AV9(KDD9 zRqAMYf7K9*Fcmn%GO1}JFo~`KhBhuM1kF=}sId!}l%&QBxT)|R3#mrIC9n(Ql3@`0 zwQA_ZT3)ji&977)6(hzGp2|COV86U}!E39i+#U%#1O&=Bszf04V{Z~eJL^L zAh##?YRO}>?tzyN!xvgqHmYO;5`Uqz2jDeqe@j?4K|m{Opi(nDVmmHxI*JYh7>-zt%BVYF&R?OO z*WGdb8p0dHtvlDQsn&l6uw`pZK<}xT^_q4fY?JB4%&ut+_Y#Y}>}D@}3Mk9$>026A ze@e#RUB6}>?YwbA-V~$rhAXQ?r7fxQ7*S1@L{?0>^0ZA9@d!F+w|%1IEEsX;yS&OexZ^At}B{55fg$ zqoN;b1xlilQ&SCHJzi-xD^%ldUW1A`e|hVfmf4zFKClF}Sgf3j)KtJ%7sv>U!Sn=z{KM2p;zaF}wRR^MD zklBdAz##dhiVbA`FBSg7Q-(@e1E^l0JJ z#~$8x47~}Hy#rP+uG|tH zy6&|*O1<-^hF8wJQ=cyW!PJRkJ1b`Y@chCBNq*<$M~hXx9juHm`RSg2CA;_EdS~~= z($AWH?Sj+SUe+C-UO#>P;c)hPMvm2+B0_5%M>6`6Dc&nfHLwWopn)K|jeH zF+96i!jc6^?qLM4p^3*q00Tl7GsRc5ML+SHdNgjMFgC!53R8VCU61Ss5xlM4N1|484gJsqM5S2e~vuE0#4~CnHw}c zE_bEm6=9rhI}ZfJGn8+$Zz_R)=AngDPnl6 z*ajhq%1N2IYQm{5h)pRprfQf~$H)V5WBN3xFKojJ>zZIT0ZT(2CTs%^GeZLd@{4W) zSOsn(9MBlQ=q4OUf1E+Ho4^mE|I7UGz-!K>pXljda)k-ZmCGwM&Q$&X_FT?@tTMO~ zo@-Rg|1XbCZvtm(3b%glCQXqRUP@P1c(b^5vrzDP4ttJ)zpUhTK`;OR;x9oG^rZi4 zkc|hCxii8YvrcF4jjxKm(r3#<49zVsT77z#3YrEj;v-c0&CPuWvG`bMcB4v z^k)ZJMcpa}t1*j40UX19uo#F;w{BhE-QPXmY7|0U!^NmrZ*74c=e@Z-gXd#yR*UCi z;@JN)e*~4K3XO&=X!z<4Gqz(YAS>pU$k8=Usis+&?q6G7QD#D?kG%nn5ea4c* ztA@6=<2+icz5PhHdt|@MR81KE!M&w|ZAO)S!YNG^ZE?d1qU5W#em1@X{rY@P%jKc& zKA<-jFb2?qe!4Zp-~Pbwe&|#0KG<%Zd1!rjf9Wyw;_YumFKkq=*#436XOEo2hGkNM zJII#IrJ6Kp!blGM8iLUyd7cb>wUY*tI&|P7QhM{Nm6BetK5ks81mF7E>G9WfUkSw? ztg7Q5qT|orqH}Z|?!4*Ms-Ya`f8rX|;9IBBUjeb&gU0n+bT>e)@o%4dJ_VcC0;F|W ze`fcx7Z|HyFVm?ha5_sg0boR+tCQ5YC1=l;`~f;UN@FMKvD6r32S8Z>_FLT6H9|v} zjA~7R9EN+SfCM+Mx7se@J!sB~(zmD#prc^{WX9S)LiHr??d>?zVl$^VQl+shDrN z<=C*s|5T2FzC^?f9KgD96Hxk8tz4?Qoz_-@H}JCVvH_3}XDLW4G;fhT;bw@FpA)@`#*_p?Zy z;gcvbZ=)yI(|`Nq$HB=KYQuXycEY@86@>~Wc=li4^lJK$haY2s8G#15E zhrg`&Kz@?hU{9GkRyb$oe|uX^gGOOs7?dp!msW?D@K&X);8z4%g$0_Uwm0%KomwS7 z?5*uw8YZ4I-MTo!v|8RX(Q#w^RnYjBjH3o33OY;-KrjcV5Bf3WuLN zap#>UK1bk-+fwD>U>aN!3ROf7EgX+MoueT{kIj zSC`tCbo$AhV|n@M_LVJwXZvQHh=|9W0e-YpaqCn0dbPFXP=7Ye6T_aG-+$iG1Bdoq zYbSF{EsL^pp z4fMkJyLjz}JH}st?^lk0a{GJU^Pcfv+=c3IyL0>^`ri1vfA{{z|5-=1m%fO0BJ0EN zx%WM2>1}TtfA`LF_G2FZ5YUpF3v#(p2FQa{owW(zKNpbTx=#PFW_`s3P{{9H5K<#CuPb!B& zN{};ui!37yqy-}Na^@Kb!d}|{=&l>dO{+oWwz#M(iD-Lj>34xZ8Qs!~g&7C9VGK-! zZ&BOorRZB5yKAI9_w>wcXl=FD%2adSiWpbQcA_9mf0DJDx6ecPBXG&W%l$!>w0GpF zLUL4*QP4s`B!X`t_0GFbodOg6qr30M9@5@^*W2HYj8jis1)F!j{qA%6V4m!d=np89 z08BONjL-|GzWIUn7r)qk@UO}CCuuDu&gkr(5ie(A4Om+b!U%+z(AeeX+Ox(u5FU%K(O3w8ag|BBw! z)VMjm;hh8n>m+YSZyLW1Z!w8+&R6iO#LL~E`*`k;a?j+xo_cXbGVB0AWzTD zk5YGOC#>OK=Vci(!%kbu>N*-Qo`M&!#Zfq%1XWY`GL$h?d3!Fho6n|t7fzqV6lBqc znVDshH#McvvKT=uwyQK)2&u9F$k0FvrHGq5s;>@pu4|3FT3eVm4MnF6 z8eVx%B+G`wLr2}f1|{FymG`<+ZDTHS)m@=1nfOjND~T|Zd0O4 zeyJF^x@-gF6ew`oEo%27&*K(XBhfrqp{!gjAYLgg&l+Lh%)j!y+Ey$8&?pKf4%HQl zlfryZV$8B^$1cx?b2{K&t^j@kT<16p1B_F?hS4jbv6Lxu%Yrz-DU$!VHKz%Ue~DvG z$qC#-;oNxnD!D7oBWi#SPXN%l!#25VeDi!`d<}UV&k?8HnEyY%gHJZ*&-o!;{0!LK zR&HPJ0=P1nV6z$u()5mXl%}Cch$t~`XRwF`>kda=PrBXG{nB65PsKbDDNvED3y2WD z@3cWE0t989^0;oDCa<$}Lw&ble;NYu^4#qB-v>4Qo!&i$%aqbHWm5$qBF?)#&ExTz zA8zL>=#jGYc{4z2s>+34|H1POoy$#tCxff6zHhFEj*UNCZK#gpa5xbdjn?W$ItZ0> z-@W&qZ5N*ub0r;4 zyJorkqe|rmAe?~34Ca;1-^W5l&#MXti2nhEw^9Z`4KwzeoGZmr-YFF|_;pWLhVH$9 z)$>k($$)UUCrRUZiVrt^f4*G)N0kT!r+P*E_nBf$+M!Rj?ATMnX0tz2(ZHhS*&4DCi2uQgt7y{1Mr+Ei1nak z5zcg~Nn)gn*dzSFeyF0S+P1-f)B*2epfikxsn{_&DTW2dDccMYe;%t^y-;>cwcXm+ zoT6mcO}j6;@W2~F-}kRMV3-6n#no`7vp8Hc48;O?(!`Wv(;)dgFesowm>Bg2RHIbo zA#Ze~|0pI%W4utNpt9_W4^6mT2y8{&UAvF%z4c zu-kZIx&2&G#>-k>f8vp3S&9=Qy-vF+N~pIi-M`FBr2RoJyzVbv_qzAiFy+Yk*?ZTp zga0V|IsWJD=ZDelJFsM8AjbSl8|KaIWAMUfO!nsGPer|bXS^o=tvo( zSR-LTl1`dzx1P#VU;0sD&`h!shXc59lm_Echsr25MDjZg_5>m55kHK&GW|70r%<3o z>Xyx9&S=}ebz9ekqQ9Y>f47BVP6NnMOBG^R2K-%Zm@p`K zo?#dx3brhh2Mj)g6RN^qH&DM!0Z@eCPN?PloN(^S2Q(H zOP(QA)sjwOu42$sFn$&blog7HjxN;{&1>h)fEd8KZHF<1slb$F6b)0Y%9x{4iz?jY zoKm*1f4+Z}5E#e`ea_)tNP=X+@+nK+X;(_BXcwkjMcDc6CI@=~w=l708I}#!z|<9) zS!7WYfl81?R+!93;0BC$tTHHnQ8tUMH^JJzKWhsW&SMgRU9S-pGsoZtpnyd<;Uhr7 z!6-jgEEIiL6{7BYB|%nH)64@*Tei$jzRG|df5r=9O*IV9Fw`38T0%%(W0X+Pkvwk# zs1Slu(&Va!L1!~zLZQ2yDWD=u10yJv26l&HBTr;T55nUTVT_rUPAkh+V8r=-od@31&>lqR6l0Nw zN+$l%$MAbV7e=`xcS-KGxnBbd+XO|ie~P*=6cX_ZwgcfOtu&68q0m4Mz;%=bnc#%r ztZOZ$s;N$hCgvr}=FPw|s1s?QQ%wX)<8n~wh<=bD1>^6h+kZ|FyiTWCn^m%&ex1f1to??#X|@wmE}XbddrHkuyo=1-NdEu+Br9^=!=EzGY76!fAqE4 z^A!6k#0T|ti${g2sR6EH17m|s%P*bG5FHbZTn<(n3+#QGcz>_0$6kVaFn){Bd}`N)IS(O2=pf;yQ10*ZbSrQmM4E7K`kVd~KytiiLQ%(OH)`uuDi5yb>3 zCJM!x=UZ2T@LmL}h`thd0lSDWdZp`nwPHcoluJRc&)TMC+m?Aetk>roe+}Rh>G!ol z*DYOt1g>~hdMQJko`Snwyzl($t~-C<_>)L;eOE(4HFzwjhG8|pFR4ya0<}d9?wHMb zEq}kt=rnIp#%POAQ>NY@mg-H@5jug>YBfhKmN72>iG1LEee$V_9@jM96Y9I*5*VTF zHWUCD$F$PHF|yFvBC=ORu~U6dkQ}Iu*({zXjc~ysfgado-Bd^ch?WTx2?TD0h=&B56c* zQpe0gJZikm`lpdqe*hGlVdx;+E1P%ol4hqnOp}>bQLER@W|yQX(9nbuGh|vPy*J<* z^nI0RH30<`pnVy;JZiZjT4i#a=yGVxQ)kqdGgpr^0Qoz7-kblQ@;$;_!N)44Njr;g@SSy(8ecg>U@$q*Mv zu1p~X#XEwQe@uPPGT85RYp6T&By0*?*#$vDMu{H`NXj2Op5zvtK_ehwQv}m^XcfDm zO78*F-|A11d$1r;w_pZz^I)XVd!Tuz`Yrf2gJsKb#&s~mpM^6_@>x|T5?c}ZG`Zrl zB(ee-YE1UAEz5ZW&Dp~p`=#9%ayZ}z;42|4R)B{?#m`$+C#XwA$i zKXI`FL+T9Llh9H>S)Mptq-dt;1nImekZrk7GO!U4ss#8I53=ManMjb*E&=;f0GsAf zr9qZ7e_24nV418gGul(Rm+`A^kVRCo=)h)LD4ZgZF(_)$!(SjG&`eD;0diT6UrW=# z43QTqu_He!298H{kZD~9m?sd642#ON3<7cp^acTMk41SN`;!Q**aEHzpB1#(^$AV!Z7h3Gzj9MhwKO%>wBFgRq4C}0k$OfOYlL*dgeSbS{yQ`5v<8+H#gvXApXFR*k=jTsr+Ub3~4#LD&H++|5W(ntPb~ z@v=x*FM`37UW0D|Yv||;dII33ZEFNm^VdEOZ5Gy>D|>e6GDHKoTodFopE1O7%o#9W z%5`JW&=jE>mNL+&Wj+WMj*nh=!C_R_e`58N%ZNjWscJl4jCJ3j4fOHgMELy*1HPRH zIB0r>Tc8aa>piE|-VQ^CD2}JunmI6ZMpeV%vm2HIyFDRAAg z<~yc{cnXmjgr9uXwd6nVyo_A%BqvW{{1idWcfRx#Mo+`L@s8%=Q%_=~!uz%3M-YPV zjmyu)Kb|4^+!eXs$mW+wXOE1)gOWV>X zTsrvB9XsWG1n4V*8wCTToXAW%s!(!lqt=bRR;dx@f#Ml17!;y{g=qRk3EYXyWv}Tl zdI1UTu2FFnEdX8*Ob-J|@VH~x7Su$}X2KkX2}QVoz^0RH9spq>C@+X&e+d<98n7-X zJ^sA&(+1+TPZxB>RKeCL4&jU|x>bp+Q>qH6hjA_xV1U5%6k`Fa;qG!CK(nqpuCDM2 z4|p3n4Wq@#IWSTxxpo#;?rg$B_VORm%YUF(-**1CY zpS`Bl>+SFLzV>IYndxp6`qD*ro=?IPq4d?le-|NB_fDnn@h;+4 ztOv;(uO|7u`C6|;W-q-Q|Brp-afJ4fiJg{tv{9}I?O0D|babGci471u0d#Mpptp`> z-ozpru}&l+(TX}mTU#UVBWKRQ$Jwo`9(`_nX!{Qzee{Ru4_2Q;Z~XbA>DE1uu0HqN z>Qj$y|KSg}Z^mu#e`8?NF!W%O z$M3o4arDCH-}9c&WAD);XU-gXlstO$%$cK)PGls+UxQYi&TY*dl{R}I4J}U{S`)h( zHM6u2sR`EQ?5^R6MY$8Zzrh#;2^D@$g$!2xoXE-f6>qH&Ce(4uZh-NjT29M)eR~&xJM4I-gbqB zKWc)h)z(RXqS@DJHm?+%nAo1CyEfJCs7>!}96ETvT?|we3|)I{LP+baFA@AG(Fo?4buaV!|_TCYH)cX2NnDi;AuwA13Ay3Oc(iK zOQw=F+>Yo7XqBGZ!0(S5jm1V|{Kx3q;}!I&8+ZTa&8R!xwH@7ozEXz+i}l|hpFwv9 z<4@di)6@9cX_9!sw?|nTl2cdw;!qd ze@r{N^9tyerclR!(@Ji;_qL>k_io=IzfS?oLQi9YVne&vi{qE-vvv4;S*#rNN1R*A z?T{lZjVw&k)zdl;GaHK1tYJCTB$BiO>}wcgt0G`I3t$02p|{xd;{C5awr}s2S+r%# z-hCgFTjMXK_4@U_N7e!REhL7B_uRTKe_5SE&24+-=PJHEziY`H&Td|jUVH4o_%YOK z-uhd=z8B4TYIBLMY~6S3ZuIuaRonLNNG3XIfoui-`4-t{P}1RK61{vS%^(Yg1X++| zasYN`@m*>BxkyqBDDmNDkT=EnG9I5M@yIYNAjIK7PEk!~ORuHNAV{z-N$Dgie=B#9 zd6j`Y12N%LXq*Yn19%8{Tcw&SBFip1rtcIDOsF-mlss3dO-Q{|tkICQi_Pj>t(fNq z*lx-(pDJqkL3_pEWcKhumujMbbVWDS@`9j=O8Qly3y*iEr56_6wz%qPj_tL0$8Q#O z#yv2A`iyP>4pkeC=6Weq1psRif1i$xg0L(=7A2XBhKX)Z)t0?kK<)5Q>KNwngC0{f zLy`!sLd|@23fl_BT3j(UG-d+<5rq0sud}?WK^i$f#Sy9G_}N^aVY*gEr|pFn?(lYJ zBn4-<1NG1gNA~QRo|&26wdbMDjUus|bA`$vKc&~bxV%p4KX*9Y7(Y^2e@I-6qj14; z8jU|aiy{97e+ID3N}8|br*nCM^rngNlUYRlbX^4*_7Ir}q-9JeU1a&f=;~urjSYKB zSK;B7`{6wBplY?8oWAhjxpU`Y|k91)MFn%_WXrOov~f2o!`>cgc7tP zW9owLy0@DE`3;)7>UhIWf47zZ_sPlJJ!q%hLr>#xe)ko$YI741V;p5GDEM_}`0l6!VHFr=X;y$JxNhKo22?PL#4&P^BagDB&%f1Alt(IlQ4Oy+pQ z->92@_cHg^aI`E_;i0+ZT$ZT24BfsQX85g@hL}1_*#k26o!)dgO>~iI6kx|B9ozID z*HAVthv#ymHm6o1o)sbencgLBur%K%YNsg~l3=IF{L*F8&PIBcuQABREd2TM4kMXu zG6K6O%L2Bs!6ve7yQsE2Pypm6Iz zm#7lCSh99_QCHI_2H_B{3OWKS9En-Q4ntnBxS^WT3OW^r^FpDrJ+WP4oEo~1@4$8} zhzLE~kQ0?TFcl1A)gbKcybuV-X)B&@2*ey4k>bpGT3DQ$e={N#!!VVr93U1e4p24? z42sx8iA@zF4d$pj+&sl0VskSQ*j}*k!jfr~7(00kl*2PZ;P&H(EQ&OlCITCSdUq(zZnITFrJXm4UiICLjq!35#sqZG>Kww|okP4`gcLvgR6htI}hp z(1EOrqR+`$f7_C{Fh!LU*nuG_L8Yo%1Tj@%Bv?vhtOfxgQ_N*J3!Bi$3?r@~feafs zS?di5o4lY@;W-G0wy=ntv}YcHn^7U1M1cP@+OW`59sPz|@%~ zagIO~o@z22DvTptK*e82 zI{ugs_u%4J3bvyOu9hyJSxHvl=AxunF(Fn-bzDqUm37K0z^A-Tq3vd&yoLzsZlgb= z(Gr}#heEvxlpZK3HLSYK941I$`4n8TjOZ)n5NM!MNq< z8!lfcv};BYgqxWhhN%#qXJX5?RRO~fp>1>?7tBrwnnXp09K^D_vcx287Wg#lmZ~US zMS>|hi~^N48Ud+%kQL+hj0-2jZD5Eaf25>pjNmpa4OxLX8cvaiT<-uy?F54y;}Qt& zMW94*QHL?B$OTy#U4fGf;g-?(9r&t_3%Ze$*&v!m1F(mCBvTFpQ% zu@V5^7)Yoi7cNO>on}i(4J7!H*w)8f==jt++hgW*B8G15dz_F@TsWN2ihi3db zF~H7%kZWQi^a$NNO8mfxyDK0Epn`#k*m|HQp680Ch1EFbhOKZjueD)^f8zKpJ#QMe z&f<9WWV4kl@4!f76TcwGV#ok%y1P1U)dm^}z>G=)Cxwcb_dcn36Q_2!P*6 z#SIm^=zA!SoR_|h^5f^_8#lj~eT~0~rpMnIk8yM2&v}AenY%mprQA2*xxhh_G$|;Q zlW(#Z+1d4VK*tUGz&QtLnBB^vVx7(0dwCiFWjQN5`|C>ym|!}A@1&OwhrKl!%mDSA zspU}{@1y&Ni^TOWtH!i!YR1m_ieJcwfudHlSa(AL_&`{Umb}FR3~5g@S$+*HI}^-N z8S`cIz-C0H(pc(De=|Tafms5Tr*X>%vIQ-#Y8p{PO{U#&Y;hGd2*qk#FJ5N?w2^+? zh*(S|6zc#SG$g}EU{4vNbh=vDCQ}ercQg13`9ZLI^ zD+?+uPtVVE!NBK(T}itP?=GL#nUBc zrvossHEitI(~#IIHUm!6>D|}>fTh>~^c3mN^>wN+j{^Kq&FK8`lLqJ&%L4MIrVHIs zQ!oVe(lky~jls3r;R8KOrd4$J9qAH<+1u?$fpRW=mHP}h=R;)&jPCJ?FL(<0|DoKS zxew$%k$WQdf8%uCSRhyQDS)4&m6dI(ff`K|op}BziS1ISUqyU>dUZd!tO% zWOI$8PPoZ7WRjv3-O(COmDGf$r)eY7S${U{7JBVO&PvYKG?QV8?qJkU9~Eupk%(Y2 z)mG-v%N)mv5(KuN<}=!;Ia#(47AZyr>ob4<$ZFZ?R<=F)2 zxx-C>Q{@__9>Jp0nl6^XK)y=?OjV8ZCBRl7z3@{voOzkLWrF|^HSek<7I|iZ9Ry_y zWZ&DDe_yQ0HIU>3RVaiIRG4KhbD!iYb%X)D|A3MxukHl|d9q{-P(DCcUO!(UV8|4r zG}M}RXwiThv-#z!341lwjKNqlYhbzi-0)~f3`6Ly0j4|;wU3LuPDyETA%Gjo$v?y} ztCVU`h!sHS3KMa8+HaxDq}@=X>Q-quWwRNif0{20>>}70MG0#%ctT{#ISDMeVv-8O zk&j=wA^8rGMxBHzJiHca;ls6fv5tr#11~T_Mab`u_zU82C*d&9vtG=S~y4%C-9e?S$~+m^<@fSvZ_0)QhpGOS4+Y$FieBy}q$ z!>)`!%LV@Y>?)tn;x_E)r9r!Wx@rT=l>1{-Ewi4}x zggkKl1RgFgOlH!vyCIVMy8TEtsJsU52+$|0$c3Q9pd%~T9)V_vhPBKY)lF{*8ie)W z6uFQo)VSFnprGH5dYvYet7{{9Ki(8kf`0}<=!>kZy-;-ESYXQ)kVJpj1BE4Hr+m~O z1cQzr48*A46+>U7pB=wB5+nH$33-Ctw9yDoOFGSjp;53jke4j=lAf6SNVmK2k=`=h zlGpA)OFy?y-Xsu-422DBB==<7qPv{*){(y~;K_2%%4iwhgC#f=tfM6Xzu-$AJ%9TJ zXTcrdvd&~jho|4zUmgUb<@AKJXJhmNPF))*Gf+K&Q{O#8O;8vKp^6YHP4f||gs5<2 z4kOc3#X(S8{3aNX?T=FSg!0Sh@OuE6MxKy>egGZ-+S4S3WS7=`jJtTe_O2X_O3X1n(h^eC>Lxqn4I4WSJ( z2e89DG5$R$)}xqKr!cx8%@4-t9@#1(ln%LFv{fd;!VRUVQPL%Aq!v9!(7ar-YW(!^ zqetMXiw{Y{bb-8lKZTwld*SDKYx4JR-65A3%I5qa^m_Rr=W7rW|jB$Zq|plm)rk5`TRXu7Fe3FL1)Y zD7*Oq{(fn1MJStt#*#7*pn~IvgnZ8U_Bsfh0`UJXkOvf!2LWEA$JenEL9oi>zUdJ^;6nCAF8A1%t{t;Xq z$mCx~UX)%bU4%rgo__*nD-(bvF8kuwVe3+8JN*6n_?yRN#~eF)BozX5a9`S4=_lYN zXtuSd2n)yPZ9>4 zPTooSa>bbi2@ETU^u`DGW4641y1!>X;`7x~3m3+Jhmw7U)_>Vi;p)YM{Rr)6qEaV# zw!H6U@hUr4kg@hwGmF8c!5rU0UN$qL-|oqj|15nIJ-z?T{sU_}FWh%qGPVCqr?IrK zYiaRJV-YVTjfLgM_n-Og+mkj%`**D0cV<8O%cYIwm zf!43S^TrEqynplRPZKj98)$gjZR0;jFT_tyxZ{`<(;54&47|f-laL6$`;Aw8di=9D z7^8AGXh(}A8vprKpFs}VuD0{_*lKKY&-anT=`2w$&Mx}XT+E2yurtV3M3br2S=yMK z&;{HNy=eGN({mlguqqli?uwz`k1Pon?E^^pUH)}Vy>E-O(QOwi!Ju#G7;&z zm8)owPL59}$Ty^qY=Q$kX}V?p96*5p_X>?h!KsQxZIqOk%JBE^{@o*si*t*M=%(@I z*9->eqko-kD{9#rzjv<=cEibMtmKQ(t^zSWx@tfCPi?>eyxV_)mFrMzp$Z1-C6h2Di~w|+h;7B;1`As;=gp>4Q!Iw{P*WHNd!*K7)Fp{+ zRbeV4X1i#qj+z&~8?Je}Nf{HRbNwV>oJ{39xz*gh-0LN)ld+A8j3*6Jh?lt8dW~sb zoPXPw9Ig>{0&lY3n_Mp|TM9RstbH$6KS3fLRI_p=iS#D2_g7`CN5U_7!T?vueME8G zo4xS44{;KjhwOsPg2eo!~9Jz{eKI)Z%ERb>9B=_ zvzDKcj?+A!FB|x6w{0mzIE?1bY)LqyD0vKr>M{c)8T2~8Y#Y!pkOL5qu7BkHZG1{5;Eum3Khd3mVjxW^1i}AfhjR8yKLQaETlzxVyk^N48u_Wl7`NqTOmY2RWJ#+NouN*u2Fqo?L^nc9U^syJm zpPHG$cfU9{^U`15dh5dv&waUjyXIn46|kOg$K5xZTgLint?TU#Z6bts6o6?@YKaCZp0^z z*+zd@-(rlvm7uoKtk2BU5`TOAH_dat(``iL-kbYV>CtJq{mqOBpkaQJrnLCsI@(l+ z-CDK+ceYqUZ*(@)8*MK8l1(j6L#9=^#AuotBNz6R^N-Wj3nV8E{fWCen{JwVXS&kS z7;P%rUU#s72Cb1?+BloGnmVOdmy& zyrI%@lfo&LM$Hkf^nVTsl(%(emo4eJluLXP#?c4hITUMU!&Cu5d8!PhB1Mw{(bz22 zO+&G?ya2ua68dOt#sTmffY|1=Fzyvd5(b8;$>1NCOWea-&bHUNqk!qrUC?qWKv_F3 zXBpZOTxzmDCRSNqlYmdf6~MpnFvD{Q4^>gnh>RDfpj^7RV1N7=LSdJak}Cs;R6{EE zpt!3VmZ>f}k^V}+fK>jsBLU+Lx}X?ta~`x7XEgUwWOH zH}jq}qmf3MP0N4=gd_wKvV}z?4A`tj2uvg!q*yFA7I82HPz(gHNsuKYtXQ@j^Ftwl zIF=l241Dl?*njy*qF(^{H%-pD)%~8p&iDKJz22&;Zr$bFv)p^m|Jd3PDlKf@C<%ht zJ@p8Bomk9B3_KO#B83k28IxXEm|}%F%u+gC4dz`J^okAOTVwQfYoU>!;5x!E7-eJz zdyi;k@%~HNMufNhCw**kTXSu#Ngimf(bdU&nwy~2ZGW79@(HH?|NM#RgPCj1$sOv< z`i&t)rV?8V=9Ef3NqgVKOYk6RVi7mEO;;0L^DmI8wN@a9!b+LSfui;u06j zf!O=8%aD$N)PX9M-}!Kd4F~JAJM62)#?Gt-Z1$l8j07E4aG!6`5<{4Ny;uy@CJ)So z@0<#1rhjSK(yWH3g88!VId(mu4_T&J3r_isvgeoQgHvJEl=g*Rt9G(OooqgTBOiYy z?VK1MdCSY+ww+$x`rvKH#&cD>#bRu^v`9eQbZ<#flYTzu`VhmO2_b@_OioEyD- zY4y~CSw8;4mmfKF>$MkGtj_6`?n|xic9tE^CVyX9+|Ih&UF+~=mmYdkK?FHu5-Br z9jw&0N7hyyssb-mIDbnW?!d8e?)?Eq2Ji^fUG&X%KX*~d@xr2Bzm0n(gFJ2{M`wBl zCVzRQC6iUEb$s2^L(R~&7<73{Gl7s9XlOEBhe^^p+A2p}m`vBWU_M)D&Ntu8D<0bhs8C=-am!=loXLUOaQEW0>N;``g=5>H-rErRUJH@IYc9CW#66 za%=wPB}UQ1%^O`?eTh-sJiC0pd2(sbPd?8hGFP}lA+Hac0Ww33$;FjgrOxHDRl#m` zl^^gFoixY0Q@b^3e=k9Vu9n)G!)?K&13$(I{x--xf7_BzJ{Pvq15|4|2g}(irHfzE zy9j#8nNtcwpK7THc_pQ+URhiM)-LM~SS!#NPr9a_O@3_0m2!EW>j5M7g)ejYU7gW8 zOr0!&u+TOZ77w0S@{+QkWt1M8X$3AwF1uBU>jF3bTOgzKe?(x~CXKougt2=irH*H{ z(y30fOrI{ir|?kWcXPB?wPBRbpz5YGD5YXGnmHwDvE26>B0iWMByI7G=kPDn1&$s#e74fy6gQir@(@A zhWn^Y)1?+VhAFA!CV6W^I-F^y6x@_PgMly>Qc8r5K?sS=v|z~X^4cXQE~(jE1ctS| zFxMDJ9dYI`2A)RJGeN^z-@MeYbwmiUbiKCrBGc1}&NX6!WkW}EphDS!amJI8P!)f6 z7|f7i8UkR6&~k(QylzOSik!-sfJG{r>7v5rh0`kjRQY@=WO)H%K-sK{C!s?#Mn$UB zNW(U+8X-+6>tl8@ujawk9f z*dvcjz7tfqS8qgMvXYm3sihaIbKZa4LA%hc12OdvUYom~$=Lx!#+r7IiE2qPfZHTo;O%yZ-t;ef}+U;cxy5qi@Kuzx+#f`cIF-UtyZZ$7kb09ki(fg^LR}7XC@$7nPqhaD;z>3LVXH zQM+vsq`mQI7A?iH{rQ5VJf5wpiCGtIV0NC|tG$n0C!X^Cet>f@;(N1BO|b4ixSqZ) z^5V3SnzC4`M4@XK@!ICvaK5wEeSJda=0}yZY?#Hc-Mr|K7ss_Y24RdLzCVr(Gi@eG z5FP9G$U=R%y17<#!*I^CE^2?a!=h=F)5@@3p*LWhcN|wVgvHS5M=v4Ce42V@s2>9! zL$nLeu}XD$b8zI~+5XbxPuh2^Y+0J`H=4@}t$Oue6l+EJZ=k&`^lu#31Jh5EdU`xr z?4LPwWU#qhEmH@Vb|)WmtM%5xasgRDPhsuiO>F&>FfWG9uy<}0_Sf66`yl*Yv7UId1`WUK~OQd5mXHFO=$rS znA{-~`G}PCR=qK$mw$f@vsYnY&YtNM`?53rjuc4zZ+(k6Pd+*M!k54Nh6f+~!#^bF zZoYYPD;(_Y?vnC0SvpL1x1T(`v%S4dhTD&At`trb&J=D^an7&bTk+(r z+{t%8f;^o;?4z%VjtED}W3C;m7vSmml3~85bexRFyo-`hj9Y&Q&#$5v&>IHxQ;hM4 zOy3J}n|+{P1h!VJHI`nu*r*i)$8s!Bm{bemwVhYpa(GaRG_4hclXpbL#fn9Kv9i=D z)jD&PP82Le)l;2A+nrNY^Q$)K>*>v{v1K1U-Rqq`YCD&m-H?a%V&q$u$*1Zob@+cW ze?#C9&kaq-F++dXbD~mtp|#REw0^LEeD~na=*ZTQW7jOrZM&x1>`cDDxX`>P3`e6N zx~S<#OKWqHy}fNmb0uz=rjaZxHacB<`}DT6xR7;&qgRgpyXm$%&DSUO`s)0AlD|!H zPk)Dg5h$%*I8?Z{@S?(p3LhzavhbP0?-#yUcml@YAQ68c{*((x+RCFPX30C+qimdq z$KZDAG^7_&pN&Y+K)o^t!iF}u?Jbys~$Evc%U3Fx&c5|t<-9=fEy2gixL16a@w z?d37m$c1JL75IWBbh2K$XAEv0B>P_mM@U>8ZpOnA{nAOx2%Xkg76T{G`MddoV=Ta1G ztkn!oouURhDEjLX4R?eC5jHWFxeoGqZPBsfa%?$^YZzF{Gl4sx=!>K&biT>Q38{wV z@y9@5=#MC~(YJ}}9`4j=TCg#yYs6dRG?6+;>}Aic z&5_PUp6((DjdxLpRH}}@01BDV!2q#!?UXknmNip*%LW>W(3fVh==imH@+(Q1~QOaIMe)QNYUh&wutKq@k{e@eeBihRz zd+rr)d&NhunOIlP?8zPa*pw#~3pJGUX}{ASx5x0VcNJ8}xZCLpOzAKh(39Wje`E5u z@bW4A@YU8^gm_Eqc6t@O+zwyJdp3U&{x*N$V-bJ2XL`I?*weQOGYV^kF<7ww1n;JN z0#*=xK_LY6dq-4Um~xKTL%c~8=+z9f3j|)OdX*qW^7uX}F=Z7x!zTIsels|ncDEY3g zPQA0Se=2mBqt-Dm&>sRH-zXd`oC0ogGxXdW3-^Gg{#%8ARrq`!+5HnqC~Bmco0NHm zSVXTJju9RNX8ahZe>eXOJu{3kyKaA4>~Hn3|Aq>EnhWLGsFwQ%-C@@5wi7&#pkhfK zcP>WVq4I`AABjOW*jyp1IIv`gT$MbY-eH{MW1yd8Hy#2XfxE$-`lCU&PyfE!9}bUn zH|L~l%>^J&33EVW7TlS8L3X6VGY9K48J_S+ zx-QqFSEnxSa<>?wv1m+-bZUwX_#LzVdCWY=7>1tO8jOn6L!BQ4-51x;9C}0($~dS; z=<=r`(ZQ^J|6E#<=6q|-sT4=+!`%Z_za7-la+MJnrbJo6IF9Sw7oylK?Ubn{pFM=O76F~xfC_HZp} zF9Ho?q(q+DqfT>GwTn>Zvsg6+an+xy7wU@6NCo?i_plX{R7Vd)Wme{t3`OZPZ`*u* zT-|B5WWKMvfH1^eHTV#<70IdAiKHXOS!WESJXS|D`e_bI>RfRlMcibsx>+MUi^}C9 zP5e2(g>G|1h?Q!&9HxI@!1bz)dO8O>6s`8HC`lr_95z0ugUaGaAVuaiG;3HbJGO8o zW74%vu;+O6-bhvt)RV+7d*rR448maBcDdfC?vWU%aeq3q~_l5!TCfgz)6RQ4J=%)EL~jeX#rzAW)1ljdJ@tLmi1 zG!)#GMN!W9@gNL@x0O_Uu&$ZVB?s+~42`NGO3sf9@NPwG!mS2FarmfNeA!Xy)cQ`x z0rh8wgTU56z9A~x0$O%K6EjUQ~?O#pb2$jh$C+4p!LOh5MepJZeY1-r5#f>m57x{nf>BvS(AIpx@htRh(z;etPpx zP!z68yoQUaRG~ixgW(Q?ImFWDfn&L6d~5P4LS$?ko}8aEy(C60M+U{fYSMQ#4zwBv zF2NJ8n_Pc9{S{q8)cIzOT-~(8Fpy*~Zx4M6bpPX+m7tSP?x|0CzE72@jAFSy*qhp= zLlv*9P^6}^1Vj*N_EexG*#l8VwlG8q9iw9+efS)(CK%Av3=qxNkwGNAlB0p{1KOd3 z*tATj6gm{9N8M0twu^DI*q!5tU;GW}t@;-p0?X1Z z&0lf0MmaSEy23!cD$T&N94wVdEXV>H1-qEnc(q#J^&7G5M4>Rin#Gvx@=h)Dijp&Z zj`4#V`f z5EFk<21kR|y{#y#1B0%0F{ll=dK9MfD=S+=5Hl0zjX=PZ;rYIQdZ|SGV_c;Kt7i^G zkxvZf4DFh|)mWZiuQnV_*|b1cL$*q;Cm$kTCI2`15hh}!MOvd64;-ic9g6YsAi_cW z5x8L(gHF_+XQ&IrMU>WgG$}%R6yet@*$aPmMURYp)G4N-gT@2<24#xJ^ArPNyKTYy z82F+vZb1iA;I_GUY+D6==bu|DPORM;s*rw9ZP{eQ?f|8Io~I5}2a2Ippr^Ku5EZN1 z9<;lyOks|}b$j4uLpYob-~`NF-XEcBIW86M&~YC(Ea9f3ZH%@+#8lk(?1RNL1WkVs zaq#LPfyJF~T)Xu4d~OJ{fp^rMXF zkpekjylSF2!DaRcB#40!J7uugY|Mixq^Xse4JYa$rZJ?rw1_cmno8E%|C~Rl_D2r^}*Q?Bqwz{0$biK*IYSpdKK5zhS(?>W= zY+E?gEZZ@NJSzcF&i#x{enX6r|9{S~M{`J5@~#}^b@GgX zZ`OOwz%r%t6@fpR^lu_`qZHG=z@|@q2cb>C8sVb}tkmHuU!`V&z6;8Q&2&R~lVZMj zOHc#DM={8OCAc2WLGa?Hu}-R{2IBxD9{pJ1raad%@B-Yo?Tr<`DFWZZ9oy+D z&D4fP7Ts1hU0d-MW!)(}HIGaY*SeU35YxOXKm(OOw4e01$lDt+M{jY50Y47|F9x&5 z7G=LI9Dm(5p@)U^Ew{R;JHjr82JU_sy7X(iF^FX7ZkO3k#C6?vKuQ7i>isrOs}Jh| z*EO8pVB5y%xohh}IH4PtO-tC34=e$uivZO{n1-vn-kjlLKWZ{`A_*$AgFADC+OpQ} zQ8v|8zQxkQOAEK>IW6;DB;=~QGp%D6b7al7NPovuqhx-DI0k>y&Cw~=-^?0TE{Pz~ z)HUd8HJl#J3!(k6GY&m#xBzB6kE&*5%Oo%;&L3@gzAR&5drK#7I=&<7e4%0&$Jv<` zMCKzZPPehOQeW6KjD`8Nb1&E^O9zzJvc0Wo)OI!&fehcdJRj)P_u8=qUGQgC+FDv& zSbxzqp@qv$vnUK(BIXY@=jO|vznUy8@3u-Xc|b|XE^3rQp_Lq_@TQxV;RT20+A(mW zVWsJmqXnmI_7qlh#Xvx5c2znbq^`@1Mi}|~`9g*Tbg~7((!?m%e$&k}%BpUHKEyB- z6dTPXo}MsD*_uu#H!r0zd2{FWA3AmUD}P+SQhCkUi{EiaON4(G#9@Ge_n%mW6SiHK ze9cY8k{@1nbyTmv;p|nXPu^Y(N~aELaWg2TnICG}-ue~&2 z?=L`((l|e!FLDkQaGn}5(pF#g=wck_*Q;z>f_@k#rwba9bKtsOnVBcn+Z(0synp6l zUV3+)_sagZzjSb(@wwaPPV5BIo_q?VLUbZ(_DV}lU=0RL{dA#u+`Vei3Yl{^W3Fyk z40r;+o7tK!bYTKh5g)yqTbQpN#tQC!H-=f6AqSE2&bx8(K^K~#;A!s04MkI0aO&P( z1>zv}3cHwn0sXdd{?F4i2fA$|YJa>6BlhTQ(s$8j09|VQ{4m@{rF;CQQKM4oUjv4g z8LMdX_rcxDDn3Kp&n`J1l}G0{`MI^TH=NyAhnIEQC|62@7xchZJ$B=GDcbC!`~3S5 z-ofEz5aZ&$HJ!Vc-Mr3;y^a@M4=-nC`3C8C!L~^X4YhiAap6qiyvo#zP=86y0u&hp+*lgn%qbF|^Fc#b{!s=96f+`2=IFT)S^g{N)vzWd;e@3tyCm1YxOT31D; zy<@|_8AX7>JD)_37OMf`w&8_k-p1)geZSOiVl1>88U1#5xxQ97~-<<8POxNM| zAzXk9Dc)Wq$v(acYKLljWV(uvn@)Lzl@7u#gR1)WznS5a9s;3AZ&{;IMp(PAl5mNyr;mw4W^lsCBrelMS%WYg%@TG1Tpbc5PfrYdYWo3VT zJpleRU)VyMFbZ%vO2zRhx0@jMqr}|vZH2j%&VZ-U2MX7$5KOr}8b8CV`3|`sw49}~ zE|@uAj3;-fZ|o|w*o;8&dJ_EtTj9q2m!0}AJmQBdD`Zt&v40qZ%G976}Z?mK#n=_>e#*1r<%}H?bA1$ zDf*@_V8D4kW4OHZqEj#Sn_cH%$HYtqdLul%TTUHx7A>##|Ct}zUZTwYobSd=LJvvh zzu7D-7k~PN!wO3dcZ<8(aF>YjXt+VrSuT$FAAA7g8%%OWx}9;qPR0=#r{sx~CqHtQ zlCzU5;pJy+#BaBs|4J@;;bxhpx05)#@QoL4l}UR0QtE_#* zfYWXJH8Ow8zmc1-e9;Zg0gV0>m3Ci^=ngV40G%*2qj7h0-XWe0UFm6H zgUY1rXIte}+oh;siQ?o@e;HU62zU4cqZXuiTwNZ7P8ir=u`t(kU|<~KiqYbc2Ko_G zn}5BX13yW&&z`%r*RUK)Z6nZ4(3&+--#T(h{hF<=#zEVqQ4~xbuerDlAus`h=IX6O z_SJ)HHa#JQg(l4^5Fv-culU(l>xmWm!kYD0#1>&*+={+w*&b3p-v>=p%ItJ;G23N{ zay1$3q45o;(+8vWRR)?ILmTBVuJ89-7=J(1$JiB>>V`8%R;yV(d(&%kKL|QnNRc@@(z%KQpgT!~7d6O`LL}Z89V(139iI(X6{ZqrPmK0}BbVrtl82uQPJ$?uGML zRwCC5%BJ2Z-HYEs&kic;+oz)=4S&;=MwnX##mA* zL0Nnud=j1k=C@0ct27gU*>nB#* z-E*hcE9vF+qvX!Xee~`1%hSr*$#aYC^_wU6?b|EM12V@oXd|NX#JHq9G1b_4*yYAaPCriq9U8`!GVh%_l$~@D2?RZ8tOv7e^y$71r zu`S{2i2|6&Bx5>o)-ZG<$8Wofn~TS*%B5@JXRgY&9eH}nMRWq&g;hlt)iw7kqC zGdBGYW(>D`C%k!Yw9cxwDBEn~fSGtE#wDz1V7?oS8?-99ulri>I);8UCdNZhTdx2S zSmlscxmnhHomU;O*Y@`JuVkm5mrps3yB!ga@x4ysE_vruPmw=;>ZuDyo_gxJr=Fru z{p^3Z4WHx`ynGy<`+vIFOX#B@@pdq_E^5ar?f@ND`rWqZ=S*#zMESuQfuB*MQ3y?H zxwkt_D?DyvG{ZPmC1Cm%HC@rJi2EQE@j8`m8|E{v)c#=g-X+j$q}Jij$_~?XQtDAQ(+_y-8}behbmx;j2E5AlZ-4Sxwq)MvyE?o0fHXb- zX7a@1;-6`@cj8XJCPB1Jp178pzJHhRoAjhcL&&mvV}XUNc}k!Rx|F6O};>!gjr9aEzMhz&AM*e5`>Va|G+JCg^qgE~93Ms8X>ASU1WY~)CCgKe7jfiu9Mv;;_}zLB7#IkPam zUA(r^FNHL^O)%*^uYDMib`p2$eln;|jTj7KidHaHF@GQHY_LPs^4U=B47)VX-%Axl z5~>9g^0!{G=uL|7N*4d{`1rinS?bh$Gb{shb_`+pW+^ea>xN;mGw0Xl6VsEH4jRLP z$DHYv9+onBdUNy7|NLn~7{&QCC>3F114jfds98*hhErLbPv$bm)GbFiLepHeCI6IS zkPox@0)K~=Rls)MDjY3b1-A2zpaZ?Oa982|g^v_IU-(aj?-l+B(MgMp$T9Ll@-p)C zie1Cr#NNf;!+xDT$UekA#(zG+evduO{+Rs*`#Sq3drq6zTH2C! znRZ%xsrD-E&DvesFKh48KB;|1`#0L}Yk#agqy3fkZQat>^)vc8{k;Cq^bhEt)IX#D zk^W`4e$1RWwC+IPR(#4g88qLwlS6)61a5)I-pEbi5PA zelpG@T2XK4jS*GdLl7@895HF0QY=s-$=bWLhjmHO+oILQ)A5=#SL24bfjdygQh!7n z8!5X8lTL6knC^R_?{JOqd>gRcf?q~Th1nbq#-b>|IKq9wRyGhZwxyTgmE+vDQnkQ! z<$RB7R7~PtA5P!O#(3)(ilQT|FtDFYprxEn%j>ofTT$SJST|rc863s%=QPF;iuMS8 z#fonZ`q(i-1wJO-QIap_LDykKB7bVJDPqb_v#yRZbcM+8D|i?h+?$?c1T+-pO1!!;agK`GGg}vdsXPu zNAMpzTfL$}xw$pQZDK4DN=wHBRGs5QAg&j1SB#x(DQul7;4V6GBb^jQ8-Iba@S4%M z+lxiJKSq5Mo?Udb8lmZvrGu<5iiqZrX3!km863g=a>Oo#HG=CmkHFD3&qnDe-NK*1 z0v;onFP0T!{*Ccq4AqBcf};imjLVEXjWI_gRBLP8FA5}XmB|z?O52#UV+?$Cj3Ce5 zt+)*}8A4V1v4EP4>ZA>gz<+Gb!%;U;)f)`QaU1$KF2)E(0N?R*ijinNBt942lv;Ub z<6>rR&BOKM{soI@vL7%OJ49AzLcpyi^ANi=bq6rroyndm}K4}ZW?$`$*5uI}Na zj&TDPw^>>lN9yT97pazR4Pd-*$5@R|=~>!Tn&E=EH%qGN4s?7XvAbH=sJ&g5)}qlrO*V}%o<$aIYPLdGD4aZpBmoCg?+ zhVBDEn7o}0Q_w>2zHl+l)%HQMlVf9VV96~Yyl#plQb!nsw10>j>!ZB+*$s_?0+TWe$Mq;aXLWq!g+uvblpgSP{p`qJEt6 zYoJshh#|U4K|_HPv;`>7Bhi{Iw6$l5;77>Tc+@Yp#(yAofP!M^=(JTtQH4wanG)1} zfD$auS1$xc_9)@dEd!)fkT^=V zHX_-LhB)t#)gaQcDsVV})~BKkJRTa0JYbw5pMO)C8q`}i1@Q|yO0Uo1hWG|73p%zp z1oA@Z)<%dr6f|2SAQ8Tidso9OXoD<;@dEl#J64zm9m{~mWJ;Y$1=bU)*#=r4Z-ZS$ z``A}$A2|k&sG^xZX2t^!07P9(TZ&Zsn<}3;rV>ZZ0@DZgk#oXb)fDdbf%bq+Y=P9; zQh$~|Hm8M?0_h4F?B*7_FAae*5G)nQ7U??X-CiH-14W zmkAx$(73K^e35bAC%&&y$q`qP5yPh%TP7Mu+-}oTFyOYsblgqAtg*PXdn@`iyin6E zP1kQixT&8Z8r7vC9nD3*31Yxy7J7Y9Lw|AwIVCxwPH7l10mh()%WuTNFhHVn*@iQ0 zAECc6MI5CJeKpkHGnB&AF@u^8{7|I|D<1Aka07*6n5Rog0C#c_atcn-EQ*NcEJw7Y zn1UF~Pyv8&A#Mm6XqqvNmT`T72tjGATtyV}6vL#7sR8#fG*UxkEQN55oB0}oiht#x zWr&3z(>(FAj?P0j!=)4yMh)TNEuuD^uE70&!j&N?^Gsh1yCYG7+VGQ^W43h(E#}Z= z7>9ibPlGQO6odsZ1&YU+yQzS;h_0!l==H+$x?w4%+UwG$XK?I3HvN%J;oq zSB;W?tFG78&zh>s2MKwQ{2nIOQGaa|GmtK(`GnjHr)qvE4Evz+8dv|StjEX_WxoL* zUyQ+;F!Bn^i|H0L7ZKw|gd_p{pc+`js%hFw2>Bu*zdo&uR+=dj0V{u9ZO|GoltBzYgDi&)Acc`qFM!nC9QK!4R4o^NM} zo}oxYbpm59ydA~6{TbFhM-a^6%|EMpi6ZqflJ_%u@Zc~ZBn8JCJwV92@@69ysWKJi zL$i*bT)V@{ear3#2$rOe4c;;s?alt!tqdV`gA;Tc%en)Kf;w? zncj_(KgI6AffXhkK!g&qL4W9P67qiaJKj_E7}SQ7Y2`5IfeQS`gro>(Jgt(U`UD+1 zO{9R_3q*AUy3FYa`;N0z>yZW_COXY9ecJm_X%myxX?bWAR2w6haKghK{);Z*B>u-p zgsM{5jf{PmkWVA>Kfx1;LS&v3YQ`EVe3qR44t6+J`pe|Iz^D{-$$z4vY#cYF(a&QG zg#wSG539z(Q4PBC9?ZmsZPpAeZv$S0gS$n@Q%FKk?mvaErRjXppi!#Fk&G2Kqo#BE zdsv>3YJ3S5Fj3z#Esc;WA$K9iQ#6tyCsb5|zC^r9b5z9XL?ME{4w$)`TGYh-7>SKx z$O6byB**0=Qxr^i`G0rlDTc5PI?^Wj=cbvOj*Ww9^J$q(aTL~o;r*HE^dSlhLJe{m zeS^_w4(wc_Q2Y!;VRZ6Y&5#MmNAmtak3z&opdwVn|7ofQUU{Cu-A~W>iK1W%eR3LZ z^#x4+z;dps;|QpmF%vzQe?1>(B!oqEJt3j0G-ea{BIBCLTz^E6Wc(QO>fsEkN*4$SFQ`_Vya#R~B}gHO+ghT;Fky$rgw)WD6X;#4j3jmhQUx=J=rNmo01z}niC`H665&<+`)&d!NP$`(v{Usn#MG4%tqayNNE4HPt=AtI431w)C69^jA zL<48AQ-8EvX5&mSnGtASC}GONDfLRp){LCoh2yzegsF6Jx(X_6YBSFf8nQ`^0~H}5 zGpYK;|G zCD0Vo5l2ZuxiDb%80Zy@;7ZUJ*by2R$i;|_IWAPNHY7;hs-~F&#=s0^2fB#PuNrRG z#4I_|^d$FPt}_f7*9?Smk;szzvr6sEgS|LM+mhx%|vDdoLvABc@#IYHA~pQQIVTKrA>6hW~LES zf6X<73yc{`MM-0zq}0%bS8-yu$fmN$V=xyf0=%NjHzqmapu!re&PpKZ9N2*l9Z6Mc zHN(QJC7NXsg+-3@L|B|#oHEaat|A8Rf8iXFI`ch@5Fy0p2qr@%91Q}cn_-Y1$VVnz zj36NHv)0k&W(Gm=cI7cLicPK)M8uV9q9?M44h7Md&+VB)XDchzzES zTq43?!kNHFn93)mq?j5Rj&2IJt+-0ARpTfd6n*&_`ke>(jU(VRI(jMN0jIy=z@Q43 z=XL}|Xli7zNV=>+g#o=uRni>Pf1FX_P~Q3IZDFApFqCu$j*Hm=pdjE-q){}i00y|I z6at(|QeeVNR0Eai0jj33bfwS3JknWSU!^-E@hP-y=njpC3c3Yvj0DHIX#gLUXf@$1 z2W5;JDutDaa@$Hg)Rr6|7leUgHn4VG^DzI7J~tNt)#%E!5TJr+MCL}Ee}}Ug+6`(N z@!@2gQpl_Y${Nf)%wRZe#H=PTH_!cA9eYq$Y@I|Q zX6Z0MS{n*53+NN>>3YCbf3QbUQlf&3$T|q>l?Icj&)J+>Hvt$rxzJl1+OyCfjiyAEAN8zPhDNPCt8;49+aUV#8$~weE z=m`IciZsepqD}RPTSf6vT^+1LDqYEugX z6?DriO;5_xI(de$q2ZWHJa3wdodp^Z)0Itz+8YR9XdOr@o*R%zdqgb&sKU{45$Q{% zT~aov50r-CRAnc^`1rQ+$H-CH z@}!JYZ=rEtxoGspen{bL|Q>{jeDz5F~Y0vW-sm#$~sXt(+~}ez>F`>144l7OXHV#$K%R)kHHy zAWY!eW(UFR-VB7Shkw?b&SKY>_O)CG0Tq>9%XJN<_0#iGIHfo>Ep7_0;_2uw33_T^ zxQQ%AjxBA>U#`3Mpxe}RcjK)aSI;j(g|0i0eSY++p;btr*@B?IdxYjqvZ7N#s8XJVC>`P=`*JUZIVd~|nb_sr4No!y<)qi2cv=F_MB>wo>TpDWc% zFHG*a{`v=h^;c*9ohyvcqqE#RM7wS z+=F-CFxk2A;A?I;N7zTnkqVPoFuM5wi@8;C~at1}) z$2q!A#1zs7{jqREZ{|a#x7rF^QYdftevVgzB$cJDS*!E=Pq-Z)P& z{n6CLK6kFqODf`$_Z~R!NF`7re+MeXdeXt{3BuP>GOkknUZ!KvAk$uvxVWBT8da?o z0m~R_nsX-dtEh&7`ea&0Rd1F0;lh=*AIOCD%L(lE_f@ z6ll@KZQqW_MV9rt5?`wp5lk)yn*eno;Mzv(Kh~19T7cHvXl}D)pe2EeQe>X(fXNor ztDyNxRw^5{_T1^MdE>6i$;hi}q5--tF@G%2%EWpjb|YmG@K>e|NOjMIgH;uq2u*TR zQ76km>RC{_-u5alMcGREu(B2?6-`iw?6ylu5B>6qG=%1|z<1%UCql1GF4r}Og`gDZ z&a!3Lz#t;fc664Y@)J6n#=#62Ww>RVv2MJ;GmA&Eoo)$r$==+04Z46+&tle%gn#WU zmQ7saqag>Clcn>rL+Bn>!1})LS~QMD8h3}J6F7u*x{v-C9zbzdPd-JiA@D-K*8TyR z`$79_B-xt$Ic-e-V&g~q{%dQjTi7bRrSJvx+{HKy3}tD%Q*aBuLV|gOIZqV2J8rG? zFuFWQd%6;1#Wn~hbYhyu8sp|E#(!U9++PcGdtnHOicEm3F#ViL=!5%6J8YK>a95_q zJH@SGpg?}OjKdCB>du+|MzP$q4rL}6UM62cFb(`6}GNu@;Z^)!d z=(iAQlaa~CtGGObo~S%hS*qY77U2qg8LYI>fx+%DG@CGy7OQIrW=Jtgn19f+j|$$= z?>+okGzmWU@V`MF2|nlFKbwDln9y$nMZWzi(>stOdx8efC!R{qWnxCGp;Ch-Bd=tu zZ?MZ4bEot!bJ4VW4s+NI%vTviZXW81&WcN$We}{@b8%L@@Z7pW;tHI|BtffH_XvEnsae^eS0b{0t-YW3}O^z`YT>pf|{| ze+T27FH%gw)}JRNZV!WDABeag58*Rzxv@3P^uDhrnzd2tm9f!0;gf2G4}YJgPfI^I zrykc{d+yv1CjXXDYr|@En9qTOk#YoMT16|BJLZNNM<$D)Bo-lrHJWCI>!8I`#~_^c zP5Fa9F}W5rUVjHCzhwE~p{D&MxENQ1z|g@VrP~g$oNO4uUjon0HuU@XgzsQB9zut! zpnYG0dn)k_ELv=1RWLV9qJKvk=C-k@-hEqW@IEyfp&dI{OM{l*vH#;#(k*B&}>F(b}ZW_0-> z?rZeM(=YizbE8h18%_EsJv4s!mZPtFNt#qAAMYJJYJp%lvwO{@X@9d(Dm9yX@nU~Z zzh8JQXji$fG-{@(k;7z$w~@+YI}L8>&fLsZh-AN~!jkgT-AN7-EOHNa)Si=B0uGD| zQ+|+2>gK*-xkB6K-HfiVaaE{%o|;-bC*_NZ%OZdQr$ zz+fbn#%S7WF-&h!m>^r~LpnW7j>nUSi@zE!l$i++ zR$d6*fRJFtNpWp7b^bR&`gR<6oMT{QU|;~^Cey7|@oW4xUm3WWUjRiI?oNF+8<}Rl z%-j#8I2f2fJOEk23)TPtc${NkWME)^!2kp-8$cxUWd=qDRImU5KJ5b-c${NkU|?W= zK|Rc}figX$=*Op)C7ZhbLbsQYJIM(LdWQL6a0^H+0CG={RFmO|6n{JegatAMYz5v1 z1_q7?5(icXkO+1O4hfNbJ@EhVBY#h2BbRD)H z8Xj66kRGfa$RA1{%zq#LAbKF$A)F!HA}}P*CORg_CkQ7bCqgKCDbg*ZEz&LwE+#HU zE|M?NFk~>sF#s_*F=jHnGY~VPG=eosHPANJH%>QbI4n3uIDj~~Id(drI?6i& zJDfbQJ~}>dKDIwdKb%5L;ysBMBYViMo>noM-E4tNK^tynn^TC?n)3!@=F#= zpiB@Mqq|vHeqUF6aWBtoMT{QU|?9l#lmo& z0R)(Um99R zmJ~^TkTNqfr~kC_zOe2!0tfIUp2E|32G8O-JdYRfB3{DFcm>;d6$}+P1PC=6w2+vhgF+7t1B-)j@CX<% z!yF4NaR{&Bb-aN$@fP03J9rm=@8NxXfDiEzKE@~b6rbU9e1R|V6~4wd_!i&cd;EYO z@e_W=FZdO|;dlIjKk*k1!Ke>MZKb+sM$=$EM+Z!9aYRzF*>zb7nH}!OgAJg zEz&mhs*sH~j>M)^YVg6a*l!lOZYY(yHd!P@@^s1sMVPV42&=Y#iz(?&YM)J|p0af6 zjn1Znk}T_(>sj8J5XmT1RE74GD|v8`Vxr`XR_~|dloK_uvX9EBrK0G#ZWLJ7vl;26 z7pClEQ7p=%)HL53gAy(KQky`PFapBWxW$c$itrB(=)SW%~z zoZ7Ih3C6d^CQ*!krIj`gQ&Vaqb(098Hm;XuVn-eOHu9ERCw&%pRh)1JLlh~oj z?GQ;06B~89YAUzO&_*J5hLkyCJd_b5H@zzV3yKNUU{g0#%LqP9JX7T3hdHI5MVCShs+ozX1Tkj!6LVR&!=05*yw0000V0000W0(S}!ZeeX@004?a0003V0005z-bxwFaBp*T004_* z000Bq000G)pce8blL!H7f0X$JkQ)Uiff_*s08=Ck-2ec1obA;GRvlRwMd3{#As*uH z?(XjH?ry~0-Q7K;HM8kKyqToy)b3$4tgmkI{&)9fK`j6p1J#8Qg&Jt|ovPxFs*lC1 zRpsl|G%sJTsj4sEQvz-t?g_{pimC1~Q1j3}Gn47+!o2BN@eL#xS=09LIPj zFp)`2W(rf8#&l*dlUdAW4s)4T6y~#lg)Cw*OIXS>mb0R`u4EOfSyQZQS;u-du#rt{ zW(!-{#&&kFlU?j)e-C@v$9@iQkV72i2uG>s7{@umNltN^Go0ld=efW|E^(PFT;&?q zxxr0tahp5bVK)@CAWbLH8uv=CWa zD&JkDmB`v!WNjm|)`_fbMb>sAYkQHkgUH%Z<=##z?>mdkyNJxYip;x-%)5)sdx*?? zip+b7%zKN>`-sf@ip=|o%=?SX2Z+oEip&Rz%m<6ihltFFip+<3;8ZRNhZic|S>H-(-<}Q$+Sn71=jUWZ!g=eKSP%%@o-; zOJv_{k$rPS_RSU9H&0~We35+%MD{Hd*|$h!-(r=&izOnBr6P@GB8}xDjTIt|l_HH* zB8}A|jWr^TwIYpmD&MnSq_;t&w^5|GNu;+~<@>jYf3&xXw6}@0w~MrQh_rW#w0DWL zcZ;<5h_v^LwD*a$_lvX-h_nxiv=5224~w*qh_sK2wChFM$3)u4McOAs+9yTYr$pMP zMcQXX+Gj=D=S14)McNlc+80IImqgl^McP+H+E+!|*F@UaMcOw++BZeow?x{vMcQ{n z+IL0TfA>V%_eI(dMA{EU+K)uqk44%~MA}b9+RsGV&qdlVMA|P!+OI^~uSME#MA~mf z+V52UUwAKa&IgfmK8l?4N#vZ*BIkS&Ip?d$Ip0Li`7Ux!gUC5QM9%q5UwgGkFkVGZ?rUL7}>?L?DlD?ge$Ub21rOLkm0+_7V5FlKU%W4!OCKIc?pr5F9N?c0Ir*k<1L zMmu&0jR}Mt!O`*Fl`p@t--nMLT5PTFe_K!M&AVH*Z(vL)WrW;*$}k9jEm}uJ=w7%z z^bJAJj1a<1SUx9EJ*?`8$V%=+?pCNDk7P+5B=|gPE#pqw65K_+o3^^0t=DkS*@Xt< z!FU6WN9(9pM{z&y_n>aQtPLClmsC+0NAg#?l8`8C0WQ+vU_3hais8xm)D4--e=H-g z?20SgKrhf9kWHRZscQ|b5FM`$*z)SYNfK?(TPs9s#Boo9dTIJ zEX{KX7E})-S95HFuIGGZDRhWUIM+F)oO6M6-3zSpOlP*}vVc`A!wNbV8bZLcxNdMl zx!L5MyeQZ)l&x8H9fZD6tTk&6f2WwZv2B>bD%v^~nBI!~K}EECee&D=KDuQ+m&w)=zBwmyMsAe5FqaF){Uq(CV%+a0 zLugNE-XX}95+>Pd{azBO{s^Uz)ld{dgQl(Vu%C?kQ92Z9H~P%w^Or9qe^ngYyY6*Y0S_p zZfKepcwx=WYcm$tL#`QG(1^W~$n)=f-~-=z^r82CbXtc+{13TGZX4K;(xa740nwScISjN(*6)I=0RjGSIRfoUkkr=^eK~F*-J+ zu4Pb4QK~z;pwB_C6TJLCc$`kJ(()2rD;st=QJag3b(I zFLcV|0(?^ub+jrprD{)yT#~yb_lDeup*6=N7-(wH$##o+AOjKfwU7fW>4s^PwBSet zjS{6IN}|ywj-x*Of0B|BCh{T}gmAbY_2qSNZht(2fhzS-Z!`v(8IH1kSVjXFv~sYg z(-WXi_&Dn?HEK7}K;CH7mq+@;K759vHB2L(a{QSc1?qKbC$GP3rimz`40Mnc+JeD| zBY}xPqrnFCO`Q$~SYS+}(xI1O5Kl}Q6*d;UWX7>i7ovRIe>HK%UtN6}2|{u4&XvOY zZjIoTrQN&Se9QLoNp*c5V_^!d=X*TPm+Az1ix`YnU4~ya>?{ghYq-#TX)zB4fua!Z z8lVs{$0X2^MmcQ4MR`yyoq#eJ@N{qg$xkDOr0T@F7dcq36`}IjH*E_u=;8Lvp^NIi zu060)w`UR}e@^eBBc)-94AlwBOk@Tx z9?9W6RrfVGIf=(|S6y1W;dlTy7>=Z3kfV0o18V_-Bu@eHhC@rJ3)W_dfON;v27)`( z(TW`Ze|;s7Aj5)3aPd&t3}s0NBOwh?Up_8@>!CDVcre(Y5H0{2?sR3d$>vMqe$tDf ztlczL3bT_sP(_elY2~0MgU&Ek10C04d53jz>* zP3MjA_a9BwW`F&any&<#*n4Fifz;(?hp;68G^TQ;Z2 zHVD>X$gx_vXv~*5cLR_Q**?R%2@1fuw0-r;=@U-bGKh_Js1;_8#|(oqrt1jKzzB@J z3-txvnfFV8UtsJKu8~69kzu@jZ>|qW^<+SIS%?mXqZ~qCc1&Oz%tTh%(gYw zK%ar0zywMO1|`;uhNW|zLK(#kekC6kf;Fgzz`D*%Ijl0fcnN+SM!f(`kiwXXf0{#p z1t|eT%?47?ll~HzNd+K55rb4>8*M)S&Qqs0H#bpfI9p#fPHTfBOT81l-ieo;EPQZw zh#tJ|I#imx6`iiqX?`t7CVT`RtP2w(9Vn?eV#u-4=d>*U`^*Ubhjt?c!l>VWYgwxu{;u#RX{8| zo*a@O?2LgdQoFE?YuH(5IBy!E?wLLl7ox8>uRQEMy-@smir!t+H;aWzp&1m+xd&PI zWRY5Xgu(LmYG=`~#2Uf42sp?tg2_W*cxz#s^39*soj}?~fFcF^e_zkps1^KQbK7#e za#u>JO}9Y*5|WT#Spf+|WgP+FPq_h{KvHEhfEXwWAT@bgQ3(z$1FE|w)q2vGcRPT;yjQV!wqLIxJWxPcgsucLRgHA$iRX;cv$d_Q=4|!sN@^@xf8Dt3~fjGx`&1M z4iwp(Gpl#hEGy6*f1I8MjcW3MSRj^{=e^KO7~&0Tn{MN9-G*ksxD$}cw|PKyFP|PL z0)2>?xc;Q?1RNu@M#xEG5UnBrQCPWioM;=_mWy(kJ#5O(^=s1HHI*^+2L#Y}v(@SP zfMEMg5JI1Hl8YwS#ry(bhB(=e9%cdEqwg;#liw|+6g8X>e_c~(QT(kvIw|j&T&E6C zPYouw!a1b^I@8G;@``ivPS(j%t^q$bP+!{A4Eqa?!J^x3! z@t^1BU^Kj2e=z2@jt-%?NO>Gx}N%So+w8e@>&?2>nUi1NH5@o!AZ;H@! zkZu)xe*(KOE?X9HU#`bq(a0OFW`xz6r(?u8!ZUeq0qj@MDFhw8RI%zEcX<$&j#@%k zkwgb@xn*+k^O$p(yD0Zk$xXBFfmaB_7g|&{szmq_e<9Ta@EUa_ESn;s-iXxj23E(^ z>4`i9LN^@Mq-@AP=?(x^P(?%Zy^dDV)|NZbf1EBW=WY&bNwos>XpSdr*VQdgGhqP3 z5vN%l_lC^-OSJo@yKmk=c+Y72?u{F2jeiB$vb{(^@9CKJTW%^`o0-JUu4#_;6NkOv zW-oXOD9h~W+nO~>Cg0h-VH54Xb&tF$Mi-3M)`?ErQsptC+bm6-BIVlCF44rJ=)Bzy ze~FfJV8mUJdkM6dY@RBTY+N-B*y}jS8YXM30H|5oT0+XLq1d?;>dYxL=4pqtxKIjH ziZpCU+Ah>VxIi^3`k_&vBq@7!-7@tP)mE!Y4esX+sF;^`zSy?gbE`*Je79o6^UZdi z9D4nw`!;sYZky5X+jnOtosVMQ!W^~Jf7Q#EhsE_q6$BWeOE>qR?*}V2k+%V?gpJZ4 z#1|agjQ!YY0C};l{6K!8m4MexL~O?2C3( zl5N5^*rl&;ceeE7SMeW#9j<2@f`jfC5*ja0~{HdwS26|Fsp?FFAALRlUiX%`-P2iRZ5yxAzVz=(hFiZ<_m3@}DbSar5|9 z2j?b_^^ae@CqL6!yr~-{|9}b`lkcC`^EbgbeUYqFx1)%7#;*FA0t9Lze}x7ZP)5F_ zL$0HaWX;_j4${oI!n2DdELoC-9Y*j5ntBlgFd%d}(?ZQy4%47zCJQ&8y<>kT5K-XO z3#RmDAwhLkV6H7_eGe-#od8lsK`-4_suaVBhh|u)&L6)nDO8FK-$&GPF ze$`7_CuzT=h~bImT7;xJCl%)FDW|3&E~U_zx@FfqD-XntnX{n2unQ+_8iKh5EDiOT za4k5@ECUS4FS-d}6}XA;Kx6!(n{Xg;7R_z~KZyBHizlL>wV*!H)4${j8<;DXR~Ve> z=Kt-voB>&7a3ws~f2vjfUmmM&0%sZ;cYf|BEuji8r7J7EUE01?DEK^&Jtx3l)^dBH zm;Zn9mne<;(kC@c$HVhK07~y#OxF}`@pUt_;~)as;>`kWvG-MH+sg}JD;!(cF&ZuO zdhqv|;rjY;C#LJCc7h2h{SOBds?w-5Jw?Fo=xi`Gw$j3f5`})_v@KzT+@UFQVZ$i0u zJpf->dGO!ifB%vzD?R3iQ-9kAHF&`dY#%j>z?qCcXrR= z-FLyi-G}#_J$v@EM~(nz%J79b<+(1DJ5kR9#vAxepTX_N)5rXx?{DJXWcC9e_~tMA zBgXSz!wa|1&CLzB-m|l_IiAT0VZhefxmmCShjJ&Rf3zrQoQTXZrC{h(Oq6_dWi|T= zSfjp@p}tazuD9 z9EWN1f6_gE$XI%0-7+?IT|n#gHy`cwjvn-xZU`$pw7*<%?WB4@c;%UrD{eVSw0zAq z&&6z@Uw@o4a(SqG2q7%>DVRCd6?!M-wx}}}qf9zYe=$mKIUjnhZ2u&Jyn0|!X zlXsneJ_VcC2BdXWX7{oe7^_h~)2RwLogYGnrSjfAx29ue7ac7mSUIhgTfh zb>IS%6|Kns+-JOoBlymXhsUoNUX1&Mnpo$Ux;Hb8x}fxMOilPDbWpoG#@ekwM-37( z$xDgF%$p;P5uv^NX80)sMwL1-88(a?e_%h;&f(4u{_44Lltay^Ig~*i`1awAeiJ=3 zx&H6}UVft9T2n<8<`Gv=oW(RCx5nq>h-yDDc|xdMa!A|W2Ff6B_Vd?0oE_+Ss6svz9dta<`Wh-wM2wLo)pY?X^(f!YwL@$9~Ow z7FEq!x5KvF&q6uFr;%aaK~HX~e+T5pq3IUtzqtdnx4m|yYc_bwIuR_ zVW=45k{pdHzQmQoUsil5KgoP_QH6R=ykO`1+ii;`ab#JPt&UdKM_2H6wW8q{MMjlH zhNpM-M}6|!5k((cd+s^;X#!v5o@+jJPCtH$9DyF5l^(sV(7ecCbtcqVp`kB{QXgGV z%T;KDI+%9Tro2;I>0Hqrqzj%CDZ$ynZ-7|R(eRuMm`+np9 zY@qt{pGUiq^MQBX_fE9(rZ-K#bI*DEv50>NXvxn-x!gDdf8>!;XB`6g&j%# z=A=<`%(jwpc%04 zaGXT#C;_pCDDJENM{m59+_oN7?<$r|Eft+WFaItO zD63aqb1>roH>{zJ@Ev+bzY=|O&%Qe8EId6oA3NKfjS4lqHz3BfikoT(lXRmV90(Bp zAY8KaLVr+I^^P1>NRBEpj@l?nMD$Ih-*fNj(_o^1e{}D?I6%gm-}vS?BkS~MuY=8d z-+b?ReXvM&N%RMlNdTrAcgN`2)8BY+=kuTMJoHy&=acmZzj6Kb-*~XTW!vzx0_@*pGsE-{Ua7gy%3I-t0c%hko!pPKjglc`5+DzE)xUN~8e{P~7;|jchEl%RmG^ncJ%Sgsh-D8Y+tN(RKrT6$Y{4j<+)cv(lcgp+p2@Idy{c(J%$@5oI&*J4Um4te=e+i5 zM41sWoob95rj<9G&j#2P3?v{b+R-&5X8XEUsxhZv)Etm(-72#XuO0REPSq_tPHYxk z0`?v%WC)E*w<*yjztk*TTXg|)3KV&re>QddNf2;{>xpO`s!~>|6%enMSLdyGVCP?a zL47+G0BAG~6OWpj!%1N=Dl_IduIEfx?E}r*8`uK}rbKAKCxl7>6bc)UDC{igKn@FXWNr)&hZg;qh1?!E*L0`Jv)PCtN z8YnSOg#s#)bpa8=_n)x{MS!53(*ZY~GvpPHY3XmbY)c?sS(u;v+o*27HF$^RGp+o^ zimihX5%2AR5%A*KAMWI9=+TPwe|a-NYP!ya+4%mCnJNiULCQ4Sgj`5mx;GWf1yPf*(^iE1!x z;HQwWgyBeT43+JK0R)xKJYQ)y8{2T;n}`ywJ%sQY1Hf%k%q42b~B6rv@eW_nBr*+hIVq#$(>>O8-%jAcOHjg@Vd*t06SuP9d<9^!DyMzW>h49k*D#{qUui z9=g5A4Chejf|-t#efb~cIAD!puA5uU9nGE2-JW|x?q89HVl+hi(N*Xr=#}VK&}-3K z(S7JWl547Vkqsy$e`)N7ac?w`c_CfJqWRR9mkdy1S7|UUt^kiD8LWoeHRF3bhB+Cq3e|DB}R!-o`(&;I9pu%#4 z47=Bm+?|1UX<(9;^oSaB%?pQSoGkSq?U2gl2wAXl6g4eur=sMF^u2=?16gVou!n~(3y{d2q%%T z+jLD+Xy&Ube|cAE_ACGmy<8=hW5M6`mJNc*^9+NWYS?jX9x(t$n5yI}+cvNUMvn=q zW71+esBgdt2FIuZ?1~{~GxQ-&IbqoJMauxy5LiOj9qE|mz~T)PWO%tiS)p|J*h*b9 zf==F!hy~2o^?tK1Q_7Y$e-Ew`0)t7TKk#@ElPFzsLdw#& zy4A8SI)xcu6K;N|%|U0wEo^K!mg9m(woOfD_c+u>Ff3(}H70WcxCLVl>kP_YlFj0p zEwFg+$=X7P^O!_IHycF9%(J)!mUtOX_#n_=FpQ6v3Z>B3g=mC9S&%i|w(|g$Rvo*W zuQ6a~f5j!Sp<7m9S$ZAx9U&xdFiI%sDW12%unR#cX>r}apf{PYq0l|fG#I9~g%Om> z0Nqcql_xT*2H{DWFve`hq}5d?vWocwfkojQ15C+geKc=TB8pZqvKR}svcpRnGwgN) zj&2(t=um<^Dj&%>A$mSCTV6RCrc`QX9jat=f2n$ltW0#u2*wT( zD8{Uo@7m5O05M?M0kvp`Os9aKkLv+l;Sz&cYV*(o5vf+os5w1*W5**W4tCq5f z%vHgLYM=lkP+gd=5o~D&HKD2ZKzk6AQ;dZQS4@4BkK=cO%};V^?uy*YbH4^wt_6x> ze;xH;D5T;SY)2wY+bWipA;Cxvz)h3|dfO|T|rHMdk zTn$PU(f1LgVf;O{{bvNh>x{~VJSn$t_DU^EscFp4E}>>=v2I-{D6tGG9!5y3t{&do zU)8jwl}j(|r9OSz?uA*+TrTXKyZA=Uf83bAKy$A{eAw7@cv6^|8R8nYFt*67{4&WL zF)=a7)nGY6PQjX%YU9!@;i;$bN+>K(TkY&Nz>aKOv~$z3TFET?7t~4n2R0h1un%mU z++2Wx{RfB$(rjtYW@*{*a+`pXj^<9~UYGmL-2E~l36wc=8A_xKf7%#y zPE?ob6xtc^ky6XjVIdD^aACTguC_o`ZaHt7=e#&Q>4k(%>6}c2bV9Ziu#!m_py-#8 z0$8g(nbO#fl~;MV0nbV@)6(MTGxH5ZG#lWMD3t0!=v)iJdl{%A`eMN5>@vdWwZ0$J zO9kOlE(N_Y@7j**I`)aU(O7IYe*x30?;C|_I;Qz3T=5chDMOr|hPz#U;KG}3y70i{ zW61DB-#}3t0$0wM8uM*{w!B|A5ZuEN@fBXq(Scrausu8!g)tCV|uH zbx$u0NM(_IC-0JdzbEKw*u9V*fjPKA4#TdMZ=Q|YJ#Gq@UOPGD&D%zUjQ zm4~95`22RZe|U~);XB)o4{GL*;+;DSG9ywa`J^(}O~SI@*_rO#dEl^X>Z@AaL4ce} zA*d8eTirNWwU_qSTM@J@f3P$}7SG<@Kwj8A(JWbp%qn(t$N5^~ zIWY*k3!}=O-QI!WCW4w35z-b-=SoM@EEn((Xl0Ys3$|s)M|)RZdn+h9TJ3hLly7|t zx@&bubaCVEg-w1qQHg`C8n`8qjsyVG==A|7sUts-JbQ1u@e^w|s%J$0U-MXaJ z?Tu7I(K>4Pd)cfFm4*yWC^18(b=189*P!p|#Hb4>r~vKD*wsnf4>9^u+_`L(5>v|y z=DI9~;f-EP!3Y$44a|uyxZkR{I)FNYZ4WFO!TV;xnjC#Te;5pP_XyaIz?HO*&Tj){ zZ!y6vZ(K$Qq@U=(e;f3m9@WbIw9u+|7JBL4w9v1%k{R0s{R|2W`P5{1>}Xv-Z^e$1 zT_G$e|{bpwg-yTfKgz@`Xh@YpH!Vx7JNOn-YYL*9V}NqPl4qFVs=D8m@_TG^puy z+XUnTfBgX$0tS5U?dbc67{VwOICaZO(t-y$91tQW)R*FK8YU=2NU$xH#fbXk00&@oJlXBBqwhT?^mZJ>~>evrKg^R~7z2pdL7)AZG zf6s_Vh^-sExLhlyqj-nEkE@nMmoG-K6w-&_}+8%`543(Nj`T??l-bI9@60=qu`+BaaTrm zQk8Key|bD71O_Mc+A6Q3EElNIRVgD1UCy7NI8+&Y*eG<_?~*N z7_`gHI1d!h^1+}G9V|pEEJ@%_WIlP_gwYE~XkVR*>u3S+dSnL}NJ79p%XOe8a+VP0 zFia@I1q3#o-0&a>3qg57G)L&ze=vY`LFw^lyq`4@uYaOoYPJrxM)L?~Tr-_&;+)oX zKs}6ep#cK~rl(m;SdaHr@&KAm)ALP@PkF$b$Qc+dR?dTwQq6U;*lu?V7P1%qNM85@ zy%g7<|ITD(@9OH_6_m?<>|24q$qTm0%m4Ic?SB7YzyH-gec4REf1uz0f6AXtH_rPK z7x7newcIeb1FT#Q$@w*Lk}BXZSP9i^vQ*BrlQVIoZIT*t*y;9@?92#cW84O{-H$*< znb=EqlzNr*oj{kKTz>ZK@}u}G z<(tcw-E$#{PsY+;3SWfGf7~~t-jlt=uR0HrS6@%^`}6gFnap2#HU94h$P)-1AX7Un zb6JyI3EHutW>s{doT&>CJOy;`qNu-#WbVQ;8nbR9646e&#Msy%?mK{T zkE*SAJhuLWAFMz1*v=pRaOYO+1V0Woe+@%F)^oY`Fp=Uc z{L}}j&!)-xa&}y=I~w=FKonn&^rFtu)uvP5aq+hJf@@A4y72J8y~fz_#K*5fULg&< z$v+_@u-wEps_2cA+Tned9^JiVKNks6v8Y^IMWTds-6r_H$x{zKgw`JVha*SccjU<5 zgVuE>KYgqGw>8Fpe~kZkkUab6Vy4|;^4t^ec*hgy+0VT5ou9$MV@J=PJ^C1V?AY0} z#~z!?NQ}PP=4)$4O-mYviMrNjg zbY*iRRKsL5tMC^&Eqj|pAH8z;lD%Mt3e})3vs?q+pv?F5e<9!A9&IcQUd{ZdhJi(l z+NM3x>Kd<$_ClSLz<9|mI<g51TqtK~@b zn=;W-R%pfu(Yue{e;;~u@))XY>7y004P>pCJD9s% z*&?Zlf95l?QbjLFsE|6TGeoUvDyvFgmrm?)OHKsn4q-d$D{4LugMZlFDlJ)Li_L4d zH>)+ z4KNEmjR`7R#(hDt_%uSA+g2=2mjM;uNaw{kveChE6`?t-bZQJ%A_^{lXd_mRg*Y_XY1njqzTB3N-od?qO ze;L%;v0r|!n_ag|6T zL!p2WM?*PHRLw%(P|FKQur5jIG%_l8f00FpE)WnwXbAN> z$E%v8nF|$;P?8g7^KC|I4T^3jh#lPJo$go)&S)3vqi2s^w0CxHZg%fQ4{vRhh}&8y zREPN)vk??4o22n`hteEE`A_(hfMwQH&Qz%8(*o&L$?4OX zJOi~}fed?yOaxRJ)x?J^XBS<6e5Sd_oiTNIxZ{5y4?L(=tE6Wxy>Pe?@Vptt<*H}R z_Le(y7grm_5I_FoOVb8pdv!Ozt!D@=>Lk|8CB4mH9|7_kwM^X$M%`XLe+BN7llgPd zPW#9!0D+Hk`+(P9m%AnRcW660fUZNoj^2VEM31AVq;^}+vb#swjAw~grwLtIB7G_& z{?gebQ*g$^fsC~*!2gB#*vuU+&Jb1 zhg-TWt)N$BI4?9R+Y`HG#;Ik7_-^bL1rc%JT5_^52d08ytXhPWz=%r=3s$0Ie;B56^#H_*ng^840D~efqQs?|rGh#7E=&-qL~YF=CmX(e@xTmBywO#T2!s+4na)U z7>QO=8LL4+$TWKu&cZe{GQ)@)NFd7vPS$=E!Zt5xHFyrfu`3)RC(hYN;bv4wClR5p z3Y!bjlD9ippT%UY6gnMxDD+Sjx1lP+~&#k3vaC1@CoFXAk zS@(QQb)9u9I>4vAOQG%Np}dxe8h&#yXV5a7ei4Ow6DU1UP-;01nY~Mp!15`$>{!W{ zD>2YW8FE7aER(?TFt#X&8;sS&3{k-%I%g!PSUUGaf0UQ0Aw<(X+t3!PYcoaNhk9zB zRRN>rMN~Ewp~($;G4K(_v7C`BQL1NCHA|gJPp6+$xt{;A^gok)I@6A!P_hT#%uQ(a zI+SGx6pxl2*!u}I<~ke)5V>(Rz(Xl7m@Z)?Q_;1uYxxNM3zCZuSW!M7=WC55@@xHF zJNsf9e^a>x|8?#!a(|ioZtfp*|0DNo?s=JEzon|wbm(X+Z#eFQE(D{KsX9_@vD}i}f86i(MSnO3xHg`qG|I5|G#U<^4d|zQ!=`j&$DnI_Lb>!Z-vKy>Lrt>nNo6Gnl~n$3t*-^WVD&YTY?!);)QBcv%c ze@1YJl}D_=JOeA_A=ey0Nhif1$G8lFdl@JZTry$IYI30yM%UmZOZXKuc}qT-+XuJ^ zw4ZB<8D{x!EEX><(ibD@C8qA0`0H@DU0!ju4WsKfI%#nZI|eA{bWAyy}X>uP8?8g zZow9&<7=)3I(4|XGB0(Ri}XC`6gRXI!c5H$9fz5OYXn~@bX$M~Q@M7F90@{>O;<2z zy2j+g2ww;n6m2<=8;jxnMB@gjA@sG2F2s8ubS;J)ho1LI;|USDvCOmyRB}{jf1r>G zbatAha-Ykh$lS<0Rx^~nQ>Jdy z|3O2>^6*-}Xvcv}81S50O{xsp`mtSng&1OQNXQLED-H%8`T2aR%(>>2k5dJ4%eMN~ZgHUB&4VL5tOZLJa-536Nrn$Cft*Jgx=lu5l zFWCL6=5(t${oAwFXsrFPDL>WTb2)PO%Y@3DR1^jMVL$1U;aGY~QSr=jfAPkLKKs-Dm2V8dZ>-~{j(zl9ltZ z-=0ixYwFMWEV(v!Z|)1ZufubJgQhAeD3%jovKZO9HEux14F|wEhbqkOWKps1R{py@ z4S=$o8J+zNqy$Vc9pD}Xe~+a{JUc8w(FV;jfPm(9B=k{aHyC7d+Tdj1NFZ`jsIm+= z74B1u=%~Y;u2OW7blyoX9gh1OGMEACIaABy4n9B+j+TiZUR5j7ifvfC7pq|*A4i&A zHHxMm6Tk<;VYD197hp&ShRyODVA+{qp3Yb(qX#Y{I+e!KWQGNbe+kSIs62x^A&@O- zdEGFG9vd?4hGU28pg}0si;dDvHb5Kc$4!XCbV9KSz`;N=d<6ECF-m7^g&i^lVSOKi zub3YKyH~0GN2D@(_ehUCN%6`4ZqhENdab&o)5`4PTn`L_3~Y~@yDn-{z(BrD0!&>m=F5Ps zKziY)X?cq>b;|_-AbQ@{Nm1mP4R#QeEs*`-Kz_L{f7cxXgW$tG^Kj z@czA8s=c%y5fsRhF+lkMU3ud|jesH3h}P8W!Qo{KZp;=}Yc}jPbh`+~nmI$qKj6p5 zVq#gs^er&ud8mD{#2b{9mzN^Ap`3t2EW1Xj0fkrtgsw4BtjvaObd|IldQ#gi4X122 zi`4Rkf1z6f8>4A)T?S8xO#3F0BUdfbVL0;1i}y&rL!?nBu?`Qf$42}}y|~;!#FBv* z7@?xbH@rP=`39L>bLyPG>KO!53L5JobQ!Sc>(Oh_o6zr|51|jEN727WpFw|w{sesy z{U!Pv^k304=pWHf0b4$=_zSQG?BD+nE88>6nT7Eg>LKU=78 z=#LWE4JGrX1{nHsP>qs4XbxHa-hhnce~sm0fgN}#fZwgBv;iLb%5}}7VJEw30{ez) zADohovIooaf%wC1Vs#slo~uMBB{7fuFolQ93)4Aub}vS9UvH4e29?*q9Rd0zRk;|G z7}XG2VmW9-o)id2&ML$$ng_$w^Pp!A3dHczqQv8SfHeex##5EYb8i?+Fa0UYgvZAqx*}6>5 z$NLF604>5cm?{W*5;{aoj$&b(l%to)Bv>IPe^In#_XN;c=MJ$_x7A0jKLIo!e3;CSM>lz?% z8o>WIf;^y@JOq$aN)T>C%e)J2_P4kED#gzzFGqz}h)m0Q64OoPp?Mab_?~^39>oY) zJer#<96lg(UMF=zG=GzjIYO+!{SjOp$>d*8UZk#+E1FuyY8C&8G5$( zH>5Y*jLuTAihh$w^hf{m^q(*s=rP(=4ZIKn45p#evfYEb8rP zcdf+N9DA3^P=9i@aL9JUEnE3Qa^>t|90V8ajc$7FnNtMxv%VQcxRBRuef4r9&#ynO z>*wb%F5)ld;#?_L%gw8Z%hrO2eYsSFT03$b4b=p7HA%fGePmM{5J=N4^XC8xM7UpQ zHVa-&EF0srvQmM+fAeo1U0z;TUPiY~uD)S7L?7zzSbx(i!Q{0otEbT$!!r6@bGecC z()r5r+Hz(2{ZIei^5?&C`yvtlaGew!$2ytbxdI6i zYEaF~)g07KWbd!bSdWBX@Pr|*lKY9~`L_r058ls7Y?H4CnkZ6%Q0eyD=YdrB_c7K! zaO$cZjP2_e^`O1k>1~KZRT;QY>$UTymNg3v87 zXS(K`0FvWT?w((3p5e!Ho!o{T;gW)8IbvBzDO)`$QG&yTB*btEo31ImnXOLU71K2- z&`;;{rK)ecR=MJ(rfYanygqvCcs(fwhJS~{JWq_$>}GZHSG~q~_?pr7bX?sqi^)wl zCq=Uyg&}^tg(&5^sV}&$o3z}bljeQw2X(uipOcQ$JYTF>_)T|hD?i6o7R~Ip`o+?yfA;H- zpgW%b_UzoTN4|Xg*dt)7INvwz2*n|yq34&VFS!rb$Jap#?nJhJem-mQ1Szeg61 zo*y$?&x~^|<#YVS&t$*+kzYA}>_Y^#{=c7?>G;3FUs3DE%8Uc)*z1xc=#H}G=S2Xv zqGN~6%UY4MaLsMk%;`~asSxPP*W8LvS@X@osIkqOd^1HItJRpBtEcYduYX(TeWyE! z$o+QiPozhu?GLsxB7la4sY+=H<4v@s4tw=%73^%mg8ukis5jYKdL^6Qt3swVxy+|Z zjggCM$~nMl1p&!P<8bP(&Zc}S?@UiR8j~$W+wTpR(6Bw0%L`{yN0n2WCu-GAmAW~S zuAa0%j0RvdM$&r&k4`$>L4T@@T!&BnJK1wVM!8DN*;@2OdV|O!rDa`Bv71CzD*y8Z zPtyRVYZ_Dkqe5sZ&kEOw3Ch5h)8zEn6@KIywiz<4F_JIo!t#9pvH)gvyT}A{05b|w z9rOURGEZJF6FV(xxO!kjB|FN4db1rpZ|QW>qHs#JS@(o5y+Z=!U4N6g6-Rn5`>fU<6}l4WR1aB0Z;*jQ(ILjpb>R{{UR!>qs~Jk~|Q zATnO8K)G~r!T7O+#(%CRWnTskseyDHKylX%EK^-fBK?(s0jd1&JS-qN79B#Ng~b~M zf#mu*^#EEOnM8{3Ur@F%_n(!no@Ku2=@WHI}(p) zqYm0ydS0#=7M=e{Qn||7`8WRDBxRxDeN`A~2mjwJ_MMg$V< zge3X`$lo+Mr+;qu8-bmCc{9D;t+rEVsXFIBTU&=WzdRhCg8#kVv%_Rqh8I>bUn_kt zy9_j+LE%v0;=(nBn+k<-TZv0tEC*um$1X!U22uyARDS2f9X1@S)9$dZ78^UW7O>fu z4lojQSiyb1K}!r_`t@QlRGU077ruQWsF|i^OS2lD2!H0wzUSEWfIeuMW-U13H_D!0 zo)1ohRa4sMex=&U4s^2l{DXYrR@yl>JoMIAzI{8rvh|@`j*REh`qFzAPOL88I6Cn1 z?#juIRk`r$n-3g%woyVr-z!MYcc5ZmSzGWGtkgvx(<`1 zb+}cIxG*p3YXBN{03-s#b zi-gQo(8t3xG$+0fj3xya&7&vw;94NWMGhFxHgnn`g($fr86Xluh_4o~&y z&>XP2&;cFQwn3!a(=bb&=n*ZV+%<~sV+tHtZ{D7qQW`sFJRh0_pMUptM+; z_kX0HdX>?^TDY%o-&UOSTV;Fk)QOH^ihJMN-i}fim~bdPhn9s05(9BTOeo8(`I{FR zMGrS`bZzxzMs@S_^4aF`r9D4+v^@pra+fLe^Im?=&P>s1D+y; z=9qJ8wX1{IcFf;7QJ$ zQW*MFOGU^lDP{G_;u5fSS$Du%fyQ{!HT7)rBRj5?%kx|h7_rZNh0E{gjNWeQWC?_Y zwz06d|Jah3lm#uL^w3Nza6xj}tx8-MxcT1#8Koxz(;;cp{Sb`Zt&}>R)k>#2&3`g| zs_?$TgN5JD5m(iQQ96T*o3aJ9qlC<61|2;$+|p!*Hi0(|^9&mp)RW}q^8gpMr&%as7%wP z7CMG0spKYkTSGdWX{Hq1ls9BOYw)PU!(}~VCVuEEuM{}S;*@1D&vUL#|E-yDco5#Sr zj3}$4p{wIj|27!RkYOqSV2RLjgZ->-NT`aO%9(&gDw^qn!li|iD*jaYd@6Kz0b)Se ztcoY0Lo-H2s+2>+Hm({WR)0Kvo^FA#di2UinfO6mSs3xPV~6Bp^1vnQ z>z5pmkKxgBCqMe=!w*ls9aOkiZ$x0Sl2>}Er5CGn-rVFv#ksj|wMvH9UP~^wtsgjB zmmaVe+pR@8yksaBTjvj5{P1_%z9C2c;xE|AKRyiq3e#LVJ{uS6piS*7Tv)ii@Q(^V zul%HeBMemNXpT+VZHpl7jYqR+DVCj|FG$Mc*{YhDb;Ap_>FXmePJbJzDT}2_6uO2HuWhak=Q~^7HzZ_kepE@zhFJ{T%?l2Aaa@aI z5XKnd`=iJ((`J$c(UESCEYydqn`=ck4Cg%Sf@V7`nnpRT4C@to9maXbaYaK|44r=L zB9hFfsb_}z5#TXIJNG=RRF^jghxVWDFHQcaef!FmrTKoNxqrOSs#pJ6u~vlt4Yap~ z{>)K5F#RN{r$>{;{;30p2Aj*(GIelick*$!T5l~ZHyggMSz9Z&zxu~+oypWLD}n}T zV*E>YsK*Xz4x7(B`%Ut;$uH2OliwyM$(!GF&#(O8%wGgo?X5|K>;f2T#K_foHbxw> zDB)waP7ypy1%If6Vb$3o^y`x+g+*^8KKo;)um4v)C`S4@&fg3wKIi1-z$3l0)a2xx zpki`8s2Jp%(gGeZxm_mmVJYd&dSgm2{}67j!l9hK(kcFAXZjl{koeC&OPnX3n0)ao zUwPvL5B&b`lQTEoIJp@P_I7tkd7CU9B)i*B9NgL7-hU>;?MJtE*97cVwy#lW!~%M&Kmf_QD`)i)g+lp;-Q1>xkK zQE{H10?{y&j_AaIE1hNk0~q3bzOsl3oy=^R+!-#@y$e`j=P>(G&_mgcry(`|Mp z-&(;{p7e=>G^e6eM>E=s=Nj0wJ8#ntRWt{ zq<_8#iHW=eSkMmb9RN?YkP;OOS+wrJZ@_bGV zL!%az>*Vf>QLwRAGdOXA8t9_g>MaPQEvE?kTVPGlG1nz*M zFOsGZ@+O}kq#BmX3zf=OvanKFD3_~*e)e(Q;oQ;P?N-|X(_g^<(24xk_T8GSCh%aL z`x|5DG}eWo2c&LPK~Ao8-Js^ zM!ZE%6RCs5UiR$T9O+!(=`MoMco%d?rRw+#ppXe23=mt_PIx0iZo#T9l)=zZpB3B_ zq6Q)4{F+mN-CGFN?m!{x*9kf)W*$X&?_pP=YX|W9>mQPv$nQNu*(=BoAAQxU9zAm< z{IU0Z@uugA_KHWJf7RPx^|7la)_;{Vdvb?9I^{{lLJj47+VAwo?J<1nT?N%K?smEY z(<+Px^!PLVXC{9GZ=c35Uu(Tph_|+GqgTM&ZSaG$5&6z;vn#8sk;grM<@2C1kz0jct@{TDmCyq9X`z|C%`N(# zX}NdoPZ51XDJy43qt zx4B>bh>*p?|v^wT^j?{s8#+Mqz*91aOlZq37OIxEnO}-zfZx!WZ($ z?jK78Q6tUVq|7s~B7)>_jPM{ZMOX|3DG3pMLHzfK<46?!I3Q>;(OLoZBlgHB%#yLI)`hQ7w<00@7C=E*L zj|Sa7{kv*^I6TzdoRh9K7l1q^%mIyAaA)pC*`W&4=(*?6siYx-w-!XxRrT#sIpy12{TVu;3~F)h-mDK_9dX5jOfd4@3zJ+(C$6{&|h-w(Pk zuAw>fkS3IIP>;~%Pk%+CgIW85xwItB`PQ0KDUQ~MyZfqsJE*1QUawPr(TZ;r;U+S9 z%4p*1Vvp$e*_>B7lwSS)jr%CGAC^VT_c>(`(deuBvtsPJ9-trXa5RQ}sgK(HW^=zwsWvVUp_TfvC*N zoRZ;D`pnxlUw+Ba;kMA>4M{9=$)3)qV9O@yi~08z_S?*nhTNZn~9nkj0Y+dsRwl*eE+m zTxsYE&t^XpRBDM^E_oH<*X60n69fZnxK=L}V>>m%24@TJbjoS5Co$rL6qh4({};j( zWgvX#_%UAQ!hfA+1O^6_boObJ{{e5}n{eFWC2|kb4;@;hx@DF;QMN7Jmf6lx+p){> zRt&Uhvwx#Wrvi-Bw~AC~nIB8NTkn}gN650_G#8qV;Q)OS>h!9bO>0{wgAS=UmgSa< zKJGr2dWn53NQ15)SmOR_kFj2zJDv=deJ_+9T~JcaVlyygRE^4BLx-7H52~>b+|!pO zo@CPeD{fVtw3vp1yRs*R?*c!+;L}gn*%PweQwg(Kv9p}2XfY%D`s@*1ZtQC*YJI>wv z)Or-h(J%epTVL~*ipEt)08~xl5yV)a$Knk+t5DBi~zH94C7=MGE@8 zU4K}`dFGx^8{P?u!d1!7a8Z>i^v7T@++i?>SlZloB=?MOO+HPCjBUe{^K+(`#Hi)S zpcq(9`tHWQR%72qc;PjZ3#Vt%B}83s*2tAjI}8I!_HyUYCqef=ikSgA`Q)DZl;`_Y znab*w>w~?iT{=|px(Y>VDoa2Fk!DW?N`I0)AY)_;LzK`lIwsPG&H!tI0Zq*S5p^9I zMA9oc8t6Wt9Xf~)%Y;f}Lt%QU|u_K2kmaP*Is6hE_@>Suqi)FhQH;dgle(2PeAQ!k5p5@?EnZyTKK%-z6^BS*K>$`pHNyd))2(Zgn1(nFlBh%|Bo-0 zh(V02lwkGDfhh8c!JMI8v$q<{^MC8rhNCH)7U*ioR>`&G!{lq^zmOkdGDKRWHHz`T zaoXRZ7#|NJ9JC*S8-_9HME!Y&xeID*T#UzFP&0?LRr-x=I9MkLQZRKQ z#`MCNNgI=h=J`wTesq=T4?0keP`fg1REX;Ysx(Z|)3k?;$gpFR^EfdxklM~{?I)Tp z7xmH^9s>OsV|t`O4j8YRC{A#hJpu`0AjD1?EH)c+Q3`2lWoE;LdQpg}1Su{pVho$6 z66;cQN6&EA66RE-VJNEPj*S~QxRZm3Q4C#DF~hEAGmH8%VUl57zK$W80)tfHf1(-Z zD^!e#X9$zSbNxluP9PAu~fyESx#76IYILKW~w=kvvE{k+A z`RD&FlJl-7O%J+;BgQurW@w;wsXYxu%`%CngLD+OvwoOGw(jdyW=C6HPHwo?WMH-G zR%q|rhqmd%9459c9BP*B7(||xfGFpF#wNce(c_ht9ZDpjhi`yi$83Rc$o6R<}3Y62^DxXKx+S)lKN za$z&wP~N1Nhusp?!0=HFa$pIrhjS3TxM{4Ds;R*^z)vupp)<-LtR0D2ZnSd3_&kyX z>YXye-cuuSzBWHG4jK%DqA>=4b~bP-pF>${aL^UhR_Pn5s)B}*8Ic6d0B)+jkRqTG z*t!IfXR$s6eLq01xtDbJe9oA1((7jDS&4FfO0ecRqx z@tY#>E!?r4uF_0xSY**{Wz)44Z&B8r!c+6e6mhMKX#g?Rx&ky%IXwGGe~Y}M5p(nw zcNp;VFz{k9Yiv>W%fiurZ4-J}NZ)d+i@GE1Vrby*hoMWqwi|;;hVFKm?L=JHZ3m4HMu2#e8(flE_A9lu}XAKv?jOS6+jBJ?%2F3ZKEzg%_ENpM-*bPT_ zM4c~G?BX~(wSvfeM8)YgwpQv3n})G4zjo$D8)fN$(pt8+HI3TN#v+j6yO!qzo%&uo zwxA3C#7bLBs|zcCx+b)6xoH-KVN1mPq2}Ct+4EPEh2`B=2__FH3E2gWQYf^N!xY|h z(=xo^z+5{9ZZxbkopQ9`l+B*Pimn(4D9x@&=Y!OBnb8O%|9sAlVF8_NL9jG2inZT# z^Q^9_o1hOd32`1*RNDwd-}q6-rf@7p9FCj zVBq~HSK)$f*Ck(fQ?cZSms}au>u)@L#mVEh6@$`={aV}%N@?bYnzpxoML!BU>0Du; z5P(pH*jup}j{4^pAV+DOAI}##hYC1PjTmXGAA58$j`RCf#w$TT0+Z7PjmX(|O|Q(% zW9#jWQg>c|^DsBOJI{M%f7@T$KhOBwEpx|q0%=b^4N@UG7BzdNr6#Zj1Ezku&^+p1 zv1o8B`8ow|M_YvtH zy?S|6cKW*08|(14P8;P)Y4D;R*s4da zA1_6lU38znAK@JwUIsBP?p@Qld&!OKoY?Dl)3xw+YL@eleg|xuq|i{ScNZ2;70#+` zxd;V+rt;Z?QY2L5^bnN7PIKmzEM9nlu4ru7gbUhnzJ{0SKX$?S!+XynudZa2W_^77 zq-`!Pnl`OYzCwDYZNIC$cWQEpZDO7@Jd4-Zldq}!2EeU5#P|xFu+KeZoA=%eSA3^c z*{L*}@YcE_GVL83{+p4s-=YqwlUKk`bzH)K1#fAUck%k&a%+3nk&$!~cy9 z1~5X}eT^ z)&(=?i}B=k^^09$7Ml?$08gNQS}WXm??pdhdzgjl7NfmmI7v_4h4|y_A&WmyR3Fv~#%UnT$PaSdNQ2p)@0yzISjbeG`=e zNoRc@u{q#Tr&NZ?^PcK^f+v?Uy*)=7bVZnMo8_WsgG^nh6bHk&jOo6PKGW5I>C^L; zC*7-7HhR4`e#q1h9el&jTpw`*y=Q0|Usr0yt0Bx}7A^M6OU<1lwq3>yty<@F+9-C5 z7cZ7-#I@#2U{1f{!b>Ngk&Yia7X9uO+b1sinai3mc-j|Uf6awg#_q*F)r6jEpSkW- z(Kmeo1J3hs6D}{l;KWP)X4lz&-!TzUUvGp5cgv}RPK)K${y*^}+e?)Bna}^2N$4S| z{0N(c_L@|RraOkPFqU+bSfd%X+a-oLg{IM0XQ1+wwR8K%}0G<&wxIOHmA z%FNjBskmzpQiDt}yj`k}&yyGAUPhuH!01oWQO}FB?jQq0#0f()8h1D69pcH*m7WH+ zkW9*cwpCuWU5b)k6eo{=_{#=E%@zKEaRt&Ot}YKkCk$-RoSADnFffjA#c1(R1N~X4 z&ECN|7+u?^&)nQ=SPrGO5$GmpZSGLO% zMtt4t_4mNUu&TxagLFpi~|N+VZb?4`9Y8~SR|V*4_GVI^t-<{V<`rs~2STL`Q*L{S(rR3_7&jVc z%6T)*LBd=*i+UJMgJcwKVJf8AVt1mlQRBJ>&ARI|>dU5o`H+wxYYKNyC3qnx?pio| zc_nhKpls@m(mnVq^z5Lbetjl7)G*DvgIGvQech*NCfLP5hQA2kLQ7AJ9%4{QE8Jg) zmfqw4*rg%=2QH-_zN*B!oyzWRd23kf1!vW-3wA1->erdM6!`bmW+Th|xdgtpF{V*U zP+DFL--Ks>fEnnRxRjrHm6VhJpNj>LvclfHuCSWQ4+xwI0opLO2EEY6&7etc{^V-A zd*#Pa}s?9d`nTcnjfBcFDW~9NmL93Gcy07)FVd&3cqF+9>^$HMy zRStQTn`O<{dDQ{CVQ-)La(3bcxpmXH+Y#}97(eSI?vi&s`6T({C!aib=*cIafAUHC zH8GQsK-VVliMeSJmzoX+szuOl5oT+V-C_h*ua2holg%qTg z9lO&Mv*R{;)sIv41WY5MrYq{VaUX;t-ltM8!+gdS!|$)&vjlpL)H?h**vxtt#FG2m6Ue}m7mCG!s7)!Bvnr0MxL zlE)Vp|3tIBV|Vy938H24_|??({X2c%q{mI$EZw;D#L|^>#oLY^ZFQ*j>SIhWeW{}j zd#kHNzu}B2JLmIBx`nuq6)wxy&QleC{)7yD8UZy9;$j|D0CqbD8K_C1oLu&7w*)gJ zO!*hCm1ZOI+~+#`1l#B^hfRKCV|zVI$aT_TmNofJ+hoTX@Zc2-MBdVF6A{#e)R+HZ9xnXgin0)(lkt4} z9#jh^^O1>=?rG%~E1;*A2sBXU?z9C#EMY9W;go zk2%vjJ$%aKsm;wl`?IGEVHD?o)1Xv@i47bPxS(b+9U4w$aXy*L98FBJZL;k$+ZNp#X8BXWein7o4g zEO|Tm74mWNdGZ8#ihP6oEfsW&UPy0HSRL=B@1q~2zfB*ae?b2OeTsg6gMO3#fSveX76La${t`JW*=vtWWU26Vt>f~oPC3RlRdA^Yb|X_ zyF@#wy_7?3-s7EjDA-CC;A8VPwAi4 z|3LqW{&oEs{agC?IGFQ)yulav62F9>9i=Pp%5pNg2DBdmJD?T9}6@Mh25`QWFxA;4wVswmU=ZvK*gut34Fx3KO?m zVr$e_wnQc{)a!h-4efCPOfQ2crXGT)qXU>I z_LFfQS&4c>Z;a@F-X4NUf#HbB+mvE~8cEjPr9G@mio5=;E?$oJq`4Y5#Ery(I+h~p z)kxVznEZf?!F2xxeOPOR=i5>37MvL=6=rid7>l9+;|TYsTG>Fv*p^;`caC%0O4S0_ zH}k!cQ89^oeYkup8)MNiJc^F6!oYqqftGSQeO|YP7=HqPZ^XI*v&rBnhT*0$y7IS2 za26}RIp|}@2o->rbVo_Ps03Yyk#VTSrig7i&A>Uz)IGy#??Dr3KjB3T!9dsaxQoa$ zUEl@zQ=m))D;%eTF*FNTAwAlKdJM{X#4 zAHx6G+3FpC70S)6F>Y7l6EP*tcz~*NoCw610`7`2d@Y6XG6jA`XJVw2qG%&v6W%i# zcYCpD_s6Jj!mEo8KO;1KvUHI3MG;X2(hQn|J5(bmFGr3-cpbQZ^AH?u^K6uk(k(m% z7VsEBW${@t=7AUw#!!8DB{*t8z_{GS(-?CVLbbMk#{Hr|;#OHL;ij~W$s@+VSH}o8 z+}(=XP?I54r5_8Z$*4}+&g6j3tOrSu@5JECbkv6LYp9LyKHcyak;;haipFubdhT5)&Rx}cP`av#OlVl-NoTjlrvW86}vDRXjJu)u2f)Fh7_5` zNJm}hhOrtVxV0+~ZdWz)An8LHDRg}|9(2`zKw%$alm+ff!ZaTal$8Ok%DWgyVH|ID zafTy2FT!p1#&L^xv13429rIBFHZyRecsfj@Y@BDDLKRmi0Pt?Mh4il0bb;)ESW>lx zp<-XbhOFVpAz9$&h6I$xe>Zyn#<{0pWF1B#}CX}%JWFHW(#fY88Y=DvNay{i>)z;9iX5XIy!9?QB)yQK&AwB zAD{$_^VJK1k)d(7o!}1`*cjNxIG(cKtP5=#Dw9n4WyAfM$_a*T1SZ-YAXL)`(XCb6 zida+NV{L)V0!A6+?g0HCGmOK3OgGbhtB&3QaIPm1!9x+TXeoZ(?qZ-HForQtMmX|g zr8c*_K$Zgp;-r-mHi66?g@d(Hf7Jtr1899J+Q8$XvB(3)8S*)mF+hEEQxLzPqxAY53d9d!SCnhp`h6s0g3R9+{+hcK^tT#j2F;{+Bw0L*!T=+Os3SCRA4=!nr)!<@iy31 zw2yt2_K{=Yh$@=tV}>?=-~d3>#k8eJwZExyOJiDT)GRQKZ67%&l&Yq1w-2-jY+?(f z)|RsTu{kZA6i8RdU^lnWjb;dxfuNm0wn*18@AmpwA5iKr&||BoP-exD-btuoHA2^6 zxJ>A{hQ@VW@KJk5xN{;w`j2J%E*fP;L;zygFf}^$_rc2#_Ffe;5F74io`|B^( zbW79q8xZ#Br-(*%X-G$NasQqeaGQnR2h@;UK_5wu=tmlQ?t?L?;qn`CFdU8OT(;o~ z+eZj0OjU*{*Nq&a2%o~#F@u^8oTyTTSq|kA4H2X;+|Z>YfRY@9K7va$iz2c%%aIi+ zrozQ%s6aHh5%I}?3^dJ{M$5RqK!l()RxS$)0f}L9ztn(o42{$fp-CYvaAgR}JmVC@?nqR?Fq|@T?65AO#T-Hl zq{|c5Jp~MN-^Dn<|1<0h>#?J6RLqlteU31jF2x8@@s^`eab6H<;}&$K&g*n#o+DZ zq=Q|qptIC}ZE6ivjq?;x3p1^;63_erE!5h@#3j`E@P=(?^6rP0o(5K^x!wW4Ue+qYg zby^xFe~jIM11n58fJhl+gV0|mEs!Vbque}#Mp7?pz1SX7jaE zAZGL-)i^k+L08_58Nsm4nxW-wz?*Pzw+ML>NeG_%kKt!&I$tzsl74!! zK2JzBzJv;xsPCITjgTrKcOu7AG?F4GR8)e0J}taSb9BDxL?ME{9+SKx z$O6cdB*%v%Q#3|+`#0#Zgzy47(kA(*rkR?Kje}|P=`)$)D69d)`%}~DLlh=~8sswi zsG{!{*ttZZ$r*?S=;X7SArp`f=ly}6Z5X3ZMW~4XGgJ+{@_>TUPtN#>qF@SrauSMv z`XVMFU^&;+aRgLh%tTMxU(LrE31Ly)Pe`aLji~ltVq7zsi%5oyKaIqun4cgk%HfKT zkM3dCv6~1pkR0BFjiut9n9Ll7i{VQgB@V1t=&~dz9lLO>&YLV9w zUH>K3!x>bSE)Wo2P^~t39~2@bNFj-T+ghT;FmZ&&gw)WT5a?a1%ouhAQUx}#QkT^ui6;Jzz#C0k)AtGOP-DU2ltXfe!- zi%xu#ae5hlW5JplLZosUVH6w+)GEf=63{9smyAoH*sI5KA3BYAj0i)eZb!m)LGo*J zC5j0JXc3bQcn&anVkW)~!~lIFvj{4r)f<=yz)xa)T!t(N(}Q3F_&7x(fX2&OAVUu- z1v9$81SF~`f!lUeMBZz~w)E9p)Fd_G8Jgk*f(A8z(ZCt(6fKw8I1@}}1ezC0n6hw5 zy;8C@BPVy^c&-*pUbP=6jHQcU=*;=ION$$H`XBfJy83-vNrKuZ8 zLlO&&np%X4~<`F&KbIth{4ug#& zgj$~h9;y= zO_hIth;UVo()jSdVZy!W8S8;SW}0hrWaqR3y%M>Y-2!Nwd$Ea>q&YyCwq;=VIi^C1 zf+iJJvkbZbCFm0U%PgE!67y%H-m2L!^eki^M`UvB2B1=dD-97fQ5_!y^9m-V!8PFk zNtP_&9)!85Cu6E$p0oIzsYw_lJ2C<&fLdjL7!(K5Dqu_yqDzCySVj?~8qD;_M(<8c zO~aJC0x=yRT>?iicaTt`Ofilk^c^q~UCA#*sM19)5n(XlO5h_*2EkNsKVvB9YGNy85t~+E^AO>KyOl&yahFX zXH+swpg8>GLp; zbe7jw>CQ-e3bh%!L*t=>5W#|x;5au8;G+_)CY+45y9Q$pq#G zI^$yG4#*;+W;s{$8fGBKSyvc96~OKVDos#DT{*`qd^H(C!_yc`g#+*?iV3i6&9ht9NTi5jQEBvv->PyX%s zTrAMvE*R+3O4{#?Mb+BMMzz{*Zn{O&Da7jPZ$zoqDSYcRytGNyW zDk{5{>l#Yyr)Q;bN^xph+!S8L)6riN^whv`6IqNLTiTdMTX*e2x2fsw#@jZooL__r zU30t@nObRe%Z^MBY(_UazBVs4AMdZTH(*sPVv3OOhP z4ya(#oY9uY+(J};m`60~nM0}SNfjl)g8nYq#Jt1<^4y8HXI)B8>>fUK;?7P+$;sUd z|0O%e*eA}?r%zsZwJ zv%hiX`0+F3hQ}VelN^D6KOn`ozkTuN8;Wm74ciZsU7X7Db@2SQ0?*6Qbf7wQ}syk-$-|pL3HaFz*<)%AL7o2vx z>Ewx@fxk!ISlMYk^2o-<{TJ`~rtHwq6zoC~N-mJDXwS;S+yH&}*6+d_`PlBx?y1A8 zJG(ophffppEhkUon0!E5rLf6hF3&2Pf%jx%SjfokOO zkC(9>sKG|zQkbQ0D%@B2Wa06`UlpE5-_5D}A@oSp?@Prs2uacNF8CdIjS4*EZP$i5;m# zKICsfrC3ioh~h7NEhXbB)$d_C2Cp&gRf&u1DW*}?TA1~IsAq~sCT0~H@ z7;FO6g@9`ttv{_LYqbEaxzXHa$v{g27p2HN+X0g;s8>Prm8?`YYVEm`Tl2=9mE)0D z)kFhye_di&o|TF9M(jq)BH*{C4oG#+go9NTBL_`#Q&A_&LF!rXbiM6WUXHSr@?m8y zP%4_B4%uy&l3oK1MrjDmWr6QPsmDUEOfJqbR?A20HnV$WOm$b^kV%!$XA$v{k;x~jxIBcOs60~HlHev5 z;R<~jthCU9!R|0Ln=qjht78gwNikZLf6%gz3f|%GKJ+;>2|oYOzd{`ezURL`m;Zi< z&~E`nzT*nh+m|Cuf(Fkgo=OsBVrHhHQiCNUx3bkY+GULWQhJxUXxcr8IqW*-t4tO* z4)sK5#ih+M2-fISgv#mJ4d(RH4OFH1TL_tK?}N70D5KidHIj%ndV+OcueDScFi@Xqp+WgBDNS0B+L0A^)R~Pp$@y*Wba(FIhf#sA+#0 zZpPIhFm!N8>9zwbCmTlaFM(HQ8~Xih!gnw$457nS(7rFiJ(c)67A>~1Dwv}rf6*fi zbF5g@Z_H!3F(ZeE!^v+BhxZMKZyOHp9}f58X1+wfSa1qemE9ZFN6|)zh;~NWDpF5P zi=IlUitz=bUP3l*yZ)uOuq)W=)d%)n$cS@=8C`mS`x?FeF}#xmL}E7Cwlu2TOb%t?Ot_pf7)!6O3mgT2EyOb?-gDL+Ewl=jhZQH&zGdFV;s@LzSu%tYNbdrPBirj-8wdZ7(2%Pkw~ds7^%waGj6tCAQ8a|I7WECVM_m(hgi4G{1tYOEM$=x4VakK4e`U1ijL`Hv zX*$why80m4Xu{Af(>T}&olw&~W;_RcmfKqD>)Z^%Wb+zZDQK~+@_Z?FKp~E6h6&B3cos}(*_ zp)*TXF!gUG{h%HFgDl|ui~T>&1=ClWp()c+0gPM)Id|Kwb)f96e{l~Q%T7&-!Y~B+ z8!nB>4~s?p&`yQ!gZuN43f zX7UblQ>;8c;FT(m39_ZWq|?LXXgqnS_{-r!nVIm%$_t?z5E867DXxvC&i?^lO+XF+ z0C=2ZU}Rum0Ai!{e}{L+^V@u7;AVaS6k)jYcJU;^;~g1QYW004NL zV_;-pV1B^>1S}gsB=cnkMg~+c4*)*v0~UCkV_;xlV17Xz%(8(pJ*4QzrIsa|dj7#? zCm}bI69)7Q@4?^}kXiuiK#hI?000000001T0Tcl)0gwVXe*-21ZUr0#Rt3lf@CJAW z{0Be>dI(?$_z8>&#tJkF#0$0z7z{QHU<{B9$_-2nZVj#uG!9e_m_+!G=bJQJ=IG8EDm92d|Sq!~mR@*12QjvN>qW*s0MVjadF1|B>f zSRQm9_8)j3f2trRAci4GA&4Q|Bzh(0CUhpWCeSACC?+X>El@3fEx0YxE%GitE|f14 zFQ_mOFmf=gFyt{3GH5cqGfp%vH1;)oHi9<#HxxJ6H}W_vIAl2@I!rotI<7lNJX}5I zJ{&$`KJ-6GLM%eKLu5m&L@q>%MH)pwMVdwaMv6xAQ%8zOBuL>FX21<}i084CH zm|6B(##;JY7+X|Zf?LR4KwV~DT3u8T44kL0C=2ZU}Rumn8(G!aGn7Kn1GlI z2pJgugZT^qEcF7Cv!jb70e_SAo7^@KhW9(;?OmEQWoGUbQpS`SQ^u<-jcu)ENhiq* zDKj%u`ma`doAjsZc{DTDBadgkc|BSit=4$-|LX|WkRU~d90f|OV~h=KVh@hPUhKp1 z*pCx%B2L1|I0dKTG@Onza3;>e**FL1;yj#>3veMW!o|1*m*O&9j(;m~CAM%CuEsUE z76))0uE!0y5jWvx+=5$i8*axPxDy9)7w*PAxEJ@~emsB&@em%yBX|^#;c+~HC-D@X z#vwd|XYm}K#|wB7FX3gpf>-exUdJ}x07C^10YZ%iEhHxBpwL6Zz+wju9svWUm|>0u z4&zO{g}3nz-o<-(AAcXZyrp`_4~qoFVAl>|l2 zrZQ$JYw4(Bo_~sEP@8#P#~#R~wb3DIWXR)ghia`cNn;LVjS(iEIVKb>om9Sv&bB)$ zYuuni+6>B;E#sYHx=CBst8p8rL|W>`7cKK0DHYG>l*f`xHzY01(>C;~kc~Eu#D-LA z@WHXzXck3oD3!W4SyV#ubixEhGiBqYS@l~?NOw~EY=0v4gryU2bT%23WLd{t&+^WM zs6wG44%$<$#+yD0UbCj;+%W_T1KGlB({S|L|Xes9SEG^gS!AGZW$=J{o13#12hvhbVFw+X(2YsoX9? z8->^%Qs#*9P(~ZI=~emPrIl=x|r`I)T4dWev+WtT~a z5QGR<8T<): Promise { + // eslint-disable-next-line no-async-promise-executor const serverPromise = new Promise(async (res, rej) => { progress.report({ message: 'Starting Test Resolver' }); outputChannel = vscode.window.createOutputChannel('TestResolver'); @@ -129,7 +130,7 @@ export function activate(context: vscode.ExtensionContext) { }); }); return serverPromise.then(serverAddr => { - return new Promise(async (res, _rej) => { + return new Promise((res, _rej) => { const proxyServer = net.createServer(proxySocket => { outputChannel.appendLine(`Proxy connection accepted`); let remoteReady = true, localReady = true; @@ -230,7 +231,27 @@ export function activate(context: vscode.ExtensionContext) { }, (progress) => doResolve(_authority, progress)); }, tunnelFactory, - tunnelFeatures: { elevation: true, public: !!vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') }, + tunnelFeatures: { + elevation: true, + public: !!vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts'), + privacyOptions: vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') ? [ + { + id: 'public', + label: 'Public', + themeIcon: 'eye' + }, + { + id: 'other', + label: 'Other', + themeIcon: 'circuit-board' + }, + { + id: 'private', + label: 'Private', + themeIcon: 'eye-closed' + } + ] : [] + }, showCandidatePort }); context.subscriptions.push(authorityResolverDisposable); @@ -389,6 +410,7 @@ async function tunnelFactory(tunnelOptions: vscode.TunnelOptions, tunnelCreation localAddress, remoteAddress: tunnelOptions.remoteAddress, public: !!vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') && tunnelOptions.public, + privacy: tunnelOptions.privacy, protocol: tunnelOptions.protocol, onDidDispose: onDidDispose.event, dispose: () => { diff --git a/extensions/xml/xml.language-configuration.json b/extensions/xml/xml.language-configuration.json index 2284296049..d04db08380 100644 --- a/extensions/xml/xml.language-configuration.json +++ b/extensions/xml/xml.language-configuration.json @@ -25,6 +25,8 @@ { "open": "(", "close": ")" }, { "open": "<", "close": ">" } ], + "colorizedBracketPairs": [ + ], "folding": { "markers": { "start": "^\\s*", diff --git a/extensions/yaml/package.json b/extensions/yaml/package.json index 1ef92e2cf9..cc2de87346 100644 --- a/extensions/yaml/package.json +++ b/extensions/yaml/package.json @@ -39,7 +39,8 @@ ".yml", ".eyaml", ".eyml", - ".yaml" + ".yaml", + ".cff" ], "firstLine": "^#cloud-config", "configuration": "./language-configuration.json" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 756d90c6fd..8eb8d57da6 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -15,19 +15,19 @@ cson-parser@^1.3.3: coffee-script "^1.10.0" esbuild@^0.11.12: - version "0.11.12" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.12.tgz#8cbe15bcb44212624c3e77c896a835f74dc71c3c" - integrity sha512-c8cso/1RwVj+fbDvLtUgSG4ZJQ0y9Zdrl6Ot/GAjyy4pdMCHaFnDMts5gqFnWRPLajWtEnI+3hlET4R9fVoZng== + version "0.11.23" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.23.tgz#c42534f632e165120671d64db67883634333b4b8" + integrity sha512-iaiZZ9vUF5wJV8ob1tl+5aJTrwDczlvGP0JoMmnpC2B0ppiMCu8n8gmy5ZTGl5bcG081XBVn+U+jP+mPFm5T5Q== fast-plist@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -typescript@^4.4.1-rc: - version "4.4.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.1-rc.tgz#f75f17985a2329a94fd911b3ef2e619c33636fa0" - integrity sha512-SYdeKrJiOajqNTI+sweR70JET43Z567HFNo7DvvBof8J5/bt2cywy7VoWXqZyrsHEmQ9foraLtLr30mcfpfz9w== +typescript@^4.8.0-dev.20220614: + version "4.8.0-dev.20220628" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.0-dev.20220628.tgz#a8cbabf786f7e97b6da9d2bfd7e5839b96ef701b" + integrity sha512-89n1Fp/JQev/bPPuVferlfw0VguLxr0uFergLfPbhs+6Hlu3M5rLwplGLHL+JZ+0+BsTyWg779TmaMcqa5FB5Q== vscode-grammar-updater@^1.0.3: version "1.0.3" diff --git a/package.json b/package.json old mode 100644 new mode 100755 index b87274b2ec..5bab275196 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "azuredatastudio", "version": "1.38.0", - "distro": "8f4d839fbcb98eaa7aa3fd8bf9f84ab9bae899d9", + "distro": "b6bf3fb97bc70074aeebbf9bd86ec03f637f5361", "author": { "name": "Microsoft Corporation" }, @@ -70,15 +70,16 @@ "@angular/platform-browser-dynamic": "~4.1.3", "@angular/router": "~4.1.3", "@microsoft/applicationinsights-web": "^2.6.4", + "@parcel/watcher": "2.0.0", "@vscode/sqlite3": "4.0.12", - "@vscode/vscode-languagedetection": "1.0.18", + "@vscode/vscode-languagedetection": "1.0.21", "angular2-grid": "2.0.6", "ansi_up": "^5.1.0", "applicationinsights": "1.0.8", "azdataGraph": "github:Microsoft/azdataGraph#0.0.29", "chart.js": "^2.9.4", - "chokidar": "3.5.2", - "graceful-fs": "4.2.6", + "chokidar": "3.5.1", + "graceful-fs": "4.2.8", "gridstack": "^3.1.3", "html-to-image": "^1.6.2", "http-proxy-agent": "^2.1.0", @@ -91,11 +92,10 @@ "mark.js": "^8.11.1", "minimist": "^1.2.6", "native-is-elevated": "0.4.3", - "native-keymap": "2.2.1", + "native-keymap": "3.0.1", "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", "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", @@ -108,17 +108,18 @@ "turndown": "^7.0.0", "turndown-plugin-gfm": "^1.0.2", "v8-inspect-profiler": "^0.0.21", + "vscode-nsfw": "2.1.8", "vscode-oniguruma": "1.5.1", "vscode-proxy-agent": "^0.11.0", "vscode-regexpp": "^3.1.0", - "vscode-ripgrep": "^1.12.0", - "vscode-textmate": "5.4.0", - "xterm": "4.14.0-beta.21", - "xterm-addon-search": "0.9.0-beta.4", - "xterm-addon-serialize": "0.6.0-beta.7", - "xterm-addon-unicode11": "0.3.0-beta.6", - "xterm-addon-webgl": "0.12.0-beta.10", - "xterm-headless": "4.14.0-beta.11", + "vscode-ripgrep": "^1.12.1", + "vscode-textmate": "5.4.1", + "xterm": "4.15.0-beta.10", + "xterm-addon-search": "0.9.0-beta.5", + "xterm-addon-serialize": "0.7.0-beta.2", + "xterm-addon-unicode11": "0.3.0", + "xterm-addon-webgl": "0.12.0-beta.15", + "xterm-headless": "4.15.0-beta.10", "yauzl": "^2.9.2", "yazl": "^2.4.3", "zone.js": "^0.11.4" @@ -127,7 +128,6 @@ "7zip": "0.0.6", "@types/applicationinsights": "0.20.0", "@types/chart.js": "2.9.4", - "@types/chokidar": "2.1.3", "@types/cookie": "^0.3.3", "@types/copy-webpack-plugin": "^6.0.3", "@types/cssnano": "^4.0.0", @@ -169,7 +169,6 @@ "eslint": "6.8.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^19.1.0", - "eslint-plugin-mocha": "8.0.0", "event-stream": "3.3.4", "fancy-log": "^1.3.3", "fast-plist": "0.1.2", @@ -232,10 +231,10 @@ "source-map-support": "^0.3.2", "style-loader": "^1.0.0", "temp-write": "^3.4.0", - "ts-loader": "^9.2.3", + "ts-loader": "^9.2.7", "tsec": "0.1.4", "typemoq": "^0.3.2", - "typescript": "^4.5.0-dev.20210817", + "typescript": "^4.8.0-dev.20220614", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", @@ -261,7 +260,7 @@ "vscode-windows-registry": "1.0.3", "windows-foreground-love": "0.4.0", "windows-mutex": "0.4.1", - "windows-process-tree": "0.3.0" + "windows-process-tree": "^0.3.2" }, "resolutions": { "elliptic": "^6.5.3", diff --git a/product.json b/product.json index d99827b8d6..8622f4b7d8 100644 --- a/product.json +++ b/product.json @@ -37,7 +37,7 @@ "gettingStartedUrl": "https://go.microsoft.com/fwlink/?linkid=862039", "releaseNotesUrl": "https://go.microsoft.com/fwlink/?linkid=875578", "documentationUrl": "https://go.microsoft.com/fwlink/?linkid=862277", - "vscodeVersion": "1.59.0", + "vscodeVersion": "1.62.0", "commit": "9ca6200018fc206d67a47229f991901a8a453781", "date": "2017-12-15T12:00:00.000Z", "recommendedExtensions": [ @@ -87,6 +87,7 @@ }, "builtInExtensions": [ { + "version": "1.61.0", "name": "Microsoft.sqlservernotebook", "version": "0.5.0", "repo": "https://github.com/microsoft/azuredatastudio" diff --git a/remote/package.json b/remote/package.json old mode 100644 new mode 100755 index 8301a092aa..691afbdadb --- a/remote/package.json +++ b/remote/package.json @@ -12,15 +12,15 @@ "@angular/platform-browser-dynamic": "~4.1.3", "@angular/router": "~4.1.3", "@microsoft/applicationinsights-web": "^2.6.4", - "@vscode/vscode-languagedetection": "1.0.18", + "@parcel/watcher": "2.0.0", + "@vscode/vscode-languagedetection": "1.0.21", "applicationinsights": "1.0.8", "angular2-grid": "2.0.6", "ansi_up": "^5.1.0", "azdataGraph": "github:Microsoft/azdataGraph#0.0.29", "chart.js": "^2.9.4", - "chokidar": "3.5.2", "cookie": "^0.4.0", - "graceful-fs": "4.2.6", + "graceful-fs": "4.2.8", "gridstack": "^3.1.3", "kburtram-query-plan": "2.6.1", "html-to-image": "^1.6.2", @@ -34,7 +34,6 @@ "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", "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", @@ -45,23 +44,24 @@ "turndown": "^7.0.0", "turndown-plugin-gfm": "^1.0.2", "tas-client-umd": "0.1.4", + "vscode-nsfw": "2.1.8", "vscode-oniguruma": "1.5.1", "vscode-proxy-agent": "^0.11.0", "vscode-regexpp": "^3.1.0", - "vscode-ripgrep": "^1.12.0", - "vscode-textmate": "5.4.0", - "xterm": "4.14.0-beta.21", - "xterm-addon-search": "0.9.0-beta.4", - "xterm-addon-serialize": "0.6.0-beta.7", - "xterm-addon-unicode11": "0.3.0-beta.6", - "xterm-addon-webgl": "0.12.0-beta.10", - "xterm-headless": "4.14.0-beta.11", + "vscode-ripgrep": "^1.12.1", + "vscode-textmate": "5.4.1", + "xterm": "4.15.0-beta.10", + "xterm-addon-search": "0.9.0-beta.5", + "xterm-addon-serialize": "0.7.0-beta.2", + "xterm-addon-unicode11": "0.3.0", + "xterm-addon-webgl": "0.12.0-beta.15", + "xterm-headless": "4.15.0-beta.10", "yauzl": "^2.9.2", "yazl": "^2.4.3", "zone.js": "^0.11.4" }, "optionalDependencies": { "vscode-windows-registry": "1.0.3", - "windows-process-tree": "0.3.0" + "windows-process-tree": "^0.3.2" } } diff --git a/remote/web/package.json b/remote/web/package.json old mode 100644 new mode 100755 index 0190237bb4..8d95399df0 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -12,7 +12,7 @@ "@angular/platform-browser-dynamic": "~4.1.3", "@angular/router": "~4.1.3", "@microsoft/applicationinsights-web": "^2.6.4", - "@vscode/vscode-languagedetection": "1.0.18", + "@vscode/vscode-languagedetection": "1.0.21", "angular2-grid": "2.0.6", "ansi_up": "^5.1.0", "azdataGraph": "github:Microsoft/azdataGraph#0.0.29", @@ -35,10 +35,10 @@ "turndown-plugin-gfm": "^1.0.2", "tas-client-umd": "0.1.4", "vscode-oniguruma": "1.5.1", - "vscode-textmate": "5.4.0", - "xterm": "4.14.0-beta.21", - "xterm-addon-search": "0.9.0-beta.4", - "xterm-addon-unicode11": "0.3.0-beta.6", - "xterm-addon-webgl": "0.12.0-beta.10" + "vscode-textmate": "5.4.1", + "xterm": "4.15.0-beta.10", + "xterm-addon-search": "0.9.0-beta.5", + "xterm-addon-unicode11": "0.3.0", + "xterm-addon-webgl": "0.12.0-beta.15" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 586cb3edd8..aee95ff330 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -123,10 +123,10 @@ resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.4.tgz#40e1c0ad20743fcee1604a7df2c57faf0aa1af87" integrity sha512-Ot53G927ykMF8cQ3/zq4foZtdk+Tt1YpX7aUTHxBU7UHNdkEiBvBfZSq+rnlUmKCJ19VatwPG4mNzvcGpBj4og== -"@vscode/vscode-languagedetection@1.0.18": - version "1.0.18" - resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.18.tgz#05d78cbd4b6ba5a0da4f76c88fdc98f67e99786a" - integrity sha512-z98y3RZtuJQbWdqRJNxV6MNv8nJb4WMxjhvxltzfPZhrH+vHcNRiS8GvX1DoJTEV7DN4GrodjHpTh07YGLthDQ== +"@vscode/vscode-languagedetection@1.0.21": + version "1.0.21" + resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" + integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== angular2-grid@2.0.6: version "2.0.6" @@ -339,9 +339,9 @@ mark.js@^8.11.1: integrity sha1-GA8fnr74sOY45BZq1S24eb6y/8U= moment@^2.10.2: - version "2.29.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" - integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== ng2-charts@^1.6.0: version "1.6.0" @@ -479,32 +479,32 @@ vscode-oniguruma@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.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-textmate@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.1.tgz#09d566724fc76b60b3ad9791eebf1f0b50f29e5a" + integrity sha512-4CvPHmfuZQaXrcCpathdh6jo7myuR+MU8BvscgQADuponpbqfmu2rwTOtCXhGwwEgStvJF8V4s9FwMKRVLNmKQ== xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -xterm-addon-search@0.9.0-beta.4: - version "0.9.0-beta.4" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.4.tgz#e332f99d5eb5991f8c0e361c9b0d45b23f454323" - integrity sha512-PMzAPtUOjQjJcqpjB2k9BkbjOZPH4PFuQkBtln2599mCPeA9WdA++FpVN6WdBHgeIR5QILoT4pWg0hA8USInzg== +xterm-addon-search@0.9.0-beta.5: + version "0.9.0-beta.5" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.5.tgz#e0e60a203d1c9d6c8af933648a46865dba299302" + integrity sha512-ylfqim0ISBvuuX83LQwgu/06p5GC545QsAo9SssXw03TPpIrcd0zwaVMEnhOftSIzM9EKRRsyx3GbBjgUdiF5w== -xterm-addon-unicode11@0.3.0-beta.6: - version "0.3.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.6.tgz#8914f377757d5078e7b4daee7d3e2b7428b6edf0" - integrity sha512-Qwa18yMhtacf9Jtxy+UuxHfjIeIjaX9q0LUfHtZU8/Lwjh+bGcn8E8IABVSGvXZgPNKw/4TqEpgLFexn+sfc5g== +xterm-addon-unicode11@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0.tgz#e4435c3c91a5294a7eb8b79c380acbb28a659463" + integrity sha512-x5fHDZT2j9tlTlHnzPHt++9uKZ2kJ/lYQOj3L6xJA22xoJsS8UQRw/5YIFg2FUHqEAbV77Z1fZij/9NycMSH/A== -xterm-addon-webgl@0.12.0-beta.10: - version "0.12.0-beta.10" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.10.tgz#ba23287043da8172f4f9e53babb620f54ad36189" - integrity sha512-mzMOAqgM95FAgzcVzCH/Q0NfN0CTMHVDWCCFyg4B5ZcsuRiQKqQQw0HS+5uOQDtoZEDl2BqGFby7pGpENWGjZQ== +xterm-addon-webgl@0.12.0-beta.15: + version "0.12.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.15.tgz#9ae82127f2a39b3cb7f5ae45a6af223810c933d4" + integrity sha512-LWZ3iLspQOCc26OoT8qa+SuyuIcn2cAMRbBkinOuQCk4aW5kjovIrGovj9yVAcXNvOBnPm3sUqmnwGlN579kDA== -xterm@4.14.0-beta.21: - version "4.14.0-beta.21" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.21.tgz#2d50328389cc79021c0202405689955fc18cb703" - integrity sha512-9ELD78FTUL91OBRfNVWh+gxEqufNNWsrFkkOFxhKBSk3YRuJdcapZBb6afobgpAaQglw8v8Ze1eBkTtctW20jQ== +xterm@4.15.0-beta.10: + version "4.15.0-beta.10" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.15.0-beta.10.tgz#8cda3d7885e8345f2fc6cf9275a43f3833d29acf" + integrity sha512-valoh5ZcY/y7Pe+ffgcSAEFeuZfjzVeUUXcthdxTTsrGEiU1s4QR2EOg4U5jn5wye/Nc6mSfLW3s79R6Ac186w== diff --git a/remote/yarn.lock b/remote/yarn.lock index 8da56ebbd1..f9f6fd2c38 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -123,15 +123,23 @@ resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.4.tgz#40e1c0ad20743fcee1604a7df2c57faf0aa1af87" integrity sha512-Ot53G927ykMF8cQ3/zq4foZtdk+Tt1YpX7aUTHxBU7UHNdkEiBvBfZSq+rnlUmKCJ19VatwPG4mNzvcGpBj4og== +"@parcel/watcher@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.0.tgz#ebe992a4838b35c3da9a568eb95a71cb26ddf551" + integrity sha512-ByalKmRRXNNAhwZ0X1r0XeIhh1jG8zgdlvjgHk9ZV3YxiersEGNQkwew+RfqJbIL4gOJfvC2ey6lg5kaeRainw== + dependencies: + node-addon-api "^3.2.1" + node-gyp-build "^4.3.0" + "@tootallnate/once@1", "@tootallnate/once@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@vscode/vscode-languagedetection@1.0.18": - version "1.0.18" - resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.18.tgz#05d78cbd4b6ba5a0da4f76c88fdc98f67e99786a" - integrity sha512-z98y3RZtuJQbWdqRJNxV6MNv8nJb4WMxjhvxltzfPZhrH+vHcNRiS8GvX1DoJTEV7DN4GrodjHpTh07YGLthDQ== +"@vscode/vscode-languagedetection@1.0.21": + version "1.0.21" + resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" + integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== agent-base@4: version "4.2.0" @@ -176,14 +184,6 @@ ansi_up@^5.1.0: resolved "https://registry.yarnpkg.com/ansi_up/-/ansi_up-5.1.0.tgz#9cf10e6d359bb434bdcfab5ae4c3abfe1617b6db" integrity sha512-3wwu+nJCKBVBwOCurm0uv91lMoVkhFB+3qZQz3U11AmAdDJ4tkw1sNPWJQcVxMVYwe0pGEALOjSBOxdxNc+pNQ== -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - applicationinsights@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" @@ -202,11 +202,6 @@ array-uniq@^1.0.2: version "0.0.29" resolved "https://codeload.github.com/Microsoft/azdataGraph/tar.gz/a4fb6daaffe19cbfaf8d5a33cd44ddedd597e228" -binary-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" - integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== - bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -214,13 +209,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -258,21 +246,6 @@ chartjs-color@^2.1.0: chartjs-color-string "^0.6.0" color-convert "^1.9.3" -chokidar@3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -420,13 +393,6 @@ file-uri-to-path@2: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba" integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -436,11 +402,6 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - ftp@^0.3.10: version "0.3.10" resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" @@ -461,14 +422,12 @@ get-uri@^3.0.2: fs-extra "^8.1.0" ftp "^0.3.10" -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" +graceful-fs@4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== -graceful-fs@4.2.6, graceful-fs@^4.1.6, graceful-fs@^4.2.0: +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== @@ -556,30 +515,6 @@ ip@^1.1.5: resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -686,11 +621,26 @@ ng2-charts@^1.6.0: dependencies: chart.js "^2.6.0" -node-addon-api@*, node-addon-api@^3.0.2: +node-addon-api@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== +node-addon-api@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-addon-api@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + +node-gyp-build@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" + integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== + 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" @@ -698,18 +648,6 @@ node-pty@0.11.0-beta7: dependencies: nan "^2.14.0" -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -nsfw@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/nsfw/-/nsfw-2.1.2.tgz#4fa841e7f7122b60b2e1f61187d1b57ad3403428" - integrity sha512-zGPdt32aJ5b1laK9rvgXQmXGAagrx3VkcMt0JePtu6wBfzC1o4xLCM3kq7FxZxUnxyxYhODyBYzpt3H16FhaGA== - dependencies: - node-addon-api "*" - number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -720,16 +658,6 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -picomatch@^2.0.4: - version "2.0.7" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" - integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== - -picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - plotly.js-dist-min@^1.53.0: version "1.58.4" resolved "https://registry.yarnpkg.com/plotly.js-dist-min/-/plotly.js-dist-min-1.58.4.tgz#6a5b9baf1988b6aca6b20804503e4d70f3085186" @@ -768,13 +696,6 @@ readable-stream@^3.1.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - reflect-metadata@^0.1.8: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" @@ -895,13 +816,6 @@ tas-client-umd@0.1.4: resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.4.tgz#49db4130dd63a8342fabf77185a740fc6a7bea80" integrity sha512-1hFqJeLD3ryNikniIaO7TItlXhS5vx7bJ+wbPDf8o+IifgwwOWK2ARisdEM9SnJd0ccfcwNPG6Po+RiKn5L2hg== -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - tslib@^2.0.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" @@ -929,6 +843,13 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +vscode-nsfw@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-2.1.8.tgz#88f5e56b22b2fd0be582e73eb1158ea8257f6c6c" + integrity sha512-tFnxPIuM65czw/Kjz8KXD88fIJtnCjzQ0ighS0a1yasVv6jKkANAlGffiOitTLMkDjvFCY8OyP6xjarTkpu/VQ== + dependencies: + node-addon-api "^4.2.0" + vscode-oniguruma@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.5.1.tgz#9ca10cd3ada128bd6380344ea28844243d11f695" @@ -954,18 +875,18 @@ 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.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.12.0.tgz#8fee3f892349f2bf1c7ef9743e3bbccb108ad9d7" - integrity sha512-tn+bM7RbVElyuIGjIFyuSZZSuqodDjPNVQeHdo9w7EOIFEOuNtXuZ82s/Sy59lG/gJyMEkXjXjKunbUNNa5kOw== +vscode-ripgrep@^1.12.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.13.2.tgz#8ccebc33f14d54442c4b11962aead163c55b506e" + integrity sha512-RlK9U87EokgHfiOjDQ38ipQQX936gWOcWPQaJpYf+kAkz1PQ1pK2n7nhiscdOmLu6XGjTs7pWFJ/ckonpN7twQ== dependencies: https-proxy-agent "^4.0.0" proxy-from-env "^1.1.0" -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-textmate@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.4.1.tgz#09d566724fc76b60b3ad9791eebf1f0b50f29e5a" + integrity sha512-4CvPHmfuZQaXrcCpathdh6jo7myuR+MU8BvscgQADuponpbqfmu2rwTOtCXhGwwEgStvJF8V4s9FwMKRVLNmKQ== vscode-windows-ca-certs@^0.3.0: version "0.3.0" @@ -979,10 +900,10 @@ vscode-windows-registry@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.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== +windows-process-tree@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.3.2.tgz#8c39f39e7707e09fd74638a7ef644b5f389096d3" + integrity sha512-x8Y4KOV8tUhhPiO0TH7wOMTZ677rw7VEwq+dTuHHiLTClkrNXWSY3XzP6ez3fs2Cab4FajrtmiqRs0jTMZHfyw== dependencies: nan "^2.13.2" @@ -996,35 +917,35 @@ xtend@^4.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -xterm-addon-search@0.9.0-beta.4: - version "0.9.0-beta.4" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.4.tgz#e332f99d5eb5991f8c0e361c9b0d45b23f454323" - integrity sha512-PMzAPtUOjQjJcqpjB2k9BkbjOZPH4PFuQkBtln2599mCPeA9WdA++FpVN6WdBHgeIR5QILoT4pWg0hA8USInzg== +xterm-addon-search@0.9.0-beta.5: + version "0.9.0-beta.5" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.5.tgz#e0e60a203d1c9d6c8af933648a46865dba299302" + integrity sha512-ylfqim0ISBvuuX83LQwgu/06p5GC545QsAo9SssXw03TPpIrcd0zwaVMEnhOftSIzM9EKRRsyx3GbBjgUdiF5w== -xterm-addon-serialize@0.6.0-beta.7: - version "0.6.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.6.0-beta.7.tgz#71363f56acd8ca7256cf4b76d17a03bd4ca16044" - integrity sha512-y4EhWFlqUmPW/UbdMJ9wIhtZF+X8MSLdBdo+ltzpqvKM97SJlmNr4qrAomV81TAJ6sHdxGfIo5mNjyIO7YoQRg== +xterm-addon-serialize@0.7.0-beta.2: + version "0.7.0-beta.2" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.7.0-beta.2.tgz#ced9f664c74ab88448e7b63850721bc272aa6806" + integrity sha512-KuSwdx2AAliUv7SvjKYUKHrB7vscbHLv8QsmwSDI3pgL1BpjyLJ8LR99iFFfuNpPW9CG4TX6adKPIJXtqiN3Vg== -xterm-addon-unicode11@0.3.0-beta.6: - version "0.3.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.6.tgz#8914f377757d5078e7b4daee7d3e2b7428b6edf0" - integrity sha512-Qwa18yMhtacf9Jtxy+UuxHfjIeIjaX9q0LUfHtZU8/Lwjh+bGcn8E8IABVSGvXZgPNKw/4TqEpgLFexn+sfc5g== +xterm-addon-unicode11@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0.tgz#e4435c3c91a5294a7eb8b79c380acbb28a659463" + integrity sha512-x5fHDZT2j9tlTlHnzPHt++9uKZ2kJ/lYQOj3L6xJA22xoJsS8UQRw/5YIFg2FUHqEAbV77Z1fZij/9NycMSH/A== -xterm-addon-webgl@0.12.0-beta.10: - version "0.12.0-beta.10" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.10.tgz#ba23287043da8172f4f9e53babb620f54ad36189" - integrity sha512-mzMOAqgM95FAgzcVzCH/Q0NfN0CTMHVDWCCFyg4B5ZcsuRiQKqQQw0HS+5uOQDtoZEDl2BqGFby7pGpENWGjZQ== +xterm-addon-webgl@0.12.0-beta.15: + version "0.12.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.15.tgz#9ae82127f2a39b3cb7f5ae45a6af223810c933d4" + integrity sha512-LWZ3iLspQOCc26OoT8qa+SuyuIcn2cAMRbBkinOuQCk4aW5kjovIrGovj9yVAcXNvOBnPm3sUqmnwGlN579kDA== -xterm-headless@4.14.0-beta.11: - version "4.14.0-beta.11" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.14.0-beta.11.tgz#c97052e31ab07a50c577cdcf05878e4cff76deec" - integrity sha512-EL3cK0yXvQ9BDYqcAMXGd2NkHFFknYQZ7sWgVq6xWrMcSrOMGfIpNyZ1zlP4V5pUk0+yur52TS4xumJ+fYld5w== +xterm-headless@4.15.0-beta.10: + version "4.15.0-beta.10" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.15.0-beta.10.tgz#2dbcb40dfda7ecfdacc7b63889c80da965480ce7" + integrity sha512-kDAzmaeFX8hAJvbPUJc4dW4SoVBSg4onCVOPyi8QTmxZz1o7I9mX4U7DX1v3PceyfrU27A9k6zXjuTuPjxCCSQ== -xterm@4.14.0-beta.21: - version "4.14.0-beta.21" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.21.tgz#2d50328389cc79021c0202405689955fc18cb703" - integrity sha512-9ELD78FTUL91OBRfNVWh+gxEqufNNWsrFkkOFxhKBSk3YRuJdcapZBb6afobgpAaQglw8v8Ze1eBkTtctW20jQ== +xterm@4.15.0-beta.10: + version "4.15.0-beta.10" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.15.0-beta.10.tgz#8cda3d7885e8345f2fc6cf9275a43f3833d29acf" + integrity sha512-valoh5ZcY/y7Pe+ffgcSAEFeuZfjzVeUUXcthdxTTsrGEiU1s4QR2EOg4U5jn5wye/Nc6mSfLW3s79R6Ac186w== yauzl@^2.9.2: version "2.10.0" diff --git a/resources/linux/debian/postinst.template b/resources/linux/debian/postinst.template index 11eb563363..1639a259e4 100644 --- a/resources/linux/debian/postinst.template +++ b/resources/linux/debian/postinst.template @@ -13,9 +13,8 @@ ln -s /usr/share/@@NAME@@/bin/@@NAME@@ /usr/bin/@@NAME@@ update-alternatives --install /usr/bin/editor editor /usr/bin/@@NAME@@ 0 # Install the desktop entry -if hash desktop-file-install 2>/dev/null; then - desktop-file-install /usr/share/applications/@@NAME@@.desktop - desktop-file-install /usr/share/applications/@@NAME@@-url-handler.desktop +if hash update-desktop-database 2>/dev/null; then + update-desktop-database fi # Update mimetype database to pickup workspace mimetype diff --git a/resources/linux/snap/electron-launch b/resources/linux/snap/electron-launch index 9f4eb6a23b..87011928b4 100644 --- a/resources/linux/snap/electron-launch +++ b/resources/linux/snap/electron-launch @@ -29,6 +29,6 @@ if [ -f "$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders" ]; then fi # Create $XDG_RUNTIME_DIR if not exists (to be removed when https://pad.lv/1656340 is fixed) -[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p "$XDG_RUNTIME_DIR" -m 700 +[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p -m 700 "$XDG_RUNTIME_DIR" exec "$@" diff --git a/resources/server/bin-dev/code-web.js b/resources/server/bin-dev/code-web.js new file mode 100644 index 0000000000..093e6b6bca --- /dev/null +++ b/resources/server/bin-dev/code-web.js @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check + +const cp = require('child_process'); +const path = require('path'); +const os = require('os'); + +const serverArgs = []; + +// Server Config +let PORT = 9888; +let DRIVER = undefined; +let LOGS_PATH = undefined; + +// Workspace Config +let FOLDER = undefined; +let WORKSPACE = undefined; + +// Settings Sync Config +let GITHUB_AUTH_TOKEN = undefined; +let ENABLE_SYNC = false; + +for (let idx = 0; idx <= process.argv.length - 2; idx++) { + const arg = process.argv[idx]; + switch (arg) { + case '--port': PORT = Number(process.argv[idx + 1]); break; + case '--folder': FOLDER = process.argv[idx + 1]; break; + case '--workspace': WORKSPACE = process.argv[idx + 1]; break; + case '--driver': DRIVER = process.argv[idx + 1]; break; + case '--github-auth': GITHUB_AUTH_TOKEN = process.argv[idx + 1]; break; + case '--logsPath': LOGS_PATH = process.argv[idx + 1]; break; + case '--enable-sync': ENABLE_SYNC = true; break; + } +} + +serverArgs.push('--port', String(PORT)); +if (FOLDER) { + serverArgs.push('--folder', FOLDER); +} +if (WORKSPACE) { + serverArgs.push('--workspace', WORKSPACE); +} +if (DRIVER) { + serverArgs.push('--driver', DRIVER); + + // given a DRIVER, we auto-shutdown when tests are done + serverArgs.push('--enable-remote-auto-shutdown', '--remote-auto-shutdown-without-delay'); +} +if (LOGS_PATH) { + serverArgs.push('--logsPath', LOGS_PATH); +} +if (GITHUB_AUTH_TOKEN) { + serverArgs.push('--github-auth', GITHUB_AUTH_TOKEN); +} +if (ENABLE_SYNC) { + serverArgs.push('--enable-sync', true); +} + +// Connection Token +serverArgs.push('--connectionToken', '00000'); + +// Server should really only listen from localhost +serverArgs.push('--host', '127.0.0.1'); + +const env = { ...process.env }; +env['VSCODE_AGENT_FOLDER'] = env['VSCODE_AGENT_FOLDER'] || path.join(os.homedir(), '.vscode-web-dev'); +const entryPoint = path.join(__dirname, '..', '..', '..', 'out', 'vs', 'server', 'main.js'); + +startServer(); + +function startServer() { + const proc = cp.spawn(process.execPath, [entryPoint, ...serverArgs], { env }); + + proc.stdout.on('data', data => { + // Log everything + console.log(data.toString()); + }); + + // Log errors + proc.stderr.on('data', data => { + console.error(data.toString()); + }); +} diff --git a/resources/server/bin-dev/code.cmd b/resources/server/bin-dev/code.cmd new file mode 100644 index 0000000000..ac90678504 --- /dev/null +++ b/resources/server/bin-dev/code.cmd @@ -0,0 +1,6 @@ +@echo off +setlocal +SET VSCODE_PATH=%~dp0..\..\.. +FOR /F "tokens=* USEBACKQ" %%g IN (`where /r "%VSCODE_PATH%\.build\node" node.exe`) do (SET "NODE=%%g") +call "%NODE%" "%VSCODE_PATH%\out\vs\server\cli.js" "Code Server - Dev" "" "" "code.cmd" %* +endlocal diff --git a/resources/server/bin-dev/code.sh b/resources/server/bin-dev/code.sh new file mode 100755 index 0000000000..61e57cb7ab --- /dev/null +++ b/resources/server/bin-dev/code.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + VSCODE_PATH=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) +else + VSCODE_PATH=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) +fi + +PROD_NAME="Code Server - Dev" +VERSION="" +COMMIT="" +EXEC_NAME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" +CLI_SCRIPT="$VSCODE_PATH/out/vs/server/cli.js" +node "$CLI_SCRIPT" "$PROD_NAME" "$VERSION" "$COMMIT" "$EXEC_NAME" "$@" diff --git a/resources/server/bin-dev/helpers/browser.cmd b/resources/server/bin-dev/helpers/browser.cmd new file mode 100644 index 0000000000..4f195ce7ec --- /dev/null +++ b/resources/server/bin-dev/helpers/browser.cmd @@ -0,0 +1,6 @@ +@echo off +setlocal +SET VSCODE_PATH=%~dp0..\..\..\.. +FOR /F "tokens=* USEBACKQ" %%g IN (`where /r "%VSCODE_PATH%\.build\node" node.exe`) do (SET "NODE=%%g") +call "%NODE%" "%VSCODE_PATH%\out\vs\server\cli.js" "Code Server - Dev" "" "" "code.cmd" "--openExternal" %* +endlocal diff --git a/resources/server/bin-dev/helpers/browser.sh b/resources/server/bin-dev/helpers/browser.sh new file mode 100755 index 0000000000..60ff85d6e7 --- /dev/null +++ b/resources/server/bin-dev/helpers/browser.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + VSCODE_PATH=$(dirname $(dirname $(dirname $(dirname $(dirname $(realpath "$0")))))) +else + VSCODE_PATH=$(dirname $(dirname $(dirname $(dirname $(dirname $(readlink -f $0)))))) +fi + +PROD_NAME="Code Server - Dev" +VERSION="" +COMMIT="" +EXEC_NAME="" +CLI_SCRIPT="$VSCODE_PATH/out/vs/server/cli.js" +node "$CLI_SCRIPT" "$PROD_NAME" "$VERSION" "$COMMIT" "$EXEC_NAME" "--openExternal" "$@" diff --git a/resources/server/bin-dev/server.bat b/resources/server/bin-dev/server.bat new file mode 100644 index 0000000000..d6a61b5326 --- /dev/null +++ b/resources/server/bin-dev/server.bat @@ -0,0 +1,43 @@ +@echo off +setlocal + +title VSCode Remote Agent + +pushd %~dp0\..\..\.. + +:: Configuration +set NODE_ENV=development +set VSCODE_DEV=1 + +:: Sync built-in extensions +call yarn download-builtin-extensions + +FOR /F "tokens=*" %%g IN ('node build/lib/node.js') do (SET NODE=%%g) + +:: Download nodejs executable for remote +IF NOT EXIST "%NODE%" ( + call yarn gulp node +) + +:: Launch Agent +set _FIRST_ARG=%1 +if "%_FIRST_ARG:~0,9%"=="--inspect" ( + set INSPECT=%1 + shift +) else ( + set INSPECT= +) + +:loop1 +if "%~1"=="" goto after_loop +set RESTVAR=%RESTVAR% %1 +shift +goto loop1 + +:after_loop + +call "%NODE%" %INSPECT% "out\vs\server\main.js" %RESTVAR% + +popd + +endlocal diff --git a/resources/server/bin-dev/server.sh b/resources/server/bin-dev/server.sh new file mode 100755 index 0000000000..99a47c4fb2 --- /dev/null +++ b/resources/server/bin-dev/server.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) +else + ROOT=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) +fi + +function code() { + cd $ROOT + + # Sync built-in extensions + yarn download-builtin-extensions + + NODE=$(node build/lib/node.js) + + # Download nodejs + if [ ! -f $NODE ]; then + yarn gulp node + fi + + NODE_ENV=development \ + VSCODE_DEV=1 \ + $NODE "$ROOT/out/vs/server/main.js" "$@" +} + +code "$@" diff --git a/resources/server/bin/code.cmd b/resources/server/bin/code.cmd new file mode 100644 index 0000000000..0cbff2f7c2 --- /dev/null +++ b/resources/server/bin/code.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +call "%~dp0..\node" "%~dp0..\out\vs\server\cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" %* +endlocal \ No newline at end of file diff --git a/resources/server/bin/code.sh b/resources/server/bin/code.sh new file mode 100644 index 0000000000..2fadda2f2b --- /dev/null +++ b/resources/server/bin/code.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# +ROOT=$(dirname "$(dirname "$0")") + +APP_NAME="@@APPNAME@@" +VERSION="@@VERSION@@" +COMMIT="@@COMMIT@@" +EXEC_NAME="@@APPNAME@@" +CLI_SCRIPT="$ROOT/out/vs/server/cli.js" +"$ROOT/node" "$CLI_SCRIPT" "$APP_NAME" "$VERSION" "$COMMIT" "$EXEC_NAME" "$@" diff --git a/resources/server/bin/helpers/browser.cmd b/resources/server/bin/helpers/browser.cmd new file mode 100644 index 0000000000..33625f17d4 --- /dev/null +++ b/resources/server/bin/helpers/browser.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +call "%~dp0..\..\node" "%~dp0..\..\out\vs\server\cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" "--openExternal" %* +endlocal diff --git a/resources/server/bin/helpers/browser.sh b/resources/server/bin/helpers/browser.sh new file mode 100644 index 0000000000..2cc3570be9 --- /dev/null +++ b/resources/server/bin/helpers/browser.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# +ROOT=$(dirname "$(dirname "$(dirname "$0")")") + +APP_NAME="@@APPNAME@@" +VERSION="@@VERSION@@" +COMMIT="@@COMMIT@@" +EXEC_NAME="@@APPNAME@@" +CLI_SCRIPT="$ROOT/out/vs/server/cli.js" +"$ROOT/node" "$CLI_SCRIPT" "$APP_NAME" "$VERSION" "$COMMIT" "$EXEC_NAME" "--openExternal" "$@" diff --git a/resources/server/bin/server.cmd b/resources/server/bin/server.cmd new file mode 100644 index 0000000000..3b2c1b7a1e --- /dev/null +++ b/resources/server/bin/server.cmd @@ -0,0 +1,24 @@ +@echo off +setlocal + +set ROOT_DIR=%~dp0 + +set _FIRST_ARG=%1 +if "%_FIRST_ARG:~0,9%"=="--inspect" ( + set INSPECT=%1 + shift +) else ( + set INSPECT= +) + +:loop1 +if "%~1"=="" goto after_loop +set RESTVAR=%RESTVAR% %1 +shift +goto loop1 + +:after_loop + +"%ROOT_DIR%node.exe" %INSPECT% "%ROOT_DIR%out\vs\server\main.js" %RESTVAR% + +endlocal diff --git a/resources/server/bin/server.sh b/resources/server/bin/server.sh new file mode 100644 index 0000000000..66b7ec6381 --- /dev/null +++ b/resources/server/bin/server.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# + +case "$1" in + --inspect*) INSPECT="$1"; shift;; +esac + +ROOT="$(dirname "$0")" + +"$ROOT/node" ${INSPECT:-} "$ROOT/out/vs/server/main.js" "$@" diff --git a/resources/server/code-192.png b/resources/server/code-192.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8646f8282c77316dcfde05b9f339678079a5c5 GIT binary patch literal 2721 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7+9ErRIjnw1`sdZ(btiIVPik{pF~z5Um@8e z$d`ekN{xY`p@o6r7f`6-1p`B=0RzLU1O^7H84L{K1#@-<+5jcg1AIbUf%O0X{{v>9 zUvc(%&6e9@Rr|a5-p^fg{ph{-PLofoHXSzZK0fp4V|&MS%YaH5OM?7@862M7NCR<_ zyxmrx$9H+pg(s_~zt{uxnQDn^L`h0wNvc(H zQ7VvPFfuT-&^0vFH82Y?GO#i+wK6i-HZZU0 t8-nxGO3D+9QW+dm@{>{(JaZG%Q-e|yQz{Ejrh?jy44$rjF6*2UngGKW=cNDu literal 0 HcmV?d00001 diff --git a/resources/server/code-512.png b/resources/server/code-512.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8646f8282c77316dcfde05b9f339678079a5c5 GIT binary patch literal 2721 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7+9ErRIjnw1`sdZ(btiIVPik{pF~z5Um@8e z$d`ekN{xY`p@o6r7f`6-1p`B=0RzLU1O^7H84L{K1#@-<+5jcg1AIbUf%O0X{{v>9 zUvc(%&6e9@Rr|a5-p^fg{ph{-PLofoHXSzZK0fp4V|&MS%YaH5OM?7@862M7NCR<_ zyxmrx$9H+pg(s_~zt{uxnQDn^L`h0wNvc(H zQ7VvPFfuT-&^0vFH82Y?GO#i+wK6i-HZZU0 t8-nxGO3D+9QW+dm@{>{(JaZG%Q-e|yQz{Ejrh?jy44$rjF6*2UngGKW=cNDu literal 0 HcmV?d00001 diff --git a/resources/server/favicon.ico b/resources/server/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f9f0de3eb46f9cf6cab16ce698bef2e6896f2dd8 GIT binary patch literal 34494 zcmeHQy^CE%6dyqg8%dc;UZIdOohctCSf{d5NU48-jW%H`QLqW5*CbHre1GRkaR1Z4j~fm#{O@U-bBN)u$L3n1F%zOc+0mB!`rrTb>YCoL zok_>nAMLp9NpGTo{)?kWdw=}!RSQoK5B9XeEpFEFYqaI_xg_2+t2m=WmD1s88NZhN zb<8TA{#iWTTO1u)0rmvA>buH88?l7^#`9--j$n@R@=%=Ol&k$V(TA=)o&m?bz@>kT zeV!cLTAR0sUx?#<(09@BmB+|Wbjpj~=B(KkOmCHf}0qzAeHJnzjk%r)RS za+&Y|II^T=o!5kraftDT6w)?y45OVHCw%h?ZIK{{uU%{%=!MV}{_FPV_fL+S_am2I zQtglD$gV`s#4g?_-&FpvEw?wvhqS-XrZIJ?_9q_LlOP&hx$Sw1`Vc)=?wbGLADY06 z)6eB^+tY)rHozLXZ~mNKEWYXbkHP%0baPA-K!^=7D8vndqEZzry9`}4WmoJxp zZU5Ki4}1FTZPwZDN4fUr{*1*_`G0rgUF)8j`CfqD3HW46gfL_BdQz95|pjQ_T>3k+4sJcSYAU zwEGmVX++YfETCOgb)NRXzm2yPcSCaQRZ_w^gpfXcwn5CL6mn1Du^+Nf!aA1M4~?5^ zG?8EIA3NS;{A}m^#TwrB9@|%D+r#M^PeY9#$P$~NTbI1NUgWore~h2l`z+2Ta{G4S zL)pKJZ(sEyzkU30`hx4u5Lh5Y$Hvp(yFvVWg4`e>Q*!{@R081wxl&Y;Hj&utHxw;_${Fn|an7AS0i z)f}dCLxq`7fE~9l$8k<;uHm|d_M8?9B#8yY0<8t~ytzJcScQ%W|LyCX9XDafH+{af z{Ic$&OU>N@fWDD|%%j#{@0E4hD4~t$zxqa2{4e8Q=70P-Gh0j=B>pGFHk)j`OyXbp z_!sgWe8~P8_hkxR!~ZGo0Qj83`;QcTc^}cg{P-9D$Dcvprty#XmviO=eZRl)&c@Dv z9)oJ)HR4}QeK~K@zkK}XK8X1}82Bp`5GObzAm%oo z?;Ib){GYo2r~drI$7D%-4CB9~zMPlnU;qB!$9HhH{ZzzN?r)Bd!T(c_|DwOFe`3~k zp=~j@`F!X280J4@3VGih+s5_V1f~tfqQ5Cq$!pQS{{6qCzd5fO{>NG+xbV;Er`pf!aID|QScwD4ek;E literal 0 HcmV?d00001 diff --git a/resources/server/manifest.json b/resources/server/manifest.json new file mode 100644 index 0000000000..38b665c8c3 --- /dev/null +++ b/resources/server/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Code - OSS", + "short_name": "Code- OSS", + "start_url": "/", + "lang": "en-US", + "display": "standalone", + "icons": [ + { + "src": "/code-192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/code-512.png", + "type": "image/png", + "sizes": "512x512" + } + ] +} diff --git a/resources/server/test/test-remote-integration.bat b/resources/server/test/test-remote-integration.bat new file mode 100644 index 0000000000..47ee286f12 --- /dev/null +++ b/resources/server/test/test-remote-integration.bat @@ -0,0 +1,79 @@ +@echo off +setlocal + +pushd %~dp0\..\..\.. + +IF "%~1" == "" ( + set AUTHORITY=vscode-remote://test+test/ + :: backward to forward slashed + set EXT_PATH=%CD:\=/%/extensions + + :: Download nodejs executable for remote + call yarn gulp node +) else ( + set AUTHORITY=%1 + set EXT_PATH=%2 + set VSCODEUSERDATADIR=%3 +) +IF "%VSCODEUSERDATADIR%" == "" ( + set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% +) + +set REMOTE_VSCODE=%AUTHORITY%%EXT_PATH% +set VSCODECRASHDIR=%~dp0\..\..\..\.build\crashes +set VSCODELOGSDIR=%~dp0\..\..\..\.build\logs\remote-integration-tests +set TESTRESOLVER_DATA_FOLDER=%TMP%\testresolverdatafolder-%RANDOM%-%TIME:~6,5% + +if "%VSCODE_REMOTE_SERVER_PATH%"=="" ( + echo "Using remote server out of sources for integration tests" +) else ( + set TESTRESOLVER_INSTALL_BUILTIN_EXTENSION=ms-vscode.vscode-smoketest-check + echo "Using %VSCODE_REMOTE_SERVER_PATH% as server path" +) + +set API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --disable-keytar --disable-inspect --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% + +:: Figure out which Electron to use for running tests +if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( + echo "Storing crash reports into '%VSCODECRASHDIR%'." + echo "Storing log files into '%VSCODELOGSDIR%'." + + :: Tests in the extension host running from sources + call .\scripts\code.bat --folder-uri=%REMOTE_VSCODE%/vscode-api-tests/testWorkspace --extensionDevelopmentPath=%REMOTE_VSCODE%/vscode-api-tests --extensionTestsPath=%REMOTE_VSCODE%/vscode-api-tests/out/singlefolder-tests %API_TESTS_EXTRA_ARGS% + if %errorlevel% neq 0 exit /b %errorlevel% + + call .\scripts\code.bat --file-uri=%REMOTE_VSCODE%/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=%REMOTE_VSCODE%/vscode-api-tests --extensionTestsPath=%REMOTE_VSCODE%/vscode-api-tests/out/workspace-tests %API_TESTS_EXTRA_ARGS% + if %errorlevel% neq 0 exit /b %errorlevel% +) else ( + echo "Storing crash reports into '%VSCODECRASHDIR%'." + echo "Storing log files into '%VSCODELOGSDIR%'." + echo "Using %INTEGRATION_TEST_ELECTRON_PATH% as Electron path" + + :: Run from a built: need to compile all test extensions + :: because we run extension tests from their source folders + :: and the build bundles extensions into .build webpacked + call yarn gulp compile-extension:vscode-api-tests^ + compile-extension:vscode-test-resolver + + :: Configuration for more verbose output + set VSCODE_CLI=1 + set ELECTRON_ENABLE_LOGGING=1 + set ELECTRON_ENABLE_STACK_DUMPING=1 + + :: Tests in the extension host running from built version (both client and server) + call "%INTEGRATION_TEST_ELECTRON_PATH%" --folder-uri=%REMOTE_VSCODE%/vscode-api-tests/testWorkspace --extensionDevelopmentPath=%REMOTE_VSCODE%/vscode-api-tests --extensionTestsPath=%REMOTE_VSCODE%/vscode-api-tests/out/singlefolder-tests %API_TESTS_EXTRA_ARGS% --extensions-dir=%EXT_PATH% --enable-proposed-api=vscode.vscode-test-resolver --enable-proposed-api=vscode.vscode-api-tests --enable-proposed-api=vscode.image-preview + if %errorlevel% neq 0 exit /b %errorlevel% + + call "%INTEGRATION_TEST_ELECTRON_PATH%" --file-uri=%REMOTE_VSCODE%/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=%REMOTE_VSCODE%/vscode-api-tests --extensionTestsPath=%REMOTE_VSCODE%/vscode-api-tests/out/workspace-tests %API_TESTS_EXTRA_ARGS% --extensions-dir=%EXT_PATH% --enable-proposed-api=vscode.vscode-test-resolver --enable-proposed-api=vscode.vscode-api-tests --enable-proposed-api=vscode.image-preview + if %errorlevel% neq 0 exit /b %errorlevel% +) + +IF "%3" == "" ( + rmdir /s /q %VSCODEUSERDATADIR% +) + +rmdir /s /q %TESTRESOLVER_DATA_FOLDER% + +popd + +endlocal diff --git a/resources/server/test/test-remote-integration.sh b/resources/server/test/test-remote-integration.sh new file mode 100755 index 0000000000..09d14d50c9 --- /dev/null +++ b/resources/server/test/test-remote-integration.sh @@ -0,0 +1,114 @@ +#!/bin/bash +set -e + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) + VSCODEUSERDATADIR=`mktemp -d -t 'myuserdatadir'` + TESTRESOLVER_DATA_FOLDER=`mktemp -d -t 'testresolverdatafolder'` +else + ROOT=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) + VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` + TESTRESOLVER_DATA_FOLDER=`mktemp -d 2>/dev/null` + # --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="--disable-dev-shm-usage --use-gl=swiftshader" +fi + +cd $ROOT +if [[ "$1" == "" ]]; then + AUTHORITY=vscode-remote://test+test + EXT_PATH=$ROOT/extensions + # Load remote node + yarn gulp node +else + AUTHORITY=$1 + EXT_PATH=$2 + VSCODEUSERDATADIR=${3:-$VSCODEUSERDATADIR} +fi + +export REMOTE_VSCODE=$AUTHORITY$EXT_PATH +VSCODECRASHDIR=$ROOT/.build/crashes +VSCODELOGSDIR=$ROOT/.build/logs/remote-integration-tests + +# Figure out which Electron to use for running tests +if [ -z "$INTEGRATION_TEST_ELECTRON_PATH" ] +then + echo "Storing crash reports into '$VSCODECRASHDIR'." + echo "Storing log files into '$VSCODELOGSDIR'." + + # code.sh makes sure Test Extensions are compiled + INTEGRATION_TEST_ELECTRON_PATH="./scripts/code.sh" + + # No extra arguments when running out of sources + EXTRA_INTEGRATION_TEST_ARGUMENTS="" +else + echo "Storing crash reports into '$VSCODECRASHDIR'." + echo "Storing log files into '$VSCODELOGSDIR'." + echo "Using $INTEGRATION_TEST_ELECTRON_PATH as Electron path for integration tests" + + # Run from a built: need to compile all test extensions + # because we run extension tests from their source folders + # and the build bundles extensions into .build webpacked + yarn gulp compile-extension:vscode-api-tests \ + compile-extension:vscode-test-resolver \ + compile-extension:markdown-language-features \ + compile-extension:typescript-language-features \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension-media + + # Configuration for more verbose output + export VSCODE_CLI=1 + export ELECTRON_ENABLE_STACK_DUMPING=1 + export ELECTRON_ENABLE_LOGGING=1 + + # Running from a build, we need to enable the vscode-test-resolver extension + EXTRA_INTEGRATION_TEST_ARGUMENTS="--extensions-dir=$EXT_PATH --enable-proposed-api=vscode.vscode-test-resolver --enable-proposed-api=vscode.vscode-api-tests --enable-proposed-api=vscode.image-preview --enable-proposed-api=vscode.git" +fi + +if [ -z "$INTEGRATION_TEST_APP_NAME" ]; then + after_suite() { true; } +else + after_suite() { killall $INTEGRATION_TEST_APP_NAME || true; } +fi + +export TESTRESOLVER_DATA_FOLDER=$TESTRESOLVER_DATA_FOLDER + +# Figure out which remote server to use for running tests +if [ -z "$VSCODE_REMOTE_SERVER_PATH" ] +then + echo "Using remote server out of sources for integration tests" +else + echo "Using $VSCODE_REMOTE_SERVER_PATH as server path for integration tests" + export TESTRESOLVER_INSTALL_BUILTIN_EXTENSION='ms-vscode.vscode-smoketest-check' +fi + +# Tests in the extension host + +API_TESTS_DEFAULT_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --disable-keytar --disable-inspect --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$REMOTE_VSCODE/vscode-api-tests --extensionTestsPath=$REMOTE_VSCODE/vscode-api-tests/out/singlefolder-tests $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --file-uri=$REMOTE_VSCODE/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/vscode-api-tests --extensionTestsPath=$REMOTE_VSCODE/vscode-api-tests/out/workspace-tests $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/typescript-language-features/test-workspace --enable-proposed-api=vscode.typescript-language-features --extensionDevelopmentPath=$REMOTE_VSCODE/typescript-language-features --extensionTestsPath=$REMOTE_VSCODE/typescript-language-features/out/test/unit $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/markdown-language-features/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/markdown-language-features --extensionTestsPath=$REMOTE_VSCODE/markdown-language-features/out/test $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/emmet/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/emmet --extensionTestsPath=$REMOTE_VSCODE/emmet/out/test $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/git --extensionTestsPath=$REMOTE_VSCODE/git/out/test $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +# Clean up +if [[ "$3" == "" ]]; then + rm -rf $VSCODEUSERDATADIR +fi + +rm -rf $TESTRESOLVER_DATA_FOLDER diff --git a/resources/server/test/test-web-integration.bat b/resources/server/test/test-web-integration.bat new file mode 100644 index 0000000000..d063276409 --- /dev/null +++ b/resources/server/test/test-web-integration.bat @@ -0,0 +1,55 @@ +@echo off +setlocal + +pushd %~dp0\..\..\.. + +IF "%~1" == "" ( + set AUTHORITY=vscode-remote://test+test/ + :: backward to forward slashed + set EXT_PATH=%CD:\=/%/extensions + + :: Download nodejs executable for remote + call yarn gulp node +) else ( + set AUTHORITY=%1 + set EXT_PATH=%2 +) + +set REMOTE_VSCODE=%AUTHORITY%%EXT_PATH% + +if "%VSCODE_REMOTE_SERVER_PATH%"=="" ( + echo "Using remote server out of sources for integration web tests" +) else ( + echo "Using %VSCODE_REMOTE_SERVER_PATH% as server path for web integration tests" + + :: Run from a built: need to compile all test extensions + :: because we run extension tests from their source folders + :: and the build bundles extensions into .build webpacked + call yarn gulp compile-extension:vscode-api-tests^ + compile-extension:markdown-language-features^ + compile-extension:typescript-language-features^ + compile-extension:emmet^ + compile-extension:git^ + compile-extension-media +) + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=.\extensions\vscode-api-tests --extensionTestsPath=.\extensions\vscode-api-tests\out\singlefolder-tests %* +if %errorlevel% neq 0 exit /b %errorlevel% + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\vscode-api-tests\testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=.\extensions\vscode-api-tests --extensionTestsPath=.\extensions\vscode-api-tests\out\workspace-tests %* +if %errorlevel% neq 0 exit /b %errorlevel% + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\typescript-language-features\test-workspace --extensionDevelopmentPath=.\extensions\typescript-language-features --extensionTestsPath=.\extensions\typescript-language-features\out\test\unit %* +if %errorlevel% neq 0 exit /b %errorlevel% + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\markdown-language-features\test-workspace --extensionDevelopmentPath=.\extensions\markdown-language-features --extensionTestsPath=.\extensions\markdown-language-features\out\test %* +if %errorlevel% neq 0 exit /b %errorlevel% + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\emmet\test-workspace --extensionDevelopmentPath=.\extensions\emmet --extensionTestsPath=.\extensions\emmet\out\test %* +if %errorlevel% neq 0 exit /b %errorlevel% + +for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set TEMPDIR=%%i +set GITWORKSPACE=%TEMPDIR%\git-%RANDOM% +mkdir %GITWORKSPACE% +call node .\test\integration\browser\out\index.js --workspacePath=%GITWORKSPACE% --extensionDevelopmentPath=.\extensions\git --extensionTestsPath=.\extensions\git\out\test --enable-proposed-api=vscode.git %* +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/resources/server/test/test-web-integration.sh b/resources/server/test/test-web-integration.sh new file mode 100755 index 0000000000..8c6962b424 --- /dev/null +++ b/resources/server/test/test-web-integration.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) +else + ROOT=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) +fi + +cd $ROOT + +if [ -z "$VSCODE_REMOTE_SERVER_PATH" ] +then + echo "Using remote server out of sources for integration web tests" +else + echo "Using $VSCODE_REMOTE_SERVER_PATH as server path for web integration tests" + + # Run from a built: need to compile all test extensions + # because we run extension tests from their source folders + # and the build bundles extensions into .build webpacked + yarn gulp compile-extension:vscode-api-tests \ + compile-extension:markdown-language-features \ + compile-extension:typescript-language-features \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension-media +fi + +# Tests in the extension host +node test/integration/browser/out/index.js --workspacePath $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 "$@" +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests "$@" +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test/unit "$@" +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/markdown-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test "$@" +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/emmet/test-workspace --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test "$@" +node test/integration/browser/out/index.js --workspacePath $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test "$@" diff --git a/resources/server/web.bat b/resources/server/web.bat new file mode 100644 index 0000000000..d131dafffc --- /dev/null +++ b/resources/server/web.bat @@ -0,0 +1,24 @@ +@echo off +setlocal + +title VSCode Web Server + +pushd %~dp0\..\.. + +:: Configuration +set NODE_ENV=development +set VSCODE_DEV=1 + +:: Sync built-in extensions +call yarn download-builtin-extensions + +:: Download nodejs executable for remote +call yarn gulp node + +:: Launch Server +FOR /F "tokens=*" %%g IN ('node build/lib/node.js') do (SET NODE=%%g) +call "%NODE%" resources\server\bin-dev\code-web.js %* + +popd + +endlocal \ No newline at end of file diff --git a/resources/server/web.sh b/resources/server/web.sh new file mode 100755 index 0000000000..da072e5f2d --- /dev/null +++ b/resources/server/web.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(realpath "$0")))) +else + ROOT=$(dirname $(dirname $(dirname $(readlink -f $0)))) +fi + +function code() { + cd $ROOT + + # Sync built-in extensions + yarn download-builtin-extensions + + # Load remote node + yarn gulp node + + NODE=$(node build/lib/node.js) + + NODE_ENV=development \ + VSCODE_DEV=1 \ + $NODE $(dirname "$0")/bin-dev/code-web.js "$@" +} + +code "$@" diff --git a/resources/web/code-web.js b/resources/web/code-web.js index 962fa9bea9..fe7be55771 100644 --- a/resources/web/code-web.js +++ b/resources/web/code-web.js @@ -62,7 +62,7 @@ const args = minimist(process.argv, { if (args.help) { console.log( 'yarn web [options]\n' + - ' --no-launch Do not open VSCode web in the browser\n' + + ' --no-launch Do not open Code in the browser\n' + ' --wrap-iframe Wrap the Web Worker Extension Host in an iframe\n' + ' --enable-sync Enable sync by default\n' + ' --scheme Protocol (https or http)\n' + @@ -239,8 +239,8 @@ const requestHandler = (req, res) => { // manifest res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ - 'name': 'Code Web - OSS', - 'short_name': 'Code Web - OSS', + 'name': 'Code - OSS', + 'short_name': 'Code - OSS', 'start_url': '/', 'lang': 'en-US', 'display': 'standalone' diff --git a/samples/sample-notebook-provider/src/extension.ts b/samples/sample-notebook-provider/src/extension.ts index 2018cb1715..4661b7144f 100644 --- a/samples/sample-notebook-provider/src/extension.ts +++ b/samples/sample-notebook-provider/src/extension.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/samples/sample-notebook-provider/src/sampleController.ts b/samples/sample-notebook-provider/src/sampleController.ts index 8fa060098b..3162a8558c 100644 --- a/samples/sample-notebook-provider/src/sampleController.ts +++ b/samples/sample-notebook-provider/src/sampleController.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/samples/sample-notebook-provider/src/sampleSerializer.ts b/samples/sample-notebook-provider/src/sampleSerializer.ts index bb92bc4fba..366cc5e4ab 100644 --- a/samples/sample-notebook-provider/src/sampleSerializer.ts +++ b/samples/sample-notebook-provider/src/sampleSerializer.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/samples/sample-resource-deployment/src/extension.ts b/samples/sample-resource-deployment/src/extension.ts index 17537c2b45..fe079bd6c5 100644 --- a/samples/sample-resource-deployment/src/extension.ts +++ b/samples/sample-resource-deployment/src/extension.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 rd from 'resource-deployment'; diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 126ee1788e..35eebcc938 100755 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -53,7 +53,7 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Tests in the extension host -set ALL_PLATFORMS_API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --disable-keytar --disable-extensions --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% --no-sandbox +set ALL_PLATFORMS_API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --disable-keytar --disable-extensions --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% --no-sandbox :: {{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% @@ -65,7 +65,7 @@ set ALL_PLATFORMS_API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --cras :: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% :: if %errorlevel% neq 0 exit /b %errorlevel% -:: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\typescript-language-features\test-workspace --extensionDevelopmentPath=%~dp0\..\extensions\typescript-language-features --extensionTestsPath=%~dp0\..\extensions\typescript-language-features\out\test\unit %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% +call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\typescript-language-features\test-workspace --extensionDevelopmentPath=%~dp0\..\extensions\typescript-language-features --extensionTestsPath=%~dp0\..\extensions\typescript-language-features\out\test\unit %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% :: if %errorlevel% neq 0 exit /b %errorlevel% :: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\markdown-language-features\test-workspace --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 0d3cc2cbd7..532f72d045 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -72,7 +72,7 @@ after_suite # Tests in the extension host -ALL_PLATFORMS_API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --disable-extensions --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" +ALL_PLATFORMS_API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --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 @@ -84,7 +84,7 @@ ALL_PLATFORMS_API_TESTS_EXTRA_ARGS="--disable-telemetry --skip-welcome --crash-r # "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS # after_suite -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test/unit $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test/unit $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS # after_suite # "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/markdown-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 7338933e8a..fb46196cb4 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -43,7 +43,6 @@ * }} [options] */ async function load(modulePaths, resultCallback, options) { - const isDev = !!safeProcess.env['VSCODE_DEV']; // Error handler (TODO@sandbox non-sandboxed only) @@ -115,16 +114,14 @@ }; // use a trusted types policy when loading via script tags - if (loaderConfig.preferScriptTags) { - loaderConfig.trustedTypesPolicy = window.trustedTypes?.createPolicy('amdLoader', { - createScriptURL(value) { - if (value.startsWith(window.location.origin)) { - return value; - } - throw new Error(`Invalid script url: ${value}`); + loaderConfig.trustedTypesPolicy = window.trustedTypes?.createPolicy('amdLoader', { + createScriptURL(value) { + if (value.startsWith(window.location.origin)) { + return value; } - }); - } + throw new Error(`Invalid script url: ${value}`); + } + }); // Teach the loader the location of the node modules we use in renderers // This will enable to load these modules via c'); + + const result = renderMarkdown(mds).element; + assert.strictEqual(result.innerHTML, `

abc

`); + }); + + test('Should not render html appended as text', () => { + const mds = new MarkdownString(undefined, { supportHtml: true }); + mds.appendText('abc'); + + const result = renderMarkdown(mds).element; + assert.strictEqual(result.innerHTML, `

a<b>b</b>c

`); + }); + }); }); diff --git a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index e8862c7e5e..04bbab918e 100644 --- a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -659,6 +659,40 @@ suite('IndexTreeModel', () => { assert.deepStrictEqual(toArray(list), ['vscode', '.build', 'github', 'build.js', 'build']); }); + test('recursive filter updates when children change (#133272)', () => { + const list: ITreeNode[] = []; + let query = ''; + const filter = new class implements ITreeFilter { + filter(element: string): TreeVisibility { + return element.includes(query) ? TreeVisibility.Visible : TreeVisibility.Recurse; + } + }; + + const model = new IndexTreeModel('test', toList(list), 'root', { filter }); + + model.splice([0], 0, [ + { + element: 'a', + children: [ + { element: 'b' }, + ], + }, + ]); + + assert.deepStrictEqual(toArray(list), ['a', 'b']); + query = 'visible'; + model.refilter(); + assert.deepStrictEqual(toArray(list), []); + + model.splice([0, 0, 0], 0, [ + { + element: 'visible', children: [] + }, + ]); + + assert.deepStrictEqual(toArray(list), ['a', 'b', 'visible']); + }); + test('recursive filter with collapse', () => { const list: ITreeNode[] = []; let query = new RegExp(''); diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index b8248cf791..ccf85f06fb 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -940,6 +940,44 @@ suite('Async', () => { }); }); + suite('Promises.withAsyncBody', () => { + test('basics', async () => { + + const p1 = async.Promises.withAsyncBody(async (resolve, reject) => { + resolve(1); + }); + + const p2 = async.Promises.withAsyncBody(async (resolve, reject) => { + reject(new Error('error')); + }); + + const p3 = async.Promises.withAsyncBody(async (resolve, reject) => { + throw new Error('error'); + }); + + const r1 = await p1; + assert.strictEqual(r1, 1); + + let e2: Error | undefined = undefined; + try { + await p2; + } catch (error) { + e2 = error; + } + + assert.ok(e2 instanceof Error); + + let e3: Error | undefined = undefined; + try { + await p3; + } catch (error) { + e3 = error; + } + + assert.ok(e3 instanceof Error); + }); + }); + suite('ThrottledWorker', () => { function assertArrayEquals(actual: unknown[], expected: unknown[]) { diff --git a/src/vs/base/test/common/codicon.test.ts b/src/vs/base/test/common/codicon.test.ts deleted file mode 100644 index 192c855f2a..0000000000 --- a/src/vs/base/test/common/codicon.test.ts +++ /dev/null @@ -1,28 +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 { getCodiconAriaLabel } from 'vs/base/common/codicons'; - -suite('Codicon', () => { - 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'], - ]); - - for (const [input, expected] of testCases) { - assert.strictEqual(getCodiconAriaLabel(input), expected); - } - }); -}); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 79ac02c3de..a0b84f6c87 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors'; -import { AsyncEmitter, DebounceEmitter, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, PauseableEmitter, Relay } from 'vs/base/common/event'; +import { AsyncEmitter, DebounceEmitter, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, MicrotaskEmitter, PauseableEmitter, Relay } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; namespace Samples { @@ -274,6 +274,29 @@ suite('Event', function () { assert.strictEqual(sum, 3); }); + test('Microtask Emitter', (done) => { + let count = 0; + assert.strictEqual(count, 0); + const emitter = new MicrotaskEmitter(); + const listener = emitter.event(() => { + count++; + }); + emitter.fire(); + assert.strictEqual(count, 0); + emitter.fire(); + assert.strictEqual(count, 0); + // Should wait until the event loop ends and therefore be the last thing called + setTimeout(() => { + assert.strictEqual(count, 3); + done(); + }, 0); + queueMicrotask(() => { + assert.strictEqual(count, 2); + count++; + listener.dispose(); + }); + }); + test('Emitter - In Order Delivery', function () { const a = new Emitter(); const listener2Events: string[] = []; diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 32a160f7be..182a42d9bb 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { compareItemsByFuzzyScore, FuzzyScore, FuzzyScore2, IItemAccessor, IItemScore, pieceToQuery, prepareQuery, scoreFuzzy, scoreFuzzy2, scoreItemFuzzy } from 'vs/base/common/fuzzyScorer'; +import { compareItemsByFuzzyScore, FuzzyScore, FuzzyScore2, FuzzyScorerCache, IItemAccessor, IItemScore, pieceToQuery, prepareQuery, scoreFuzzy, scoreFuzzy2, scoreItemFuzzy } from 'vs/base/common/fuzzyScorer'; import { Schemas } from 'vs/base/common/network'; import { basename, dirname, posix, sep, win32 } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; @@ -76,10 +76,10 @@ class NullAccessorClass implements IItemAccessor { } } -function _doScore(target: string, query: string, fuzzy: boolean): FuzzyScore { +function _doScore(target: string, query: string, allowNonContiguousMatches?: boolean): FuzzyScore { const preparedQuery = prepareQuery(query); - return scoreFuzzy(target, preparedQuery.normalized, preparedQuery.normalizedLowercase, fuzzy); + return scoreFuzzy(target, preparedQuery.normalized, preparedQuery.normalizedLowercase, allowNonContiguousMatches ?? !preparedQuery.expectContiguousMatch); } function _doScore2(target: string, query: string, matchOffset: number = 0): FuzzyScore2 { @@ -88,12 +88,12 @@ function _doScore2(target: string, query: string, matchOffset: number = 0): Fuzz return scoreFuzzy2(target, preparedQuery, 0, matchOffset); } -function scoreItem(item: T, query: string, fuzzy: boolean, accessor: IItemAccessor): IItemScore { - return scoreItemFuzzy(item, prepareQuery(query), fuzzy, accessor, Object.create(null)); +function scoreItem(item: T, query: string, allowNonContiguousMatches: boolean, accessor: IItemAccessor, cache: FuzzyScorerCache = Object.create(null)): IItemScore { + return scoreItemFuzzy(item, prepareQuery(query), allowNonContiguousMatches, accessor, cache); } -function compareItemsByScore(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: IItemAccessor): number { - return compareItemsByFuzzyScore(itemA, itemB, prepareQuery(query), fuzzy, accessor, Object.create(null)); +function compareItemsByScore(itemA: T, itemB: T, query: string, allowNonContiguousMatches: boolean, accessor: IItemAccessor): number { + return compareItemsByFuzzyScore(itemA, itemB, prepareQuery(query), allowNonContiguousMatches, accessor, Object.create(null)); } const NullAccessor = new NullAccessorClass(); @@ -268,6 +268,17 @@ suite('Fuzzy Scorer', () => { assert.strictEqual(res4.descriptionMatch![1].end, 14); }); + test('scoreItem - multiple with cache yields different results', function () { + const resource = URI.file('/xyz/some/path/someFile123.txt'); + const cache = {}; + let res1 = scoreItem(resource, 'xyz sm', true, ResourceAccessor, cache); + assert.ok(res1.score); + + // from the cache's perspective this should be a totally different query + let res2 = scoreItem(resource, 'xyz "sm"', true, ResourceAccessor, cache); + assert.ok(!res2.score); + }); + test('scoreItem - invalid input', function () { let res = scoreItem(null, null!, true, ResourceAccessor); @@ -392,6 +403,13 @@ suite('Fuzzy Scorer', () => { } }); + test('scoreItem - ensure upper case bonus only applies on non-consecutive matches (bug #134723)', function () { + const resourceWithUpper = URI.file('ASDFasdfasdf'); + const resourceAllLower = URI.file('asdfasdfasdf'); + + assert.ok(scoreItem(resourceAllLower, 'asdf', true, ResourceAccessor).score > scoreItem(resourceWithUpper, 'asdf', true, ResourceAccessor).score); + }); + test('compareItemsByScore - identity', function () { const resourceA = URI.file('/some/path/fileA.txt'); const resourceB = URI.file('/some/path/other/fileB.txt'); @@ -1082,11 +1100,11 @@ suite('Fuzzy Scorer', () => { assert.strictEqual(prepareQuery('model Tester.ts').original, 'model Tester.ts'); assert.strictEqual(prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); assert.strictEqual(prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); - assert.strictEqual(prepareQuery('model Tester.ts').expectExactMatch, false); // doesn't have quotes in it + assert.strictEqual(prepareQuery('model Tester.ts').expectContiguousMatch, false); // doesn't have quotes in it assert.strictEqual(prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); assert.strictEqual(prepareQuery('ModelTester.ts').containsPathSeparator, false); assert.strictEqual(prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); - assert.strictEqual(prepareQuery('"hello"').expectExactMatch, true); + assert.strictEqual(prepareQuery('"hello"').expectContiguousMatch, true); assert.strictEqual(prepareQuery('"hello"').normalized, 'hello'); // with spaces @@ -1215,4 +1233,21 @@ suite('Fuzzy Scorer', () => { assert.ok(typeof score[0] === 'number'); assert.ok(score[1].length > 0); }); + + test('Using quotes should expect contiguous matches match', function () { + // missing the "i" in the query + assert.strictEqual(_doScore('contiguous', '"contguous"')[0], 0); + + const score = _doScore('contiguous', '"contiguous"'); + assert.strictEqual(score[0], 253); + }); + + test('Using quotes should highlight contiguous indexes', function () { + const score = _doScore('2021-7-26.md', '"26"'); + assert.strictEqual(score[0], 13); + + // The indexes of the 2 and 6 of "26" + assert.strictEqual(score[1][0], 7); + assert.strictEqual(score[1][1], 8); + }); }); diff --git a/src/vs/base/test/common/keyCodes.test.ts b/src/vs/base/test/common/keyCodes.test.ts index 254cdd2894..7ca2490003 100644 --- a/src/vs/base/test/common/keyCodes.test.ts +++ b/src/vs/base/test/common/keyCodes.test.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ChordKeybinding, createKeybinding, Keybinding, KeyChord, KeyCode, KeyMod, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { EVENT_KEY_CODE_MAP, IMMUTABLE_CODE_TO_KEY_CODE, IMMUTABLE_KEY_CODE_TO_CODE, KeyChord, KeyCode, KeyCodeUtils, KeyMod, NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE, ScanCode, ScanCodeUtils } from 'vs/base/common/keyCodes'; +import { ChordKeybinding, createKeybinding, Keybinding, SimpleKeybinding } from 'vs/base/common/keybindings'; import { OperatingSystem } from 'vs/base/common/platform'; suite('keyCodes', () => { @@ -13,6 +14,41 @@ suite('keyCodes', () => { assert.deepStrictEqual(createKeybinding(k, OS), expected); } + test('mapping for Minus', () => { + // [147, 83, 0, ScanCode.Minus, 'Minus', KeyCode.US_MINUS, '-', 189, 'VK_OEM_MINUS', '-', 'OEM_MINUS'], + assert.strictEqual(EVENT_KEY_CODE_MAP[189], KeyCode.Minus); + assert.strictEqual(NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE['VK_OEM_MINUS'], KeyCode.Minus); + assert.strictEqual(ScanCodeUtils.lowerCaseToEnum('minus'), ScanCode.Minus); + assert.strictEqual(ScanCodeUtils.toEnum('Minus'), ScanCode.Minus); + assert.strictEqual(ScanCodeUtils.toString(ScanCode.Minus), 'Minus'); + assert.strictEqual(IMMUTABLE_CODE_TO_KEY_CODE[ScanCode.Minus], KeyCode.DependsOnKbLayout); + assert.strictEqual(IMMUTABLE_KEY_CODE_TO_CODE[KeyCode.Minus], ScanCode.DependsOnKbLayout); + assert.strictEqual(KeyCodeUtils.toString(KeyCode.Minus), '-'); + assert.strictEqual(KeyCodeUtils.fromString('-'), KeyCode.Minus); + assert.strictEqual(KeyCodeUtils.toUserSettingsUS(KeyCode.Minus), '-'); + assert.strictEqual(KeyCodeUtils.toUserSettingsGeneral(KeyCode.Minus), 'OEM_MINUS'); + assert.strictEqual(KeyCodeUtils.fromUserSettings('-'), KeyCode.Minus); + assert.strictEqual(KeyCodeUtils.fromUserSettings('OEM_MINUS'), KeyCode.Minus); + assert.strictEqual(KeyCodeUtils.fromUserSettings('oem_minus'), KeyCode.Minus); + }); + + test('mapping for Space', () => { + // [21, 10, 1, ScanCode.Space, 'Space', KeyCode.Space, 'Space', 32, 'VK_SPACE', empty, empty], + assert.strictEqual(EVENT_KEY_CODE_MAP[32], KeyCode.Space); + assert.strictEqual(NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE['VK_SPACE'], KeyCode.Space); + assert.strictEqual(ScanCodeUtils.lowerCaseToEnum('space'), ScanCode.Space); + assert.strictEqual(ScanCodeUtils.toEnum('Space'), ScanCode.Space); + assert.strictEqual(ScanCodeUtils.toString(ScanCode.Space), 'Space'); + assert.strictEqual(IMMUTABLE_CODE_TO_KEY_CODE[ScanCode.Space], KeyCode.Space); + assert.strictEqual(IMMUTABLE_KEY_CODE_TO_CODE[KeyCode.Space], ScanCode.Space); + assert.strictEqual(KeyCodeUtils.toString(KeyCode.Space), 'Space'); + assert.strictEqual(KeyCodeUtils.fromString('Space'), KeyCode.Space); + assert.strictEqual(KeyCodeUtils.toUserSettingsUS(KeyCode.Space), 'Space'); + assert.strictEqual(KeyCodeUtils.toUserSettingsGeneral(KeyCode.Space), 'Space'); + assert.strictEqual(KeyCodeUtils.fromUserSettings('Space'), KeyCode.Space); + assert.strictEqual(KeyCodeUtils.fromUserSettings('space'), KeyCode.Space); + }); + test('MAC binary encoding', () => { function test(expected: Keybinding | null, k: number): void { @@ -46,10 +82,10 @@ suite('keyCodes', () => { ); test( new ChordKeybinding([ - new SimpleKeybinding(false, false, false, true, KeyCode.KEY_Y), - new SimpleKeybinding(false, false, false, false, KeyCode.KEY_Z) + new SimpleKeybinding(false, false, false, true, KeyCode.KeyY), + new SimpleKeybinding(false, false, false, false, KeyCode.KeyZ) ]), - KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z) + KeyChord(KeyMod.CtrlCmd | KeyCode.KeyY, KeyCode.KeyZ) ); }); @@ -88,10 +124,10 @@ suite('keyCodes', () => { ); test( new ChordKeybinding([ - new SimpleKeybinding(true, false, false, false, KeyCode.KEY_Y), - new SimpleKeybinding(false, false, false, false, KeyCode.KEY_Z) + new SimpleKeybinding(true, false, false, false, KeyCode.KeyY), + new SimpleKeybinding(false, false, false, false, KeyCode.KeyZ) ]), - KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z) + KeyChord(KeyMod.CtrlCmd | KeyCode.KeyY, KeyCode.KeyZ) ); }); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index bef44445d4..5debead601 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -428,7 +428,20 @@ suite('Map', () => { assert.strictEqual(iter.hasNext(), false); }); - function assertTernarySearchTree(trie: TernarySearchTree, ...elements: [string, E][]) { + function assertTstDfs(trie: TernarySearchTree, ...elements: [string, E][]) { + + assert.ok(trie._isBalanced(), 'TST is not balanced'); + + let i = 0; + for (let [key, value] of trie) { + const expected = elements[i++]; + assert.ok(expected); + assert.strictEqual(key, expected[0]); + assert.strictEqual(value, expected[1]); + } + + assert.strictEqual(i, elements.length); + const map = new Map(); for (const [key, value] of elements) { map.set(key, value); @@ -452,6 +465,7 @@ suite('Map', () => { iterCount++; } assert.strictEqual(map.size, iterCount); + } test('TernarySearchTree - set', function () { @@ -460,17 +474,17 @@ suite('Map', () => { trie.set('foobar', 1); trie.set('foobaz', 2); - assertTernarySearchTree(trie, ['foobar', 1], ['foobaz', 2]); // longer + assertTstDfs(trie, ['foobar', 1], ['foobaz', 2]); // longer trie = TernarySearchTree.forStrings(); trie.set('foobar', 1); trie.set('fooba', 2); - assertTernarySearchTree(trie, ['foobar', 1], ['fooba', 2]); // shorter + assertTstDfs(trie, ['fooba', 2], ['foobar', 1]); // shorter trie = TernarySearchTree.forStrings(); trie.set('foo', 1); trie.set('foo', 2); - assertTernarySearchTree(trie, ['foo', 2]); + assertTstDfs(trie, ['foo', 2]); trie = TernarySearchTree.forStrings(); trie.set('foo', 1); @@ -479,12 +493,12 @@ suite('Map', () => { trie.set('foob', 4); trie.set('bazz', 5); - assertTernarySearchTree(trie, - ['foo', 1], - ['foobar', 2], + assertTstDfs(trie, ['bar', 3], + ['bazz', 5], + ['foo', 1], ['foob', 4], - ['bazz', 5] + ['foobar', 2], ); }); @@ -494,6 +508,7 @@ suite('Map', () => { trie.set('foo', 1); trie.set('foobar', 2); trie.set('foobaz', 3); + assertTstDfs(trie, ['foo', 1], ['foobar', 2], ['foobaz', 3]); assert.strictEqual(trie.findSubstr('f'), undefined); assert.strictEqual(trie.findSubstr('z'), undefined); @@ -510,6 +525,7 @@ suite('Map', () => { trie.set('foo', 1); trie.set('bar', 2); trie.set('foobar', 3); + assertTstDfs(trie, ['bar', 2], ['foo', 1], ['foobar', 3]); assert.strictEqual(trie.get('foo'), 1); assert.strictEqual(trie.get('bar'), 2); @@ -540,11 +556,11 @@ suite('Map', () => { trie.set('foo', 1); trie.set('foobar', 2); trie.set('bar', 3); - assertTernarySearchTree(trie, ['foo', 1], ['foobar', 2], ['bar', 3]); + assertTstDfs(trie, ['bar', 3], ['foo', 1], ['foobar', 2]); trie.delete('foo'); - assertTernarySearchTree(trie, ['foobar', 2], ['bar', 3]); + assertTstDfs(trie, ['bar', 3], ['foobar', 2]); trie.delete('foobar'); - assertTernarySearchTree(trie, ['bar', 3]); + assertTstDfs(trie, ['bar', 3]); // superstr-delete trie = new TernarySearchTree(new StringIterator()); @@ -553,7 +569,7 @@ suite('Map', () => { trie.set('bar', 3); trie.set('foobarbaz', 4); trie.deleteSuperstr('foo'); - assertTernarySearchTree(trie, ['foo', 1], ['bar', 3]); + assertTstDfs(trie, ['bar', 3], ['foo', 1]); trie = new TernarySearchTree(new StringIterator()); trie.set('foo', 1); @@ -561,7 +577,7 @@ suite('Map', () => { trie.set('bar', 3); trie.set('foobarbaz', 4); trie.deleteSuperstr('fo'); - assertTernarySearchTree(trie, ['bar', 3]); + assertTstDfs(trie, ['bar', 3]); // trie = new TernarySearchTree(new StringIterator()); // trie.set('foo', 1); @@ -594,6 +610,143 @@ suite('Map', () => { assert.strictEqual(trie.findSubstr('/user/foo/bar/far/boo'), 1); }); + test('TernarySearchTree - (AVL) set', function () { + { + // rotate left + let trie = new TernarySearchTree(new PathIterator()); + trie.set('/fileA', 1); + trie.set('/fileB', 2); + trie.set('/fileC', 3); + assertTstDfs(trie, ['/fileA', 1], ['/fileB', 2], ['/fileC', 3]); + } + + { + // rotate left (inside middle) + let trie = new TernarySearchTree(new PathIterator()); + trie.set('/foo/fileA', 1); + trie.set('/foo/fileB', 2); + trie.set('/foo/fileC', 3); + assertTstDfs(trie, ['/foo/fileA', 1], ['/foo/fileB', 2], ['/foo/fileC', 3]); + } + + { + // rotate right + let trie = new TernarySearchTree(new PathIterator()); + trie.set('/fileC', 3); + trie.set('/fileB', 2); + trie.set('/fileA', 1); + assertTstDfs(trie, ['/fileA', 1], ['/fileB', 2], ['/fileC', 3]); + } + + { + // rotate right (inside middle) + let trie = new TernarySearchTree(new PathIterator()); + trie.set('/mid/fileC', 3); + trie.set('/mid/fileB', 2); + trie.set('/mid/fileA', 1); + assertTstDfs(trie, ['/mid/fileA', 1], ['/mid/fileB', 2], ['/mid/fileC', 3]); + } + + { + // rotate right, left + let trie = new TernarySearchTree(new PathIterator()); + trie.set('/fileD', 7); + trie.set('/fileB', 2); + trie.set('/fileG', 42); + trie.set('/fileF', 24); + trie.set('/fileZ', 73); + trie.set('/fileE', 15); + assertTstDfs(trie, ['/fileB', 2], ['/fileD', 7], ['/fileE', 15], ['/fileF', 24], ['/fileG', 42], ['/fileZ', 73]); + } + + { + // rotate left, right + let trie = new TernarySearchTree(new PathIterator()); + trie.set('/fileJ', 42); + trie.set('/fileZ', 73); + trie.set('/fileE', 15); + trie.set('/fileB', 2); + trie.set('/fileF', 7); + trie.set('/fileG', 1); + assertTstDfs(trie, ['/fileB', 2], ['/fileE', 15], ['/fileF', 7], ['/fileG', 1], ['/fileJ', 42], ['/fileZ', 73]); + } + }); + + test('TernarySearchTree - (BST) delete', function () { + + let trie = new TernarySearchTree(new StringIterator()); + + // delete root + trie.set('d', 1); + assertTstDfs(trie, ['d', 1]); + trie.delete('d'); + assertTstDfs(trie); + + // delete node with two element + trie.clear(); + trie.set('d', 1); + trie.set('b', 1); + trie.set('f', 1); + assertTstDfs(trie, ['b', 1], ['d', 1], ['f', 1]); + trie.delete('d'); + assertTstDfs(trie, ['b', 1], ['f', 1]); + + // single child node + trie.clear(); + trie.set('d', 1); + trie.set('b', 1); + trie.set('f', 1); + trie.set('e', 1); + assertTstDfs(trie, ['b', 1], ['d', 1], ['e', 1], ['f', 1]); + trie.delete('f'); + assertTstDfs(trie, ['b', 1], ['d', 1], ['e', 1]); + }); + + test('TernarySearchTree - (AVL) delete', function () { + + let trie = new TernarySearchTree(new StringIterator()); + + trie.clear(); + trie.set('d', 1); + trie.set('b', 1); + trie.set('f', 1); + trie.set('e', 1); + trie.set('z', 1); + assertTstDfs(trie, ['b', 1], ['d', 1], ['e', 1], ['f', 1], ['z', 1]); + + // right, right + trie.delete('b'); + assertTstDfs(trie, ['d', 1], ['e', 1], ['f', 1], ['z', 1]); + + trie.clear(); + trie.set('d', 1); + trie.set('c', 1); + trie.set('f', 1); + trie.set('a', 1); + trie.set('b', 1); + assertTstDfs(trie, ['a', 1], ['b', 1], ['c', 1], ['d', 1], ['f', 1]); + + // left, left + trie.delete('f'); + assertTstDfs(trie, ['a', 1], ['b', 1], ['c', 1], ['d', 1]); + + // mid + trie.clear(); + trie.set('a', 1); + trie.set('ad', 1); + trie.set('ab', 1); + trie.set('af', 1); + trie.set('ae', 1); + trie.set('az', 1); + assertTstDfs(trie, ['a', 1], ['ab', 1], ['ad', 1], ['ae', 1], ['af', 1], ['az', 1]); + + trie.delete('ab'); + assertTstDfs(trie, ['a', 1], ['ad', 1], ['ae', 1], ['af', 1], ['az', 1]); + + trie.delete('a'); + assertTstDfs(trie, ['ad', 1], ['ae', 1], ['af', 1], ['az', 1]); + }); + test('TernarySearchTree (PathSegments) - lookup', function () { const map = new TernarySearchTree(new PathIterator()); @@ -656,18 +809,18 @@ suite('Map', () => { map.set('/user/foo/flip/flop', 3); map.set('/usr/foo', 4); - assertTernarySearchTree(map, - ['/user/foo/bar', 1], + assertTstDfs(map, ['/user/foo', 2], + ['/user/foo/bar', 1], ['/user/foo/flip/flop', 3], ['/usr/foo', 4], ); // not a segment map.deleteSuperstr('/user/fo'); - assertTernarySearchTree(map, - ['/user/foo/bar', 1], + assertTstDfs(map, ['/user/foo', 2], + ['/user/foo/bar', 1], ['/user/foo/flip/flop', 3], ['/usr/foo', 4], ); @@ -678,8 +831,9 @@ suite('Map', () => { map.set('/user/foo/flip/flop', 3); map.set('/usr/foo', 4); map.deleteSuperstr('/user/foo'); - assertTernarySearchTree(map, - ['/user/foo', 2], ['/usr/foo', 4], + assertTstDfs(map, + ['/user/foo', 2], + ['/usr/foo', 4], ); }); @@ -764,9 +918,6 @@ 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(); @@ -776,6 +927,9 @@ 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); @@ -856,20 +1010,20 @@ suite('Map', () => { map.set('config.foo.flip.flop', 3); map.set('boo', 4); - assertTernarySearchTree(map, - ['config.foo.bar', 1], - ['config.foo', 2], - ['config.foo.flip.flop', 3], + assertTstDfs(map, ['boo', 4], + ['config.foo', 2], + ['config.foo.bar', 1], + ['config.foo.flip.flop', 3], ); // not a segment map.deleteSuperstr('config.fo'); - assertTernarySearchTree(map, - ['config.foo.bar', 1], - ['config.foo', 2], - ['config.foo.flip.flop', 3], + assertTstDfs(map, ['boo', 4], + ['config.foo', 2], + ['config.foo.bar', 1], + ['config.foo.flip.flop', 3], ); // delete a segment @@ -878,11 +1032,24 @@ suite('Map', () => { map.set('config.foo.flip.flop', 3); map.set('config.boo', 4); map.deleteSuperstr('config.foo'); - assertTernarySearchTree(map, - ['config.foo', 2], ['boo', 4], + assertTstDfs(map, + ['boo', 4], + ['config.foo', 2], ); }); + test('TST, fill', function () { + const tst = TernarySearchTree.forStrings(); + + const keys = ['foo', 'bar', 'bang', 'bazz']; + Object.freeze(keys); + tst.fill(true, keys); + + for (let key of keys) { + assert.ok(tst.get(key), key); + } + }); + test('ResourceMap - basics', function () { const map = new ResourceMap(); @@ -1022,6 +1189,25 @@ suite('Map', () => { assert.strictEqual(map.get(windowsFile), 'true'); assert.strictEqual(map.get(uncFile), 'true'); }); + + test('ResourceMap - files (ignorecase, BUT preservecase)', function () { + const map = new ResourceMap(uri => extUriIgnorePathCase.getComparisonKey(uri)); + + const fileA = URI.parse('file://some/filea'); + const fileAUpper = URI.parse('file://SOME/FILEA'); + + map.set(fileA, 1); + assert.strictEqual(map.get(fileA), 1); + assert.strictEqual(map.get(fileAUpper), 1); + assert.deepStrictEqual(Array.from(map.keys()).map(String), [fileA].map(String)); + assert.deepStrictEqual(Array.from(map), [[fileA, 1]]); + + map.set(fileAUpper, 1); + assert.strictEqual(map.get(fileA), 1); + assert.strictEqual(map.get(fileAUpper), 1); + assert.deepStrictEqual(Array.from(map.keys()).map(String), [fileAUpper].map(String)); + assert.deepStrictEqual(Array.from(map), [[fileAUpper, 1]]); + }); }); diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 9b6896c556..ec48ad8e83 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { toSlashes } from 'vs/base/common/extpath'; import { posix, win32 } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; -import { addTrailingPathSeparator, basename, dirname, distinctParents, extname, extUri, extUriIgnorePathCase, hasTrailingPathSeparator, isAbsolutePath, joinPath, normalizePath, relativePath, removeTrailingPathSeparator, resolvePath } from 'vs/base/common/resources'; +import { addTrailingPathSeparator, basename, dirname, distinctParents, extUri, extUriIgnorePathCase, hasTrailingPathSeparator, isAbsolutePath, joinPath, normalizePath, relativePath, removeTrailingPathSeparator, resolvePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -429,11 +429,4 @@ suite('Resources', () => { assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19'); assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20'); }); - - test('query parameters stripped from extname', () => { - const uriWithQueryParam = URI.file('/test/project/test.txt?q=1'); - const uriWithoutQueryParam = URI.file('/test/project/test.txt'); - assert.strictEqual(extname(uriWithQueryParam), '.txt'); - assert.strictEqual(extname(uriWithoutQueryParam), '.txt'); - }); }); diff --git a/src/vs/editor/contrib/inlineCompletions/test/timeTravelScheduler.ts b/src/vs/base/test/common/timeTravelScheduler.ts similarity index 89% rename from src/vs/editor/contrib/inlineCompletions/test/timeTravelScheduler.ts rename to src/vs/base/test/common/timeTravelScheduler.ts index cd6f2ceb75..27b534b12d 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/timeTravelScheduler.ts +++ b/src/vs/base/test/common/timeTravelScheduler.ts @@ -148,16 +148,18 @@ export class AsyncSchedulerProcessor extends Disposable { public get history(): readonly ScheduledTask[] { return this._history; } private readonly maxTaskCount: number; + private readonly useSetImmediate: boolean; private readonly queueEmptyEmitter = new Emitter(); public readonly onTaskQueueEmpty = this.queueEmptyEmitter.event; private lastError: Error | undefined; - constructor(private readonly scheduler: TimeTravelScheduler, options?: { maxTaskCount?: number }) { + constructor(private readonly scheduler: TimeTravelScheduler, options?: { useSetImmediate?: boolean; maxTaskCount?: number }) { super(); this.maxTaskCount = options && options.maxTaskCount ? options.maxTaskCount : 100; + this.useSetImmediate = options && options.useSetImmediate ? options.useSetImmediate : false; this._register(scheduler.onTaskScheduled(() => { if (this.isProcessing) { @@ -173,7 +175,11 @@ export class AsyncSchedulerProcessor extends Disposable { // This allows promises created by a previous task to settle and schedule tasks before the next task is run. // Tasks scheduled in those promises might have to run before the current next task. Promise.resolve().then(() => { - originalGlobalValues.setTimeout(() => this.process()); + if (this.useSetImmediate) { + originalGlobalValues.setImmediate(() => this.process()); + } else { + originalGlobalValues.setTimeout(() => this.process()); + } }); } @@ -182,9 +188,9 @@ export class AsyncSchedulerProcessor extends Disposable { if (executedTask) { this._history.push(executedTask); - if (history.length >= this.maxTaskCount && this.scheduler.hasScheduledTasks) { - const lastTasks = this._history.slice(Math.max(0, history.length - 10)).map(h => `${h.source.toString()}: ${h.source.stackTrace}`); - let e = new Error(`Queue did not get empty after processing ${history.length} items. These are the last ${lastTasks.length} scheduled tasks:\n${lastTasks.join('\n\n\n')}`); + if (this.history.length >= this.maxTaskCount && this.scheduler.hasScheduledTasks) { + const lastTasks = this._history.slice(Math.max(0, this.history.length - 10)).map(h => `${h.source.toString()}: ${h.source.stackTrace}`); + let e = new Error(`Queue did not get empty after processing ${this.history.length} items. These are the last ${lastTasks.length} scheduled tasks:\n${lastTasks.join('\n\n\n')}`); this.lastError = e; throw e; } @@ -217,14 +223,14 @@ export class AsyncSchedulerProcessor extends Disposable { } -export async function runWithFakedTimers(options: { useFakeTimers?: boolean, maxTaskCount?: number }, fn: () => Promise): Promise { +export async function runWithFakedTimers(options: { useFakeTimers?: boolean, useSetImmediate?: boolean, maxTaskCount?: number }, fn: () => Promise): Promise { const useFakeTimers = options.useFakeTimers === undefined ? true : options.useFakeTimers; if (!useFakeTimers) { return fn(); } const scheduler = new TimeTravelScheduler(); - const schedulerProcessor = new AsyncSchedulerProcessor(scheduler, { maxTaskCount: options.maxTaskCount }); + const schedulerProcessor = new AsyncSchedulerProcessor(scheduler, { useSetImmediate: options.useSetImmediate, maxTaskCount: options.maxTaskCount }); const globalInstallDisposable = scheduler.installGlobally(); let result: T; @@ -232,14 +238,14 @@ export async function runWithFakedTimers(options: { useFakeTimers?: boolean, result = await fn(); } finally { globalInstallDisposable.dispose(); - } - try { - // We process the remaining scheduled tasks. - // The global override is no longer active, so during this, no more tasks will be scheduled. - await schedulerProcessor.waitForEmptyQueue(); - } finally { - schedulerProcessor.dispose(); + try { + // We process the remaining scheduled tasks. + // The global override is no longer active, so during this, no more tasks will be scheduled. + await schedulerProcessor.waitForEmptyQueue(); + } finally { + schedulerProcessor.dispose(); + } } return result; diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index 63430e1c8f..f1aeb86b06 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -124,9 +124,12 @@ export function ensureNoDisposablesAreLeakedInTestSuite() { setDisposableTracker(tracker); }); - teardown(() => { + teardown(function (this: import('mocha').Context) { setDisposableTracker(null); - tracker!.ensureNoLeakingDisposables(); + + if (this.currentTest?.state !== 'failed') { + tracker!.ensureNoLeakingDisposables(); + } }); } diff --git a/src/vs/base/test/node/processes/processes.test.ts b/src/vs/base/test/node/processes/processes.integrationTest.ts similarity index 100% rename from src/vs/base/test/node/processes/processes.test.ts rename to src/vs/base/test/node/processes/processes.integrationTest.ts diff --git a/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts b/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts index 55308d1b7c..40ff0d758c 100644 --- a/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts +++ b/src/vs/base/test/parts/quickinput/browser/quickinput.test.ts @@ -7,6 +7,7 @@ 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 { IQuickPick, IQuickPickItem } from 'vs/base/parts/quickinput/common/quickInput'; import { IWorkbenchListOptions } from 'vs/platform/list/browser/listService'; // Simple promisify of setTimeout @@ -17,7 +18,11 @@ function wait(delayMS: number) { } suite('QuickInput', () => { - let fixture: HTMLElement, controller: QuickInputController; + let fixture: HTMLElement, controller: QuickInputController, quickpick: IQuickPick; + + function getScrollTop(): number { + return (quickpick as any).scrollTop; + } setup(() => { fixture = document.createElement('div'); @@ -48,15 +53,48 @@ suite('QuickInput', () => { widget: {} } }); + + // initial layout + controller.layout({ height: 20, width: 40 }, 0); }); teardown(() => { + quickpick?.dispose(); controller.dispose(); document.body.removeChild(fixture); }); - test('onDidChangeValue gets triggered when .value is set', async () => { - const quickpick = controller.createQuickPick(); + test('pick - basecase', async () => { + const item = { label: 'foo' }; + const pickPromise = controller.pick([item, { label: 'bar' }]); + // wait a bit to let the pick get set up. + await wait(200); + controller.accept(); + const pick = await pickPromise; + assert.strictEqual(pick, item); + }); + + test('pick - activeItem is honored', async () => { + const item = { label: 'foo' }; + const pickPromise = controller.pick([{ label: 'bar' }, item], { activeItem: item }); + // wait a bit to let the pick get set up. + await wait(200); + controller.accept(); + const pick = await pickPromise; + assert.strictEqual(pick, item); + }); + + test('input - basecase', async () => { + const inputPromise = controller.input({ value: 'foo' }); + // wait a bit to let the pick get set up. + await wait(200); + controller.accept(); + const value = await inputPromise; + assert.strictEqual(value, 'foo'); + }); + + test('onDidChangeValue - gets triggered when .value is set', async () => { + quickpick = controller.createQuickPick(); let value: string | undefined = undefined; quickpick.onDidChangeValue((e) => value = e); @@ -72,4 +110,77 @@ suite('QuickInput', () => { quickpick.dispose(); } }); + + test('keepScrollPosition - works with activeItems', async () => { + quickpick = controller.createQuickPick(); + + const items = []; + for (let i = 0; i < 1000; i++) { + items.push({ label: `item ${i}` }); + } + quickpick.items = items; + // setting the active item should cause the quick pick to scroll to the bottom + quickpick.activeItems = [items[items.length - 1]]; + quickpick.show(); + + let cursorTop = getScrollTop(); + + assert.notStrictEqual(cursorTop, 0); + + quickpick.keepScrollPosition = true; + quickpick.activeItems = [items[0]]; + assert.strictEqual(cursorTop, getScrollTop()); + + quickpick.keepScrollPosition = false; + quickpick.activeItems = [items[0]]; + assert.strictEqual(getScrollTop(), 0); + }); + + test('keepScrollPosition - works with items', async () => { + quickpick = controller.createQuickPick(); + + const items = []; + for (let i = 0; i < 1000; i++) { + items.push({ label: `item ${i}` }); + } + quickpick.items = items; + // setting the active item should cause the quick pick to scroll to the bottom + quickpick.activeItems = [items[items.length - 1]]; + quickpick.show(); + + let cursorTop = getScrollTop(); + assert.notStrictEqual(cursorTop, 0); + + quickpick.keepScrollPosition = true; + quickpick.items = items; + assert.strictEqual(cursorTop, getScrollTop()); + + quickpick.keepScrollPosition = false; + quickpick.items = items; + assert.strictEqual(getScrollTop(), 0); + }); + + test('selectedItems - verify previous selectedItems does not hang over to next set of items', async () => { + quickpick = controller.createQuickPick(); + quickpick.items = [{ label: 'step 1' }]; + quickpick.show(); + + void (await new Promise(resolve => { + quickpick.onDidAccept(() => { + console.log(quickpick.selectedItems.map(i => i.label).join(', ')); + quickpick.canSelectMany = true; + quickpick.items = [{ label: 'a' }, { label: 'b' }, { label: 'c' }]; + resolve(); + }); + + // accept 'step 1' + controller.accept(); + })); + + // accept in multi-select + controller.accept(); + + // Since we don't select any items, the selected items should be empty + assert.strictEqual(quickpick.selectedItems.length, 0); + }); }); diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index 8078b6842d..698523c95a 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -15,8 +15,8 @@ function getWorker(workerId: string, label: string): Worker | Promise { return globals.MonacoEnvironment.getWorker(workerId, label); } if (typeof globals.MonacoEnvironment.getWorkerUrl === 'function') { - const wokerUrl = globals.MonacoEnvironment.getWorkerUrl(workerId, label); - return new Worker(ttPolicy ? ttPolicy.createScriptURL(wokerUrl) as unknown as string : wokerUrl, { name: label }); + const workerUrl = globals.MonacoEnvironment.getWorkerUrl(workerId, label); + return new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label }); } } // ESM-comment-begin diff --git a/src/vs/base/worker/workerMain.ts b/src/vs/base/worker/workerMain.ts index 4d3783a0f5..7984d75123 100644 --- a/src/vs/base/worker/workerMain.ts +++ b/src/vs/base/worker/workerMain.ts @@ -24,6 +24,20 @@ : undefined ); + function canUseEval(): boolean { + try { + const func = ( + trustedTypesPolicy + ? self.eval(trustedTypesPolicy.createScript('', 'true')) + : new Function('true') + ); + func.call(self); + return true; + } catch (err) { + return false; + } + } + function loadAMDLoader() { return new Promise((resolve, reject) => { if (typeof (self).define === 'function' && (self).define.amd) { @@ -32,7 +46,7 @@ const loaderSrc: string | TrustedScriptURL = monacoBaseUrl + 'vs/loader.js'; const isCrossOrigin = (/^((http:)|(https:)|(file:))/.test(loaderSrc) && loaderSrc.substring(0, self.origin.length) !== self.origin); - if (!isCrossOrigin) { + if (!isCrossOrigin && canUseEval()) { // use `fetch` if possible because `importScripts` // is synchronous and can lead to deadlocks on Safari fetch(loaderSrc).then((response) => { @@ -68,6 +82,7 @@ baseUrl: monacoBaseUrl, catchError: true, trustedTypesPolicy, + amdModulesPattern: /^vs\// }); require([moduleId], function (ws) { setTimeout(function () { @@ -75,7 +90,7 @@ (self).postMessage(msg, transfer); }, null); - self.onmessage = (e: MessageEvent) => messageHandler.onmessage(e.data); + self.onmessage = (e: MessageEvent) => messageHandler.onmessage(e.data, e.ports); while (beforeReadyMessages.length > 0) { self.onmessage(beforeReadyMessages.shift()!); } diff --git a/src/vs/code/browser/workbench/callback.html b/src/vs/code/browser/workbench/callback.html index 9aec539bb5..939e591fdd 100644 --- a/src/vs/code/browser/workbench/callback.html +++ b/src/vs/code/browser/workbench/callback.html @@ -36,17 +36,18 @@ flex-direction: column; color: white; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", system-ui, "Ubuntu", "Droid Sans", sans-serif; - background-color: #373277; + background-color: #2C2C32; } .branding { - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGRlZnM+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtmaWxsOiNmNmY2ZjY7b3BhY2l0eTowO30uaWNvbi13aGl0ZXtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjx0aXRsZT5CcmFuZFZpc3VhbFN0dWRpb0NvZGUyMDE3UlRXXzI0eF93aGl0ZV8yNHg8L3RpdGxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTI0LDBWMjRIMFYwWiIvPjxwYXRoIGNsYXNzPSJpY29uLXdoaXRlIiBkPSJNMjQsMi41VjIxLjVMMTgsMjQsMCwxOC41di0uNTYxbDE4LDEuNTQ1VjBaTTEsMTMuMTExLDQuMzg1LDEwLDEsNi44ODlsMS40MTgtLjgyN0w1Ljg1Myw4LjY1LDEyLDNsMywxLjQ1NlYxNS41NDRMMTIsMTcsNS44NTMsMTEuMzUsMi40MTksMTMuOTM5Wk03LjY0NCwxMCwxMiwxMy4yODNWNi43MTdaIi8+PC9zdmc+"); + background-image: url("data:image/png;base64,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"); background-size: 24px; background-repeat: no-repeat; background-position: left 50%; padding-left: 36px; font-size: 20px; letter-spacing: -0.04rem; + font-family: "Segoe UI","Helvetica Neue","Helvetica",Arial,sans-serif; font-weight: 400; color: white; text-decoration: none; diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index b005090515..2c6baf7795 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -7,6 +7,12 @@ + + + + + + diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 4bbb59c008..6721cbdeab 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -3,10 +3,16 @@ + + + + + + diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 55f516bb67..d8e3ae8765 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -17,7 +17,7 @@ import { localize } from 'vs/nls'; import { parseLogLevel } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; -import { create, ICredentialsProvider, IHomeIndicator, IProductQualityChangeHandler, ISettingsSyncOptions, IURLCallbackProvider, IWindowIndicator, IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/workbench.web.api'; +import { create, ICredentialsProvider, IHomeIndicator, IProductQualityChangeHandler, ISettingsSyncOptions, IURLCallbackProvider, IWelcomeBanner, IWindowIndicator, IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/workbench.web.api'; function doCreateUri(path: string, queryValues: Map): URI { let query: string | undefined = undefined; @@ -185,6 +185,10 @@ class LocalStorageCredentialsProvider implements ICredentialsProvider { url: doCreateUri('/auth/logout', queryValues).toString(true) }, CancellationToken.None); } + + async clear(): Promise { + window.localStorage.removeItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY); + } } class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvider { @@ -390,14 +394,14 @@ class WindowIndicator implements IWindowIndicator { // Repo if (repositoryName && repositoryOwner) { - this.label = localize('playgroundLabelRepository', "$(remote) VS Code Web Playground: {0}/{1}", repositoryOwner, repositoryName); - this.tooltip = localize('playgroundRepositoryTooltip', "VS Code Web Playground: {0}/{1}", repositoryOwner, repositoryName); + this.label = localize('playgroundLabelRepository', "$(remote) Visual Studio Code Playground: {0}/{1}", repositoryOwner, repositoryName); + this.tooltip = localize('playgroundRepositoryTooltip', "Visual Studio Code Playground: {0}/{1}", repositoryOwner, repositoryName); } // No Repo else { - this.label = localize('playgroundLabel', "$(remote) VS Code Web Playground"); - this.tooltip = localize('playgroundTooltip', "VS Code Web Playground"); + this.label = localize('playgroundLabel', "$(remote) Visual Studio Code Playground"); + this.tooltip = localize('playgroundTooltip', "Visual Studio Code Playground"); } } } @@ -481,6 +485,15 @@ class WindowIndicator implements IWindowIndicator { title: localize('home', "Home") }; + // Welcome Banner + const welcomeBanner: IWelcomeBanner = { + message: localize('welcomeBannerMessage', "{0} Web. Browser based playground for testing.", product.nameShort), + actions: [{ + href: 'https://github.com/microsoft/vscode', + label: localize('learnMore', "Learn More") + }] + }; + // Window indicator (unless connected to a remote) let windowIndicator: WindowIndicator | undefined = undefined; if (!workspaceProvider.hasRemote()) { @@ -517,6 +530,7 @@ class WindowIndicator implements IWindowIndicator { settingsSyncOptions, homeIndicator, windowIndicator, + welcomeBanner, productQualityChangeHandler, workspaceProvider, urlCallbackProvider: new PollingURLCallbackProvider(), diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts index 9d9cdcd4c7..e9dea7314f 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts @@ -36,7 +36,7 @@ export class CodeCacheCleaner extends Disposable { } private async cleanUpCodeCaches(currentCodeCachePath: string): Promise { - this.logService.info('[code cache cleanup]: Starting to clean up old code cache folders.'); + this.logService.trace('[code cache cleanup]: Starting to clean up old code cache folders.'); try { const now = Date.now(); @@ -56,7 +56,7 @@ export class CodeCacheCleaner extends Disposable { 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}.`); + this.logService.trace(`[code cache cleanup]: Removing code cache folder ${codeCache}.`); return Promises.rm(codeCacheEntryPath); } diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts index 333ed498bc..f7a9bf641d 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -54,7 +54,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { } private async cleanUpLanguagePackCache(): Promise { - this.logService.info('[language pack cache cleanup]: Starting to clean up unused language packs.'); + this.logService.trace('[language pack cache cleanup]: Starting to clean up unused language packs.'); try { const installed: IStringDictionary = Object.create(null); @@ -74,11 +74,11 @@ export class LanguagePackCachedDataCleaner extends Disposable { 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.`); + this.logService.trace(`[language pack cache cleanup]: Skipping folder ${entry}. Language pack still in use.`); continue; } - this.logService.info(`[language pack cache cleanup]: Removing unused language pack: ${entry}`); + this.logService.trace(`[language pack cache cleanup]: Removing unused language pack: ${entry}`); await Promises.rm(join(cacheDir, entry)); } @@ -95,7 +95,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { 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)}`); + this.logService.trace(`[language pack cache cleanup]: Removing language pack cache folder: ${join(packEntry, entry)}`); await Promises.rm(candidate); } diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts index 8a6588ab59..f83e511cda 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts @@ -26,7 +26,7 @@ export class LogsDataCleaner extends Disposable { } private async cleanUpOldLogs(): Promise { - this.logService.info('[logs cleanup]: Starting to clean up old logs.'); + this.logService.trace('[logs cleanup]: Starting to clean up old logs.'); try { const currentLog = basename(this.environmentService.logsPath); @@ -39,7 +39,7 @@ export class LogsDataCleaner extends Disposable { const sessionsToDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9)); if (sessionsToDelete.length > 0) { - this.logService.info(`[logs cleanup]: Removing log folders '${sessionsToDelete.join(', ')}'`); + this.logService.trace(`[logs cleanup]: Removing log folders '${sessionsToDelete.join(', ')}'`); await Promise.all(sessionsToDelete.map(sessionToDelete => Promises.rm(join(logsRoot, sessionToDelete)))); } diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts index 586f65394a..c072027325 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts @@ -31,7 +31,7 @@ export class StorageDataCleaner extends Disposable { } private async cleanUpStorage(): Promise { - this.logService.info('[storage cleanup]: Starting to clean up storage folders.'); + this.logService.trace('[storage cleanup]: Starting to clean up storage folders.'); try { @@ -50,7 +50,7 @@ export class StorageDataCleaner extends Disposable { } if (emptyWorkspaces.indexOf(storageFolder) === -1) { - this.logService.info(`[storage cleanup]: Deleting storage folder ${storageFolder}.`); + this.logService.trace(`[storage cleanup]: Deleting storage folder ${storageFolder}.`); await Promises.rm(join(this.environmentService.workspaceStorageHome.fsPath, storageFolder)); } diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 977cd6b4e7..40c16dd380 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { ipcRenderer } from 'electron'; -import * as fs from 'fs'; -import { gracefulify } from 'graceful-fs'; import { hostname, release } from 'os'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; @@ -65,7 +63,7 @@ import { ICustomEndpointTelemetryService, ITelemetryService } from 'vs/platform/ import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { combinedAppender, ITelemetryAppender, NullAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { supportsTelemetry, ITelemetryAppender, NullAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { CustomEndpointTelemetryService } from 'vs/platform/telemetry/node/customEndpointTelemetryService'; import { LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; @@ -86,26 +84,51 @@ import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyn import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService'; import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; +import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter'; +import { ExtensionHostStarter } from 'vs/platform/extensions/node/extensionHostStarter'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { SignService } from 'vs/platform/sign/node/signService'; +import { ISharedTunnelsService } from 'vs/platform/remote/common/tunnel'; +import { SharedTunnelsService } from 'vs/platform/remote/node/tunnelService'; +import { ipcSharedProcessTunnelChannelName, ISharedProcessTunnelService } from 'vs/platform/remote/common/sharedProcessTunnelService'; +import { SharedProcessTunnelService } from 'vs/platform/remote/node/sharedProcessTunnelService'; +import { ipcSharedProcessWorkerChannelName, ISharedProcessWorkerConfiguration, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; +import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService'; +import { IUserConfigurationFileService, UserConfigurationFileServiceId } from 'vs/platform/configuration/common/userConfigurationFileService'; class SharedProcessMain extends Disposable { private server = this._register(new MessagePortServer()); + private sharedProcessWorkerService: ISharedProcessWorkerService | undefined = undefined; + constructor(private configuration: ISharedProcessConfiguration) { super(); - // Enable gracefulFs - gracefulify(fs); - this.registerListeners(); } private registerListeners(): void { - // Dispose on exit + // Shared process lifecycle const onExit = () => this.dispose(); process.once('exit', onExit); ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); + + // Shared process worker lifecycle + // + // We dispose the listener when the shared process is + // disposed to avoid disposing workers when the entire + // application is shutting down anyways. + // + const eventName = 'vscode:electron-main->shared-process=disposeWorker'; + const onDisposeWorker = (event: unknown, configuration: ISharedProcessWorkerConfiguration) => this.onDisposeWorker(configuration); + ipcRenderer.on(eventName, onDisposeWorker); + this._register(toDisposable(() => ipcRenderer.removeListener(eventName, onDisposeWorker))); + } + + private onDisposeWorker(configuration: ISharedProcessWorkerConfiguration): void { + this.sharedProcessWorkerService?.disposeWorker(configuration); } async open(): Promise { @@ -170,6 +193,10 @@ class SharedProcessMain extends Disposable { const logService = this._register(new FollowerLogService(logLevelClient, multiplexLogger)); services.set(ILogService, logService); + // Worker + this.sharedProcessWorkerService = new SharedProcessWorkerService(logService); + services.set(ISharedProcessWorkerService, this.sharedProcessWorkerService); + // Files const fileService = this._register(new FileService(logService)); services.set(IFileService, fileService); @@ -181,15 +208,20 @@ class SharedProcessMain extends Disposable { const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); services.set(IConfigurationService, configurationService); - await configurationService.initialize(); - // Storage (global access only) const storageService = new NativeStorageService(undefined, mainProcessService, environmentService); services.set(IStorageService, storageService); - - await storageService.initialize(); this._register(toDisposable(() => storageService.flush())); + // Initialize config & storage in parallel + await Promise.all([ + configurationService.initialize(), + storageService.initialize() + ]); + + // User Configuration File + services.set(IUserConfigurationFileService, ProxyChannel.toService(mainProcessService.getChannel(UserConfigurationFileServiceId))); + // Request services.set(IRequestService, new SyncDescriptor(RequestService)); @@ -210,31 +242,32 @@ class SharedProcessMain extends Disposable { // Telemetry let telemetryService: ITelemetryService; - let telemetryAppender: ITelemetryAppender; - if (!environmentService.isExtensionDevelopment && !environmentService.disableTelemetry && productService.enableTelemetry) { - telemetryAppender = new TelemetryLogAppender(loggerService, environmentService); - - const { appRoot, extensionsPath, isBuilt, installSourcePath } = environmentService; + const appenders: ITelemetryAppender[] = []; + if (supportsTelemetry(productService, environmentService)) { + const logAppender = new TelemetryLogAppender(loggerService, environmentService); + appenders.push(logAppender); + const { appRoot, extensionsPath, installSourcePath } = environmentService; // Application Insights - if (productService.aiConfig && productService.aiConfig.asimovKey && isBuilt) { + if (productService.aiConfig && productService.aiConfig.asimovKey) { const appInsightsAppender = new AppInsightsAppender('adsworkbench', null, productService.aiConfig.asimovKey); // {{SQL CARBON EDIT}} Use our own event prefix this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data - telemetryAppender = combinedAppender(appInsightsAppender, telemetryAppender); + appenders.push(appInsightsAppender); } telemetryService = new TelemetryService({ - appender: telemetryAppender, + appenders, commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, productService.msftInternalDomains, installSourcePath), sendErrorTelemetry: true, piiPaths: [appRoot, extensionsPath] }, configurationService); } else { telemetryService = NullTelemetryService; - telemetryAppender = NullAppender; + const nullAppender = NullAppender; + appenders.push(nullAppender); } - this.server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender)); + this.server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(appenders)); services.set(ITelemetryService, telemetryService); // Custom Endpoint Telemetry @@ -271,22 +304,30 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncResourceEnablementService, new SyncDescriptor(UserDataSyncResourceEnablementService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); - // Terminal - services.set( - ILocalPtyService, - this._register( - new PtyHostService({ - graceTime: LocalReconnectConstants.GraceTime, - shortGraceTime: LocalReconnectConstants.ShortGraceTime, - scrollback: configurationService.getValue(TerminalSettingId.PersistentSessionScrollback) ?? 100, - useExperimentalSerialization: configurationService.getValue(TerminalSettingId.PersistentSessionExperimentalSerializer) ?? true, - }, - configurationService, - logService, - telemetryService - ) - ) + const ptyHostService = new PtyHostService({ + graceTime: LocalReconnectConstants.GraceTime, + shortGraceTime: LocalReconnectConstants.ShortGraceTime, + scrollback: configurationService.getValue(TerminalSettingId.PersistentSessionScrollback) ?? 100 + }, + configurationService, + environmentService, + logService, + telemetryService ); + await ptyHostService.initialize(); + + // Terminal + services.set(ILocalPtyService, this._register(ptyHostService)); + + // Extension Host + services.set(IExtensionHostStarter, this._register(new ExtensionHostStarter(logService))); + + // Signing + services.set(ISignService, new SyncDescriptor(SignService)); + + // Tunnel + services.set(ISharedTunnelsService, new SyncDescriptor(SharedTunnelsService)); + services.set(ISharedProcessTunnelService, new SyncDescriptor(SharedProcessTunnelService)); return new InstantiationService(services); } @@ -338,6 +379,18 @@ class SharedProcessMain extends Disposable { const localPtyService = accessor.get(ILocalPtyService); const localPtyChannel = ProxyChannel.fromService(localPtyService); this.server.registerChannel(TerminalIpcChannels.LocalPty, localPtyChannel); + + // Extension Host + const extensionHostStarterChannel = ProxyChannel.fromService(accessor.get(IExtensionHostStarter)); + this.server.registerChannel(ipcExtensionHostStarterChannelName, extensionHostStarterChannel); + + // Tunnel + const sharedProcessTunnelChannel = ProxyChannel.fromService(accessor.get(ISharedProcessTunnelService)); + this.server.registerChannel(ipcSharedProcessTunnelChannelName, sharedProcessTunnelChannel); + + // Worker + const sharedProcessWorkerChannel = ProxyChannel.fromService(accessor.get(ISharedProcessWorkerService)); + this.server.registerChannel(ipcSharedProcessWorkerChannelName, sharedProcessWorkerChannel); } private registerErrorHandler(logService: ILogService): void { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 12b547a322..cec1418962 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -7,6 +7,7 @@ import { app, BrowserWindow, contentTracing, dialog, ipcMain, protocol, session, import { statSync } from 'fs'; import { hostname, release } from 'os'; import { VSBuffer } from 'vs/base/common/buffer'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { isEqualOrParent } from 'vs/base/common/extpath'; import { once } from 'vs/base/common/functional'; @@ -17,7 +18,7 @@ import { Schemas } from 'vs/base/common/network'; import { isAbsolute, join, posix } from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { assertType, withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { getMachineId } from 'vs/base/node/id'; @@ -46,6 +47,8 @@ import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/e import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; import { IFileService } from 'vs/platform/files/common/files'; +import { DiskFileSystemProviderChannel } from 'vs/platform/files/electron-main/diskFileSystemProviderIpc'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -65,10 +68,10 @@ import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { StorageDatabaseChannel } from 'vs/platform/storage/electron-main/storageIpc'; import { IStorageMainService, StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; -import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryService, machineIdKey, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { getTelemetryLevel, NullTelemetryService, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/common/updateIpc'; import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin'; @@ -279,7 +282,7 @@ export class CodeApplication extends Disposable { } // Resolve shell env - return resolveShellEnv(this.logService, args, env); + return this.resolveShellEnvironment(args, env); }); ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { @@ -531,12 +534,12 @@ export class CodeApplication extends Disposable { services.set(IURLService, new SyncDescriptor(NativeURLService)); // Telemetry - if (!this.environmentMainService.isExtensionDevelopment && !this.environmentMainService.args['disable-telemetry'] && !!this.productService.enableTelemetry) { + if (supportsTelemetry(this.productService, this.environmentMainService)) { const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); const commonProperties = resolveCommonProperties(this.fileService, release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, this.productService.msftInternalDomains, this.environmentMainService.installSourcePath); const piiPaths = [this.environmentMainService.appRoot, this.environmentMainService.extensionsPath]; - const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true }; + const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true }; services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [config])); } else { @@ -558,8 +561,16 @@ export class CodeApplication extends Disposable { const launchChannel = ProxyChannel.fromService(accessor.get(ILaunchMainService), { disableMarshalling: true }); this.mainProcessNodeIpcServer.registerChannel('launch', launchChannel); - // Configuration - mainProcessElectronServer.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(new UserConfigurationFileService(this.environmentMainService, this.fileService, this.logService))); + // Local Files + const diskFileSystemProvider = this.fileService.getProvider(Schemas.file); + assertType(diskFileSystemProvider instanceof DiskFileSystemProvider); + const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService); + mainProcessElectronServer.registerChannel('localFilesystem', fileSystemProviderChannel); + + // User Configuration File + const userConfigurationFileService = new UserConfigurationFileService(this.environmentMainService, this.fileService, this.logService); + mainProcessElectronServer.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(userConfigurationFileService)); + sharedProcessClient.then(client => client.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(userConfigurationFileService))); // Update const updateChannel = new UpdateChannel(accessor.get(IUpdateService)); @@ -846,9 +857,13 @@ export class CodeApplication extends Disposable { // File path if (uri.authority === Schemas.file) { - // we configure as fileUri, but later validation will - // make sure to open as folder or workspace if possible - return { fileUri: URI.file(uri.fsPath) }; + const fileUri = URI.file(uri.fsPath); + + if (hasWorkspaceFileExtension(fileUri)) { + return { workspaceUri: fileUri }; + } + + return { fileUri }; } // Remote path @@ -864,11 +879,14 @@ export class CodeApplication extends Disposable { if (hasWorkspaceFileExtension(path)) { return { workspaceUri: remoteUri }; - } else if (/:[\d]+$/.test(path)) { // path with :line:column syntax - return { fileUri: remoteUri }; - } else { - return { folderUri: remoteUri }; } + + if (/:[\d]+$/.test(path)) { + // path with :line:column syntax + return { fileUri: remoteUri }; + } + + return { folderUri: remoteUri }; } } @@ -971,7 +989,8 @@ export class CodeApplication extends Disposable { // Start to fetch shell environment (if needed) after window has opened // Since this operation can take a long time, we want to warm it up while // the window is opening. - resolveShellEnv(this.logService, this.environmentMainService.args, process.env); + // We also show an error to the user in case this fails. + this.resolveShellEnvironment(this.environmentMainService.args, process.env); // If enable-crash-reporter argv is undefined then this is a fresh start, // based on telemetry.enableCrashreporter settings, generate a UUID which @@ -981,8 +1000,8 @@ export class CodeApplication extends Disposable { const argvString = argvContent.value.toString(); const argvJSON = JSON.parse(stripComments(argvString)); if (argvJSON['enable-crash-reporter'] === undefined) { - const enableCrashReporterSetting = this.configurationService.getValue('telemetry.enableCrashReporter'); - const enableCrashReporter = typeof enableCrashReporterSetting === 'boolean' ? enableCrashReporterSetting : true; + const telemetryLevel = getTelemetryLevel(this.configurationService); + const enableCrashReporter = telemetryLevel >= TelemetryLevel.CRASH; const additionalArgvContent = [ '', ' // Allows to disable crash reporting.', @@ -1003,6 +1022,16 @@ export class CodeApplication extends Disposable { } } + private async resolveShellEnvironment(args: NativeParsedArgs, env: IProcessEnvironment): Promise { + try { + return await resolveShellEnv(this.logService, args, env); + } catch (error) { + this.windowsMainService?.sendToFocused('vscode:showResolveShellEnvError', toErrorMessage(error)); + } + + return {}; + } + private stopTracingEventually(accessor: ServicesAccessor, windows: ICodeWindow[]): void { this.logService.info(`Tracing: waiting for windows to get ready...`); @@ -1043,3 +1072,4 @@ export class CodeApplication extends Disposable { }); } } + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index ce123f5582..f76e077595 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/node/cli.ts b/src/vs/code/node/cli.ts index fca0f8a0a4..31a0f19b6c 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -5,20 +5,23 @@ import { ChildProcess, spawn, SpawnOptions } from 'child_process'; import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync } from 'fs'; -import { homedir } from 'os'; +import { homedir, release, tmpdir } from 'os'; import type { ProfilingSession, Target } from 'v8-inspect-profiler'; -import { isAbsolute, join } from 'vs/base/common/path'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { Event } from 'vs/base/common/event'; +import { isAbsolute, join, resolve } from 'vs/base/common/path'; +import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; import { randomPort } from 'vs/base/common/ports'; import { isString } from 'vs/base/common/types'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort } from 'vs/base/node/ports'; +import { watchFileContents } from 'vs/base/node/watcher'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv'; import { addArg, parseCLIProcessArgv } from 'vs/platform/environment/node/argvHelper'; import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener } from 'vs/platform/environment/node/stdin'; import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; import product from 'vs/platform/product/common/product'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { return !!argv['install-source'] @@ -29,6 +32,10 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { || !!argv['telemetry']; } +function createFileName(dir: string, prefix: string): string { + return join(dir, `${prefix}-${Math.random().toString(16).slice(-4)}`); +} + interface IMainCli { main: (argv: NativeParsedArgs) => Promise; } @@ -134,7 +141,7 @@ export async function main(argv: string[]): Promise { child.stdout!.on('data', (data: Buffer) => console.log(data.toString('utf8').trim())); child.stderr!.on('data', (data: Buffer) => console.log(data.toString('utf8').trim())); - await new Promise(resolve => child.once('exit', () => resolve())); + await Event.toPromise(Event.fromNodeEventEmitter(child, 'exit')); }); } @@ -188,6 +195,8 @@ export async function main(argv: string[]): Promise { } } + const isMacOSBigSurOrNewer = isMacintosh && release() > '20.0.0'; + // If we are started with --wait create a random temporary file // and pass it over to the starting instance. We can use this file // to wait for it to be deleted to monitor that the edited file @@ -198,6 +207,42 @@ export async function main(argv: string[]): Promise { if (waitMarkerFilePath) { addArg(argv, '--waitMarkerFilePath', waitMarkerFilePath); } + + // When running with --wait, we want to continue running CLI process + // until either: + // - the wait marker file has been deleted (e.g. when closing the editor) + // - the launched process terminates (e.g. due to a crash) + processCallbacks.push(async child => { + let childExitPromise; + if (isMacOSBigSurOrNewer) { + // On Big Sur, we resolve the following promise only when the child, + // i.e. the open command, exited with a signal or error. Otherwise, we + // wait for the marker file to be deleted or for the child to error. + childExitPromise = new Promise(resolve => { + // Only resolve this promise if the child (i.e. open) exited with an error + child.on('exit', (code, signal) => { + if (code !== 0 || signal) { + resolve(); + } + }); + }); + } else { + // On other platforms, we listen for exit in case the child exits before the + // marker file is deleted. + childExitPromise = Event.toPromise(Event.fromNodeEventEmitter(child, 'exit')); + } + try { + await Promise.race([ + whenDeleted(waitMarkerFilePath!), + Event.toPromise(Event.fromNodeEventEmitter(child, 'error')), + childExitPromise + ]); + } finally { + if (stdinFilePath) { + unlinkSync(stdinFilePath); // Make sure to delete the tmp stdin file if we have any + } + } + }); } // If we have been started with `--prof-startup` we need to find free ports to profile @@ -214,7 +259,7 @@ export async function main(argv: string[]): Promise { throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.'); } - const filenamePrefix = join(homedir(), 'prof-' + Math.random().toString(16).slice(-4)); + const filenamePrefix = createFileName(homedir(), 'prof'); addArg(argv, `--inspect-brk=${portMain}`); addArg(argv, `--remote-debugging-port=${portRenderer}`); @@ -319,23 +364,82 @@ export async function main(argv: string[]): Promise { options['stdio'] = 'ignore'; } - const child = spawn(process.execPath, argv.slice(2), options); + let child: ChildProcess; + if (!isMacOSBigSurOrNewer) { + // We spawn process.execPath directly + child = spawn(process.execPath, argv.slice(2), options); + } else { + // On Big Sur, we spawn using the open command to obtain behavior + // similar to if the app was launched from the dock + // https://github.com/microsoft/vscode/issues/102975 - if (args.wait && waitMarkerFilePath) { - return new Promise(resolve => { + // The following args are for the open command itself, rather than for VS Code: + // -n creates a new instance. + // Without -n, the open command re-opens the existing instance as-is. + // -g starts the new instance in the background. + // Later, Electron brings the instance to the foreground. + // This way, Mac does not automatically try to foreground the new instance, which causes + // focusing issues when the new instance only sends data to a previous instance and then closes. + const spawnArgs = ['-n', '-g']; + // -a opens the given application. + spawnArgs.push('-a', process.execPath); // -a: opens a specific application - // Complete when process exits - child.once('exit', () => resolve(undefined)); + if (verbose) { + spawnArgs.push('--wait-apps'); // `open --wait-apps`: blocks until the launched app is closed (even if they were already running) - // Complete when wait marker file is deleted - whenDeleted(waitMarkerFilePath!).then(resolve, resolve); - }).then(() => { + // The open command only allows for redirecting stderr and stdout to files, + // so we make it redirect those to temp files, and then use a logger to + // redirect the file output to the console + for (const outputType of ['stdout', 'stderr']) { - // Make sure to delete the tmp stdin file if we have any - if (stdinFilePath) { - unlinkSync(stdinFilePath); + // Tmp file to target output to + const tmpName = createFileName(tmpdir(), `code-${outputType}`); + writeFileSync(tmpName, ''); + spawnArgs.push(`--${outputType}`, tmpName); + + // Listener to redirect content to stdout/stderr + processCallbacks.push(async child => { + try { + const stream = outputType === 'stdout' ? process.stdout : process.stderr; + + const cts = new CancellationTokenSource(); + child.on('close', () => cts.dispose(true)); + await watchFileContents(tmpName, chunk => stream.write(chunk), cts.token); + } finally { + unlinkSync(tmpName); + } + }); } - }); + } + + for (const e in env) { + // Ignore the _ env var, because the open command + // ignores it anyway. + // Pass the rest of the env vars in to fix + // https://github.com/microsoft/vscode/issues/134696. + if (e !== '_') { + spawnArgs.push('--env'); + spawnArgs.push(`${e}=${env[e]}`); + } + } + + spawnArgs.push('--args', ...argv.slice(2)); // pass on our arguments + + if (env['VSCODE_DEV']) { + // If we're in development mode, replace the . arg with the + // vscode source arg. Because the OSS app isn't bundled, + // it needs the full vscode source arg to launch properly. + const curdir = '.'; + const launchDirIndex = spawnArgs.indexOf(curdir); + if (launchDirIndex !== -1) { + spawnArgs[launchDirIndex] = resolve(curdir); + } + } + + // We already passed over the env variables + // using the --env flags, so we can leave them out here. + // Also, we don't need to pass env._, which is different from argv._ + child = spawn('open', spawnArgs, { ...options, env: {} }); } return Promise.all(processCallbacks.map(callback => callback(child))); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index aa639142df..ce98fb30eb 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import { gracefulify } from 'graceful-fs'; import { hostname, release } from 'os'; import { raceTimeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -18,10 +16,12 @@ import { URI } from 'vs/base/common/uri'; import { Promises } from 'vs/base/node/pfs'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { DownloadService } from 'vs/platform/download/common/downloadService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -43,7 +43,7 @@ import { RequestService } from 'vs/platform/request/node/requestService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { supportsTelemetry, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry'; @@ -54,9 +54,6 @@ class CliMain extends Disposable { ) { super(); - // Enable gracefulFs - gracefulify(fs); - this.registerListeners(); } @@ -87,7 +84,10 @@ class CliMain extends Disposable { await this.doRun(environmentService, extensionManagementCLIService, fileService); // Flush the remaining data in AI adapter (with 1s timeout) - return raceTimeout(combinedAppender(...appenders).flush(), 1000); + await Promise.all(appenders.map(a => { + raceTimeout(a.flush(), 1000); + })); + return; }); } @@ -133,9 +133,12 @@ class CliMain extends Disposable { // Request services.set(IRequestService, new SyncDescriptor(RequestService)); + // Download Service + services.set(IDownloadService, new SyncDescriptor(DownloadService)); + // Extensions services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); - services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); + services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); // Localizations @@ -143,7 +146,7 @@ class CliMain extends Disposable { // Telemetry const appenders: AppInsightsAppender[] = []; - if (environmentService.isBuilt && !environmentService.isExtensionDevelopment && !environmentService.disableTelemetry && productService.enableTelemetry) { + if (supportsTelemetry(productService, environmentService)) { if (productService.aiConfig && productService.aiConfig.asimovKey) { appenders.push(new AppInsightsAppender('adsworkbench', null, productService.aiConfig.asimovKey)); // {{SQL CARBON EDIT}} Use our own event prefix } @@ -151,7 +154,7 @@ class CliMain extends Disposable { const { appRoot, extensionsPath, installSourcePath } = environmentService; const config: ITelemetryServiceConfig = { - appender: combinedAppender(...appenders), + appenders, sendErrorTelemetry: false, commonProperties: (async () => { let machineId: string | undefined = undefined; diff --git a/src/vs/css.js b/src/vs/css.js index d10367e63e..6c64a99dba 100644 --- a/src/vs/css.js +++ b/src/vs/css.js @@ -51,13 +51,7 @@ var CSSLoaderPlugin; BrowserCSSLoader.prototype._insertLinkNode = function (linkNode) { this._pendingLoads++; var head = document.head || document.getElementsByTagName('head')[0]; - var other = head.getElementsByTagName('link') || head.getElementsByTagName('script'); - if (other.length > 0) { - head.insertBefore(linkNode, other[other.length - 1]); - } - else { - head.appendChild(linkNode); - } + head.appendChild(linkNode); }; BrowserCSSLoader.prototype.createLinkTag = function (name, cssUrl, externalCallback, externalErrorback) { var _this = this; diff --git a/src/vs/editor/browser/config/charWidthReader.ts b/src/vs/editor/browser/config/charWidthReader.ts index 45f6d71f13..89f04e65e7 100644 --- a/src/vs/editor/browser/config/charWidthReader.ts +++ b/src/vs/editor/browser/config/charWidthReader.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isSafari } from 'vs/base/browser/browser'; +import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; export const enum CharWidthRequestType { @@ -62,13 +64,15 @@ class DomCharWidthReader { } private _createDomElements(): void { + const fontFamily = this._bareFontInfo.getMassagedFontFamily(isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null); + const container = document.createElement('div'); container.style.position = 'absolute'; container.style.top = '-50000px'; container.style.width = '50000px'; const regularDomNode = document.createElement('div'); - regularDomNode.style.fontFamily = this._bareFontInfo.getMassagedFontFamily(); + regularDomNode.style.fontFamily = fontFamily; regularDomNode.style.fontWeight = this._bareFontInfo.fontWeight; regularDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px'; regularDomNode.style.fontFeatureSettings = this._bareFontInfo.fontFeatureSettings; @@ -77,7 +81,7 @@ class DomCharWidthReader { container.appendChild(regularDomNode); const boldDomNode = document.createElement('div'); - boldDomNode.style.fontFamily = this._bareFontInfo.getMassagedFontFamily(); + boldDomNode.style.fontFamily = fontFamily; boldDomNode.style.fontWeight = 'bold'; boldDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px'; boldDomNode.style.fontFeatureSettings = this._bareFontInfo.fontFeatureSettings; @@ -86,7 +90,7 @@ class DomCharWidthReader { container.appendChild(boldDomNode); const italicDomNode = document.createElement('div'); - italicDomNode.style.fontFamily = this._bareFontInfo.getMassagedFontFamily(); + italicDomNode.style.fontFamily = fontFamily; italicDomNode.style.fontWeight = this._bareFontInfo.fontWeight; italicDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px'; italicDomNode.style.fontFeatureSettings = this._bareFontInfo.fontFeatureSettings; diff --git a/src/vs/editor/browser/config/configuration.ts b/src/vs/editor/browser/config/configuration.ts index 624302e980..b8d85322f3 100644 --- a/src/vs/editor/browser/config/configuration.ts +++ b/src/vs/editor/browser/config/configuration.ts @@ -11,7 +11,7 @@ import * as platform from 'vs/base/common/platform'; import { CharWidthRequest, CharWidthRequestType, readCharWidths } from 'vs/editor/browser/config/charWidthReader'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { CommonEditorConfiguration, IEnvConfiguration } from 'vs/editor/common/config/commonEditorConfig'; -import { EditorOption, EditorFontLigatures } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, EditorFontLigatures, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo, FontInfo, SERIALIZED_FONT_INFO_VERSION } from 'vs/editor/common/config/fontInfo'; import { IDimension } from 'vs/editor/common/editorCommon'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; @@ -238,29 +238,13 @@ class CSSBasedConfiguration extends Disposable { const wsmiddotWidth = this.createRequest(String.fromCharCode(0x2E31), CharWidthRequestType.Regular, all, null); // monospace test: some characters - this.createRequest('|', CharWidthRequestType.Regular, all, monospace); - this.createRequest('/', CharWidthRequestType.Regular, all, monospace); - this.createRequest('-', CharWidthRequestType.Regular, all, monospace); - this.createRequest('_', CharWidthRequestType.Regular, all, monospace); - this.createRequest('i', CharWidthRequestType.Regular, all, monospace); - this.createRequest('l', CharWidthRequestType.Regular, all, monospace); - this.createRequest('m', CharWidthRequestType.Regular, all, monospace); + const monospaceTestChars = '|/-_ilm%'; + for (let i = 0, len = monospaceTestChars.length; i < len; i++) { + this.createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Regular, all, monospace); + this.createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Italic, all, monospace); + this.createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Bold, all, monospace); - // monospace italic test - this.createRequest('|', CharWidthRequestType.Italic, all, monospace); - this.createRequest('_', CharWidthRequestType.Italic, all, monospace); - this.createRequest('i', CharWidthRequestType.Italic, all, monospace); - this.createRequest('l', CharWidthRequestType.Italic, all, monospace); - this.createRequest('m', CharWidthRequestType.Italic, all, monospace); - this.createRequest('n', CharWidthRequestType.Italic, all, monospace); - - // monospace bold test - this.createRequest('|', CharWidthRequestType.Bold, all, monospace); - this.createRequest('_', CharWidthRequestType.Bold, all, monospace); - this.createRequest('i', CharWidthRequestType.Bold, all, monospace); - this.createRequest('l', CharWidthRequestType.Bold, all, monospace); - this.createRequest('m', CharWidthRequestType.Bold, all, monospace); - this.createRequest('n', CharWidthRequestType.Bold, all, monospace); + } readCharWidths(bareFontInfo, all); @@ -312,7 +296,7 @@ class CSSBasedConfiguration extends Disposable { export class Configuration extends CommonEditorConfiguration { public static applyFontInfoSlow(domNode: HTMLElement, fontInfo: BareFontInfo): void { - domNode.style.fontFamily = fontInfo.getMassagedFontFamily(); + domNode.style.fontFamily = fontInfo.getMassagedFontFamily(browser.isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null); domNode.style.fontWeight = fontInfo.fontWeight; domNode.style.fontSize = fontInfo.fontSize + 'px'; domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings; @@ -321,7 +305,7 @@ export class Configuration extends CommonEditorConfiguration { } public static applyFontInfo(domNode: FastDomNode, fontInfo: BareFontInfo): void { - domNode.setFontFamily(fontInfo.getMassagedFontFamily()); + domNode.setFontFamily(fontInfo.getMassagedFontFamily(browser.isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null)); domNode.setFontWeight(fontInfo.fontWeight); domNode.setFontSize(fontInfo.fontSize); domNode.setFontFeatureSettings(fontInfo.fontFeatureSettings); diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index c62248f05b..ea502581e1 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -625,7 +625,7 @@ export namespace CoreNavigationCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.LeftArrow, - mac: { primary: KeyCode.LeftArrow, secondary: [KeyMod.WinCtrl | KeyCode.KEY_B] } + mac: { primary: KeyCode.LeftArrow, secondary: [KeyMod.WinCtrl | KeyCode.KeyB] } } })); @@ -658,7 +658,7 @@ export namespace CoreNavigationCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.RightArrow, - mac: { primary: KeyCode.RightArrow, secondary: [KeyMod.WinCtrl | KeyCode.KEY_F] } + mac: { primary: KeyCode.RightArrow, secondary: [KeyMod.WinCtrl | KeyCode.KeyF] } } })); @@ -691,7 +691,7 @@ export namespace CoreNavigationCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.UpArrow, - mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.WinCtrl | KeyCode.KEY_P] } + mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.WinCtrl | KeyCode.KeyP] } } })); @@ -759,7 +759,7 @@ export namespace CoreNavigationCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.DownArrow, - mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.WinCtrl | KeyCode.KEY_N] } + mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.WinCtrl | KeyCode.KeyN] } } })); @@ -979,7 +979,7 @@ export namespace CoreNavigationCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_A } + mac: { primary: KeyMod.WinCtrl | KeyCode.KeyA } } })); @@ -991,7 +991,7 @@ export namespace CoreNavigationCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_A } + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyA } } })); @@ -1112,7 +1112,7 @@ export namespace CoreNavigationCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_E } + mac: { primary: KeyMod.WinCtrl | KeyCode.KeyE } } })); @@ -1124,7 +1124,7 @@ export namespace CoreNavigationCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_E } + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyE } } })); @@ -1526,7 +1526,7 @@ export namespace CoreNavigationCommands { kbOpts: { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_L + primary: KeyMod.CtrlCmd | KeyCode.KeyL } }); } @@ -1749,7 +1749,7 @@ export namespace CoreEditingCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_O } + mac: { primary: KeyMod.WinCtrl | KeyCode.KeyO } } }); } @@ -1816,7 +1816,7 @@ export namespace CoreEditingCommands { kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.Backspace, secondary: [KeyMod.Shift | KeyCode.Backspace], - mac: { primary: KeyCode.Backspace, secondary: [KeyMod.Shift | KeyCode.Backspace, KeyMod.WinCtrl | KeyCode.KEY_H, KeyMod.WinCtrl | KeyCode.Backspace] } + mac: { primary: KeyCode.Backspace, secondary: [KeyMod.Shift | KeyCode.Backspace, KeyMod.WinCtrl | KeyCode.KeyH, KeyMod.WinCtrl | KeyCode.Backspace] } } }); } @@ -1840,7 +1840,7 @@ export namespace CoreEditingCommands { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.Delete, - mac: { primary: KeyCode.Delete, secondary: [KeyMod.WinCtrl | KeyCode.KEY_D, KeyMod.WinCtrl | KeyCode.Delete] } + mac: { primary: KeyCode.Delete, secondary: [KeyMod.WinCtrl | KeyCode.KeyD, KeyMod.WinCtrl | KeyCode.Delete] } } }); } diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 0356bbd0ef..8d2d905b29 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -176,7 +176,16 @@ export class MouseHandler extends ViewEventHandler { } protected _createMouseTarget(e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget { - return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, testEventTarget ? e.target : null); + let target = e.target; + if (!this.viewHelper.viewDomNode.contains(target)) { + const shadowRoot = dom.getShadowRoot(this.viewHelper.viewDomNode); + if (shadowRoot) { + target = (shadowRoot).elementsFromPoint(e.posx, e.posy).find( + (el: Element) => this.viewHelper.viewDomNode.contains(el) + ); + } + } + return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, testEventTarget ? target : null); } private _getMouseColumn(e: EditorMouseEvent): number { diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 5406deb152..c66d5213d4 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -578,6 +578,9 @@ export class TextAreaHandler extends ViewPart { top, left, canUseZeroSizeTextarea ? 0 : 1, this._lineHeight ); + // In case the textarea contains a word, we're going to try to align the textarea's cursor + // with our cursor by scrolling the textarea as much as possible + this.textArea.domNode.scrollLeft = 1000000; return; } diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 4d0bddb54a..347b47b2c8 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -644,9 +644,6 @@ class ClipboardEventUtils { if (e.clipboardData) { return true; } - if ((window).clipboardData) { - return true; - } return false; } @@ -671,12 +668,6 @@ class ClipboardEventUtils { return [text, metadata]; } - if ((window).clipboardData) { - e.preventDefault(); - const text: string = (window).clipboardData.getData('Text'); - return [text, null]; - } - throw new Error('ClipboardEventUtils.getTextData: Cannot use text data!'); } @@ -691,12 +682,6 @@ class ClipboardEventUtils { return; } - if ((window).clipboardData) { - (window).clipboardData.setData('Text', text); - e.preventDefault(); - return; - } - throw new Error('ClipboardEventUtils.setTextData: Cannot use text data!'); } } diff --git a/src/vs/editor/browser/core/markdownRenderer.ts b/src/vs/editor/browser/core/markdownRenderer.ts index 9530f5ec24..e85fc489ca 100644 --- a/src/vs/editor/browser/core/markdownRenderer.ts +++ b/src/vs/editor/browser/core/markdownRenderer.ts @@ -12,9 +12,10 @@ import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Emitter } from 'vs/base/common/event'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/modes'; +import { ILanguageIdCodec, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/modes'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { URI } from 'vs/base/common/uri'; +import { Configuration } from 'vs/editor/browser/config/configuration'; export interface IMarkdownRenderResult extends IDisposable { element: HTMLElement; @@ -33,8 +34,8 @@ export interface IMarkdownRendererOptions { export class MarkdownRenderer { private static _ttpTokenizer = window.trustedTypes?.createPolicy('tokenizeToString', { - createHTML(value: string, tokenizer: ITokenizationSupport | undefined) { - return tokenizeToString(value, tokenizer); + createHTML(value: string, languageIdCodec: ILanguageIdCodec, tokenizer: ITokenizationSupport | undefined) { + return tokenizeToString(value, languageIdCodec, tokenizer); } }); @@ -52,17 +53,15 @@ export class MarkdownRenderer { } render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): IMarkdownRenderResult { - const disposables = new DisposableStore(); - - let element: HTMLElement; if (!markdown) { - element = document.createElement('span'); - } else { - element = renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposables), ...options }, markedOptions); + const element = document.createElement('span'); + return { element, dispose: () => { } }; } + const disposables = new DisposableStore(); + const rendered = disposables.add(renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposables), ...options }, markedOptions)); return { - element, + element: rendered.element, dispose: () => disposables.dispose() }; } @@ -74,29 +73,28 @@ export class MarkdownRenderer { // In markdown, // it is possible that we stumble upon language aliases (e.g.js instead of javascript) // it is possible no alias is given in which case we fall back to the current editor lang - let modeId: string | undefined | null; + let languageId: string | undefined | null; if (languageAlias) { - modeId = this._modeService.getModeIdForLanguageName(languageAlias); + languageId = this._modeService.getModeIdForLanguageName(languageAlias); } else if (this._options.editor) { - modeId = this._options.editor.getModel()?.getLanguageIdentifier().language; + languageId = this._options.editor.getModel()?.getLanguageId(); } - if (!modeId) { - modeId = 'plaintext'; + if (!languageId) { + languageId = 'plaintext'; } - this._modeService.triggerMode(modeId); - const tokenization = await TokenizationRegistry.getPromise(modeId) ?? undefined; + this._modeService.triggerMode(languageId); + const tokenization = await TokenizationRegistry.getPromise(languageId) ?? undefined; const element = document.createElement('span'); - element.innerHTML = (MarkdownRenderer._ttpTokenizer?.createHTML(value, tokenization) ?? tokenizeToString(value, tokenization)) as string; + element.innerHTML = (MarkdownRenderer._ttpTokenizer?.createHTML(value, this._modeService.languageIdCodec, tokenization) ?? tokenizeToString(value, this._modeService.languageIdCodec, tokenization)) as string; // use "good" font - let fontFamily = this._options.codeBlockFontFamily; if (this._options.editor) { - fontFamily = this._options.editor.getOption(EditorOption.fontInfo).fontFamily; - } - if (fontFamily) { - element.style.fontFamily = fontFamily; + const fontInfo = this._options.editor.getOption(EditorOption.fontInfo); + Configuration.applyFontInfoSlow(element, fontInfo); + } else if (this._options.codeBlockFontFamily) { + element.style.fontFamily = this._options.codeBlockFontFamily; } return element; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index eccc5beca8..67c2a31fb4 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from 'vs/base/common/event'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -325,7 +326,7 @@ export interface IPartialEditorMouseEvent { */ export interface IPasteEvent { readonly range: Range; - readonly mode: string | null; + readonly languageId: string | null; } /** @@ -371,6 +372,22 @@ export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { * Defaults to an internal DOM node. */ overflowWidgetsDomNode?: HTMLElement; + + /** + * Aria label for original editor. + */ + originalAriaLabel?: string; + + /** + * Aria label for modified editor. + */ + modifiedAriaLabel?: string; + + /** + * Is the diff editor inside another editor + * Defaults to false + */ + isInEmbeddedEditor?: boolean; } /** @@ -386,47 +403,47 @@ export interface ICodeEditor extends editorCommon.IEditor { * An event emitted when the content of the current model has changed. * @event */ - onDidChangeModelContent(listener: (e: IModelContentChangedEvent) => void): IDisposable; + onDidChangeModelContent: Event; /** * An event emitted when the language of the current model has changed. * @event */ - onDidChangeModelLanguage(listener: (e: IModelLanguageChangedEvent) => void): IDisposable; + onDidChangeModelLanguage: Event; /** * An event emitted when the language configuration of the current model has changed. * @event */ - onDidChangeModelLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) => void): IDisposable; + onDidChangeModelLanguageConfiguration: Event; /** * An event emitted when the options of the current model has changed. * @event */ - onDidChangeModelOptions(listener: (e: IModelOptionsChangedEvent) => void): IDisposable; + onDidChangeModelOptions: Event; /** * An event emitted when the configuration of the editor has changed. (e.g. `editor.updateOptions()`) * @event */ - onDidChangeConfiguration(listener: (e: ConfigurationChangedEvent) => void): IDisposable; + onDidChangeConfiguration: Event; /** * An event emitted when the cursor position has changed. * @event */ - onDidChangeCursorPosition(listener: (e: ICursorPositionChangedEvent) => void): IDisposable; + onDidChangeCursorPosition: Event; /** * An event emitted when the cursor selection has changed. * @event */ - onDidChangeCursorSelection(listener: (e: ICursorSelectionChangedEvent) => void): IDisposable; + onDidChangeCursorSelection: Event; /** * An event emitted when the model of this editor has changed (e.g. `editor.setModel()`). * @event */ - onDidChangeModel(listener: (e: editorCommon.IModelChangedEvent) => void): IDisposable; + onDidChangeModel: Event; /** * An event emitted when the decorations of the current model have changed. * @event */ - onDidChangeModelDecorations(listener: (e: IModelDecorationsChangedEvent) => void): IDisposable; + onDidChangeModelDecorations: Event; /** * An event emitted when the text inside this editor gained focus (i.e. cursor starts blinking). * @event @@ -476,29 +493,29 @@ export interface ICodeEditor extends editorCommon.IEditor { * An event emitted when users paste text in the editor. * @event */ - onDidPaste(listener: (e: IPasteEvent) => void): IDisposable; + onDidPaste: Event; /** * An event emitted on a "mouseup". * @event */ - onMouseUp(listener: (e: IEditorMouseEvent) => void): IDisposable; + onMouseUp: Event; /** * An event emitted on a "mousedown". * @event */ - onMouseDown(listener: (e: IEditorMouseEvent) => void): IDisposable; + onMouseDown: Event; /** * An event emitted on a "mousedrag". * @internal * @event */ - onMouseDrag(listener: (e: IEditorMouseEvent) => void): IDisposable; + onMouseDrag: Event; /** * An event emitted on a "mousedrop". * @internal * @event */ - onMouseDrop(listener: (e: IPartialEditorMouseEvent) => void): IDisposable; + onMouseDrop: Event; /** * An event emitted on a "mousedropcanceled". * @internal @@ -509,48 +526,54 @@ export interface ICodeEditor extends editorCommon.IEditor { * An event emitted on a "contextmenu". * @event */ - onContextMenu(listener: (e: IEditorMouseEvent) => void): IDisposable; + onContextMenu: Event; /** * An event emitted on a "mousemove". * @event */ - onMouseMove(listener: (e: IEditorMouseEvent) => void): IDisposable; + onMouseMove: Event; /** * An event emitted on a "mouseleave". * @event */ - onMouseLeave(listener: (e: IPartialEditorMouseEvent) => void): IDisposable; + onMouseLeave: Event; /** * An event emitted on a "mousewheel" * @event * @internal */ - onMouseWheel(listener: (e: IMouseWheelEvent) => void): IDisposable; + onMouseWheel: Event; /** * An event emitted on a "keyup". * @event */ - onKeyUp(listener: (e: IKeyboardEvent) => void): IDisposable; + onKeyUp: Event; /** * An event emitted on a "keydown". * @event */ - onKeyDown(listener: (e: IKeyboardEvent) => void): IDisposable; + onKeyDown: Event; /** * An event emitted when the layout of the editor has changed. * @event */ - onDidLayoutChange(listener: (e: EditorLayoutInfo) => void): IDisposable; + onDidLayoutChange: Event; /** * An event emitted when the content width or content height in the editor has changed. * @event */ - onDidContentSizeChange(listener: (e: editorCommon.IContentSizeChangedEvent) => void): IDisposable; + onDidContentSizeChange: Event; /** * An event emitted when the scroll in the editor has changed. * @event */ - onDidScrollChange(listener: (e: editorCommon.IScrollEvent) => void): IDisposable; + onDidScrollChange: Event; + + /** + * An event emitted when hidden areas change in the editor (e.g. due to folding). + * @event + */ + onDidChangeHiddenAreas: Event; /** * Saves current view state of the editor in a serializable object. @@ -958,16 +981,6 @@ export interface IDiffEditor extends editorCommon.IEditor { * @internal */ readonly ignoreTrimWhitespace: boolean; - /** - * Returns whether the diff editor is rendering side by side or not. - * @internal - */ - readonly renderSideBySide: boolean; - /** - * Returns whether the diff editor is rendering +/- indicators or not. - * @internal - */ - readonly renderIndicators: boolean; /** * Timeout in milliseconds after which diff computation is cancelled. * @internal diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index 432b9df4ba..309ef86654 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -181,6 +181,7 @@ export class MultiCommand extends Command { public runCommand(accessor: ServicesAccessor, args: any): void | Promise { const logService = accessor.get(ILogService); + logService.trace(`Executing Command '${this.id}' which has ${this._implementations.length} bound.`); for (const impl of this._implementations) { const result = impl.implementation(accessor, args); if (result) { @@ -191,6 +192,7 @@ export class MultiCommand extends Command { return result; } } + logService.trace(`The Command '${this.id}' was not handled by any implementation.`); } } @@ -591,7 +593,7 @@ export const UndoCommand = registerCommand(new MultiCommand({ precondition: undefined, kbOpts: { weight: KeybindingWeight.EditorCore, - primary: KeyMod.CtrlCmd | KeyCode.KEY_Z + primary: KeyMod.CtrlCmd | KeyCode.KeyZ }, menuOpts: [{ menuId: MenuId.MenubarEditMenu, @@ -613,9 +615,9 @@ export const RedoCommand = registerCommand(new MultiCommand({ precondition: undefined, kbOpts: { weight: KeybindingWeight.EditorCore, - primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, - secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z } + primary: KeyMod.CtrlCmd | KeyCode.KeyY, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ], + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ } }, menuOpts: [{ menuId: MenuId.MenubarEditMenu, @@ -638,7 +640,7 @@ export const SelectAllCommand = registerCommand(new MultiCommand({ kbOpts: { weight: KeybindingWeight.EditorCore, kbExpr: null, - primary: KeyMod.CtrlCmd | KeyCode.KEY_A + primary: KeyMod.CtrlCmd | KeyCode.KeyA }, menuOpts: [{ menuId: MenuId.MenubarEditMenu, // {{SQL CARBON EDIT}} - Put this in the edit menu since we disabled the selection menu diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index a324c97e96..836f7da3db 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -339,7 +339,8 @@ export class DecorationTypeOptionsProvider implements IModelDecorationOptionsPro isWholeLine: this.isWholeLine, overviewRuler: this.overviewRuler, stickiness: this.stickiness, - before: this.beforeInjectedText + before: this.beforeInjectedText, + after: this.afterInjectedText }; } diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index eea5cbafe7..da7c56478b 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -69,12 +69,9 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe } const overallWidth = Math.round(firstLineBreakColumn * fontInfo.typicalHalfwidthCharacterWidth); - - // Cannot respect WrappingIndent.Indent and WrappingIndent.DeepIndent because that would require - // two dom layouts, in order to first set the width of the first line, and then set the width of the wrapped lines - if (wrappingIndent === WrappingIndent.Indent || wrappingIndent === WrappingIndent.DeepIndent) { - wrappingIndent = WrappingIndent.Same; - } + const additionalIndent = (wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0); + const additionalIndentSize = Math.round(tabSize * additionalIndent); + const additionalIndentLength = Math.ceil(fontInfo.spaceWidth * additionalIndentSize); const containerDomNode = document.createElement('div'); Configuration.applyFontInfoSlow(containerDomNode, fontInfo); @@ -123,7 +120,7 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe } const renderLineContent = lineContent.substr(firstNonWhitespaceIndex); - const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb); + const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength); firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex; wrappedTextIndentLengths[i] = wrappedTextIndentLength; renderLineContents[i] = renderLineContent; @@ -152,7 +149,7 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe } const firstNonWhitespaceIndex = firstNonWhitespaceIndices[i]; - const wrappedTextIndentLength = wrappedTextIndentLengths[i]; + const wrappedTextIndentLength = wrappedTextIndentLengths[i] + additionalIndentSize; const visibleColumns = allVisibleColumns[i]; const breakOffsetsVisibleColumn: number[] = []; @@ -189,8 +186,18 @@ const enum Constants { SPAN_MODULO_LIMIT = 16384 } -function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: IStringBuilder): [number[], number[]] { - sb.appendASCIIString('
'); // if (containsRTL) { diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css index 87699c609e..70bacbbee9 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.css @@ -3,13 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* - Keeping name short for faster parsing. - cigr = core ident guides rendering (div) -*/ -.monaco-editor .lines-content .cigr { - position: absolute; -} -.monaco-editor .lines-content .cigra { +.monaco-editor .lines-content .core-guide { position: absolute; + box-sizing: border-box; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index bfc674a39a..073a5a96b1 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -5,30 +5,33 @@ import 'vs/css!./indentGuides'; import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; -import { Position } from 'vs/editor/common/core/position'; -import { editorActiveIndentGuides, editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; +import { editorActiveIndentGuides, editorBracketHighlightingForeground1, editorBracketHighlightingForeground2, editorBracketHighlightingForeground3, editorBracketHighlightingForeground4, editorBracketHighlightingForeground5, editorBracketHighlightingForeground6, editorBracketPairGuideActiveBackground1, editorBracketPairGuideActiveBackground2, editorBracketPairGuideActiveBackground3, editorBracketPairGuideActiveBackground4, editorBracketPairGuideActiveBackground5, editorBracketPairGuideActiveBackground6, editorBracketPairGuideBackground1, editorBracketPairGuideBackground2, editorBracketPairGuideBackground3, editorBracketPairGuideBackground4, editorBracketPairGuideBackground5, editorBracketPairGuideBackground6, editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry'; import { RenderingContext } from 'vs/editor/common/view/renderingContext'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; - +import { EditorOption, InternalGuidesOptions } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { HorizontalGuidesState, IndentGuide } from 'vs/editor/common/model'; +import { ArrayQueue } from 'vs/base/common/arrays'; +import { BracketPairGuidesClassNames } from 'vs/editor/common/model/textModel'; +import { Color } from 'vs/base/common/color'; +import { isDefined } from 'vs/base/common/types'; export class IndentGuidesOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; - private _primaryLineNumber: number; + private _primaryPosition: Position | null; private _lineHeight: number; private _spaceWidth: number; private _renderResult: string[] | null; - private _enabled: boolean; - private _activeIndentEnabled: boolean; private _maxIndentLeft: number; + private _bracketPairGuideOptions: InternalGuidesOptions; constructor(context: ViewContext) { super(); this._context = context; - this._primaryLineNumber = 0; + this._primaryPosition = null; const options = this._context.configuration.options; const wrappingInfo = options.get(EditorOption.wrappingInfo); @@ -36,9 +39,8 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; - this._enabled = options.get(EditorOption.renderIndentGuides); - this._activeIndentEnabled = options.get(EditorOption.highlightActiveIndentGuide); this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); + this._bracketPairGuideOptions = options.get(EditorOption.guides); this._renderResult = null; @@ -60,17 +62,16 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { this._lineHeight = options.get(EditorOption.lineHeight); this._spaceWidth = fontInfo.spaceWidth; - this._enabled = options.get(EditorOption.renderIndentGuides); - this._activeIndentEnabled = options.get(EditorOption.highlightActiveIndentGuide); this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth); + this._bracketPairGuideOptions = options.get(EditorOption.guides); + return true; } public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { const selection = e.selections[0]; - const newPrimaryLineNumber = selection.isEmpty() ? selection.positionLineNumber : 0; - - if (this._primaryLineNumber !== newPrimaryLineNumber) { - this._primaryLineNumber = newPrimaryLineNumber; + const newPosition = selection.getPosition(); + if (!this._primaryPosition?.equals(newPosition)) { + this._primaryPosition = newPosition; return true; } @@ -105,53 +106,128 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { // --- end event handlers public prepareRender(ctx: RenderingContext): void { - if (!this._enabled) { + if (!this._bracketPairGuideOptions.indentation && this._bracketPairGuideOptions.bracketPairs === false) { this._renderResult = null; return; } const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; - const { indentSize } = this._context.model.getTextModelOptions(); - const indentWidth = indentSize * this._spaceWidth; const scrollWidth = ctx.scrollWidth; const lineHeight = this._lineHeight; - const indents = this._context.model.getLinesIndentGuides(visibleStartLineNumber, visibleEndLineNumber); + const activeCursorPosition = this._primaryPosition; + + const indents = this.getGuidesByLine( + visibleStartLineNumber, + visibleEndLineNumber, + activeCursorPosition + ); + + const output: string[] = []; + for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { + const lineIndex = lineNumber - visibleStartLineNumber; + const indent = indents[lineIndex]; + let result = ''; + const leftOffset = ctx.visibleRangeForPosition(new Position(lineNumber, 1))?.left ?? 0; + for (const guide of indent) { + const left = leftOffset + (guide.visibleColumn - 1) * this._spaceWidth; + if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) { + break; + } + + const className = guide.horizontalLine ? (guide.horizontalLine.top ? 'horizontal-top' : 'horizontal-bottom') : 'vertical'; + + const width = guide.horizontalLine + ? (ctx.visibleRangeForPosition( + new Position(lineNumber, guide.horizontalLine.endColumn) + )?.left ?? (left + this._spaceWidth)) - left + : this._spaceWidth; + + result += `
`; + } + output[lineIndex] = result; + } + this._renderResult = output; + } + + private getGuidesByLine( + visibleStartLineNumber: number, + visibleEndLineNumber: number, + activeCursorPosition: Position | null + ): IndentGuide[][] { + const bracketGuides = this._bracketPairGuideOptions.bracketPairs !== false + ? this._context.model.getBracketGuidesInRangeByLine( + visibleStartLineNumber, + visibleEndLineNumber, + activeCursorPosition, + { + highlightActive: this._bracketPairGuideOptions.highlightActiveBracketPair, + horizontalGuides: this._bracketPairGuideOptions.bracketPairsHorizontal === true + ? HorizontalGuidesState.Enabled + : this._bracketPairGuideOptions.bracketPairsHorizontal === 'active' + ? HorizontalGuidesState.EnabledForActive + : HorizontalGuidesState.Disabled, + includeInactive: this._bracketPairGuideOptions.bracketPairs === true, + } + ) + : null; + + const indentGuides = this._bracketPairGuideOptions.indentation + ? this._context.model.getLinesIndentGuides( + visibleStartLineNumber, + visibleEndLineNumber + ) + : null; let activeIndentStartLineNumber = 0; let activeIndentEndLineNumber = 0; let activeIndentLevel = 0; - if (this._activeIndentEnabled && this._primaryLineNumber) { - const activeIndentInfo = this._context.model.getActiveIndentGuide(this._primaryLineNumber, visibleStartLineNumber, visibleEndLineNumber); + + if (this._bracketPairGuideOptions.highlightActiveIndentation && activeCursorPosition) { + const activeIndentInfo = this._context.model.getActiveIndentGuide(activeCursorPosition.lineNumber, visibleStartLineNumber, visibleEndLineNumber); activeIndentStartLineNumber = activeIndentInfo.startLineNumber; activeIndentEndLineNumber = activeIndentInfo.endLineNumber; activeIndentLevel = activeIndentInfo.indent; } - const output: string[] = []; - for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { - const containsActiveIndentGuide = (activeIndentStartLineNumber <= lineNumber && lineNumber <= activeIndentEndLineNumber); - const lineIndex = lineNumber - visibleStartLineNumber; - const indent = indents[lineIndex]; + const { indentSize } = this._context.model.getTextModelOptions(); - let result = ''; - if (indent >= 1) { - const leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1)); - let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0; - for (let i = 1; i <= indent; i++) { - const className = (containsActiveIndentGuide && i === activeIndentLevel ? 'cigra' : 'cigr'); - result += `
`; - left += indentWidth; - if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) { - break; - } + const result: IndentGuide[][] = []; + for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { + const lineGuides = new Array(); + result.push(lineGuides); + + const bracketGuidesInLine = bracketGuides ? bracketGuides[lineNumber - visibleStartLineNumber] : []; + const bracketGuidesInLineQueue = new ArrayQueue(bracketGuidesInLine); + + const indentGuidesInLine = indentGuides ? indentGuides[lineNumber - visibleStartLineNumber] : []; + + for (let indentLvl = 1; indentLvl <= indentGuidesInLine; indentLvl++) { + const indentGuide = (indentLvl - 1) * indentSize + 1; + const isActive = + // Disable active indent guide if there are bracket guides. + bracketGuidesInLine.length === 0 && + activeIndentStartLineNumber <= lineNumber && + lineNumber <= activeIndentEndLineNumber && + indentLvl === activeIndentLevel; + lineGuides.push(...bracketGuidesInLineQueue.takeWhile(g => g.visibleColumn < indentGuide) || []); + const peeked = bracketGuidesInLineQueue.peek(); + if (!peeked || peeked.visibleColumn !== indentGuide || peeked.horizontalLine) { + lineGuides.push( + new IndentGuide( + indentGuide, + isActive ? 'core-guide-indent-active' : 'core-guide-indent', + null + ) + ); } } - output[lineIndex] = result; + lineGuides.push(...bracketGuidesInLineQueue.takeWhile(g => true) || []); } - this._renderResult = output; + + return result; } public render(startLineNumber: number, lineNumber: number): string { @@ -166,13 +242,66 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { } } +function transparentToUndefined(color: Color | undefined): Color | undefined { + if (color && color.isTransparent()) { + return undefined; + } + return color; +} + registerThemingParticipant((theme, collector) => { const editorIndentGuidesColor = theme.getColor(editorIndentGuides); if (editorIndentGuidesColor) { - collector.addRule(`.monaco-editor .lines-content .cigr { box-shadow: 1px 0 0 0 ${editorIndentGuidesColor} inset; }`); + collector.addRule(`.monaco-editor .lines-content .core-guide-indent { box-shadow: 1px 0 0 0 ${editorIndentGuidesColor} inset; }`); } const editorActiveIndentGuidesColor = theme.getColor(editorActiveIndentGuides) || editorIndentGuidesColor; if (editorActiveIndentGuidesColor) { - collector.addRule(`.monaco-editor .lines-content .cigra { box-shadow: 1px 0 0 0 ${editorActiveIndentGuidesColor} inset; }`); + collector.addRule(`.monaco-editor .lines-content .core-guide-indent-active { box-shadow: 1px 0 0 0 ${editorActiveIndentGuidesColor} inset; }`); + } + + const colors = [ + { bracketColor: editorBracketHighlightingForeground1, guideColor: editorBracketPairGuideBackground1, guideColorActive: editorBracketPairGuideActiveBackground1 }, + { bracketColor: editorBracketHighlightingForeground2, guideColor: editorBracketPairGuideBackground2, guideColorActive: editorBracketPairGuideActiveBackground2 }, + { bracketColor: editorBracketHighlightingForeground3, guideColor: editorBracketPairGuideBackground3, guideColorActive: editorBracketPairGuideActiveBackground3 }, + { bracketColor: editorBracketHighlightingForeground4, guideColor: editorBracketPairGuideBackground4, guideColorActive: editorBracketPairGuideActiveBackground4 }, + { bracketColor: editorBracketHighlightingForeground5, guideColor: editorBracketPairGuideBackground5, guideColorActive: editorBracketPairGuideActiveBackground5 }, + { bracketColor: editorBracketHighlightingForeground6, guideColor: editorBracketPairGuideBackground6, guideColorActive: editorBracketPairGuideActiveBackground6 } + ]; + const colorProvider = new BracketPairGuidesClassNames(); + + + let colorValues = colors + .map(c => { + const bracketColor = theme.getColor(c.bracketColor); + const guideColor = theme.getColor(c.guideColor); + const guideColorActive = theme.getColor(c.guideColorActive); + + const effectiveGuideColor = transparentToUndefined(transparentToUndefined(guideColor) ?? bracketColor?.transparent(0.3)); + const effectiveGuideColorActive = transparentToUndefined(transparentToUndefined(guideColorActive) ?? bracketColor); + + if (!effectiveGuideColor || !effectiveGuideColorActive) { + return undefined; + } + + return { + guideColor: effectiveGuideColor, + guideColorActive: effectiveGuideColorActive, + }; + }) + .filter(isDefined); + + if (colorValues.length > 0) { + for (let level = 0; level < 30; level++) { + const colors = colorValues[level % colorValues.length]; + collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level).replace(/ /g, '.')} { --guide-color: ${colors.guideColor}; --guide-color-active: ${colors.guideColorActive}; }`); + } + + collector.addRule(`.monaco-editor .vertical { box-shadow: 1px 0 0 0 var(--guide-color) inset; }`); + collector.addRule(`.monaco-editor .horizontal-top { border-top: 1px solid var(--guide-color); }`); + collector.addRule(`.monaco-editor .horizontal-bottom { border-bottom: 1px solid var(--guide-color); }`); + + collector.addRule(`.monaco-editor .vertical.${colorProvider.activeClassName} { box-shadow: 1px 0 0 0 var(--guide-color-active) inset; }`); + collector.addRule(`.monaco-editor .horizontal-top.${colorProvider.activeClassName} { border-top: 1px solid var(--guide-color-active); }`); + collector.addRule(`.monaco-editor .horizontal-bottom.${colorProvider.activeClassName} { border-bottom: 1px solid var(--guide-color-active); }`); } }); diff --git a/src/vs/editor/browser/viewParts/lines/rangeUtil.ts b/src/vs/editor/browser/viewParts/lines/rangeUtil.ts index efeff901da..41972bb10f 100644 --- a/src/vs/editor/browser/viewParts/lines/rangeUtil.ts +++ b/src/vs/editor/browser/viewParts/lines/rangeUtil.ts @@ -4,27 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Constants } from 'vs/base/common/uint'; -import { HorizontalRange } from 'vs/editor/common/view/renderingContext'; - -class FloatHorizontalRange { - _floatHorizontalRangeBrand: void = undefined; - - public readonly left: number; - public readonly width: number; - - constructor(left: number, width: number) { - this.left = left; - this.width = width; - } - - public toString(): string { - return `[${this.left},${this.width}]`; - } - - public static compare(a: FloatHorizontalRange, b: FloatHorizontalRange): number { - return a.left - b.left; - } -} +import { FloatHorizontalRange } from 'vs/editor/common/view/renderingContext'; export class RangeUtil { @@ -63,38 +43,33 @@ export class RangeUtil { } } - private static _mergeAdjacentRanges(ranges: FloatHorizontalRange[]): HorizontalRange[] { + private static _mergeAdjacentRanges(ranges: FloatHorizontalRange[]): FloatHorizontalRange[] { if (ranges.length === 1) { // There is nothing to merge - return [new HorizontalRange(ranges[0].left, ranges[0].width)]; + return ranges; } ranges.sort(FloatHorizontalRange.compare); - let result: HorizontalRange[] = [], resultLen = 0; - let prevLeft = ranges[0].left; - let prevWidth = ranges[0].width; + let result: FloatHorizontalRange[] = [], resultLen = 0; + let prev = ranges[0]; for (let i = 1, len = ranges.length; i < len; i++) { const range = ranges[i]; - const myLeft = range.left; - const myWidth = range.width; - - if (prevLeft + prevWidth + 0.9 /* account for browser's rounding errors*/ >= myLeft) { - prevWidth = Math.max(prevWidth, myLeft + myWidth - prevLeft); + if (prev.left + prev.width + 0.9 /* account for browser's rounding errors*/ >= range.left) { + prev.width = Math.max(prev.width, range.left + range.width - prev.left); } else { - result[resultLen++] = new HorizontalRange(prevLeft, prevWidth); - prevLeft = myLeft; - prevWidth = myWidth; + result[resultLen++] = prev; + prev = range; } } - result[resultLen++] = new HorizontalRange(prevLeft, prevWidth); + result[resultLen++] = prev; return result; } - private static _createHorizontalRangesFromClientRects(clientRects: DOMRectList | null, clientRectDeltaLeft: number): HorizontalRange[] | null { + private static _createHorizontalRangesFromClientRects(clientRects: DOMRectList | null, clientRectDeltaLeft: number): FloatHorizontalRange[] | null { if (!clientRects || clientRects.length === 0) { return null; } @@ -111,7 +86,7 @@ export class RangeUtil { return this._mergeAdjacentRanges(result); } - public static readHorizontalRanges(domNode: HTMLElement, startChildIndex: number, startOffset: number, endChildIndex: number, endOffset: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] | null { + public static readHorizontalRanges(domNode: HTMLElement, startChildIndex: number, startOffset: number, endChildIndex: number, endOffset: number, clientRectDeltaLeft: number, endNode: HTMLElement): FloatHorizontalRange[] | null { // Panic check const min = 0; const max = domNode.children.length - 1; diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 16fe85d523..e74178bb61 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -10,7 +10,7 @@ import { IVisibleLine } from 'vs/editor/browser/view/viewLayer'; import { RangeUtil } from 'vs/editor/browser/viewParts/lines/rangeUtil'; 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 { FloatHorizontalRange, VisibleRanges } from 'vs/editor/common/view/renderingContext'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine, LineRange, DomPosition } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -327,7 +327,7 @@ export class ViewLine implements IVisibleLine { } } - public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): VisibleRanges | null { + public getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): VisibleRanges | null { if (!this._renderedViewLine) { return null; } @@ -353,7 +353,7 @@ export class ViewLine implements IVisibleLine { endColumn = stopRenderingLineAfter + 1; } - const horizontalRanges = this._renderedViewLine.getVisibleRangesForRange(startColumn, endColumn, context); + const horizontalRanges = this._renderedViewLine.getVisibleRangesForRange(lineNumber, startColumn, endColumn, context); if (horizontalRanges && horizontalRanges.length > 0) { return new VisibleRanges(outsideRenderedLine, horizontalRanges); } @@ -374,7 +374,7 @@ interface IRenderedViewLine { readonly input: RenderLineInput; getWidth(): number; getWidthIsFast(): boolean; - getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null; + getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null; getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number; } @@ -398,7 +398,7 @@ class FastRenderedViewLine implements IRenderedViewLine { } public getWidth(): number { - return this._getCharPosition(this._characterMapping.length); + return Math.round(this._getCharPosition(this._characterMapping.length)); } public getWidthIsFast(): boolean { @@ -423,15 +423,15 @@ class FastRenderedViewLine implements IRenderedViewLine { return createRenderedLine(this.domNode, this.input, this._characterMapping, false, ForeignElementType.None); } - public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { + public getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null { const startPosition = this._getCharPosition(startColumn); const endPosition = this._getCharPosition(endColumn); - return [new HorizontalRange(startPosition, endPosition - startPosition)]; + return [new FloatHorizontalRange(startPosition, endPosition - startPosition)]; } private _getCharPosition(column: number): number { const charOffset = this._characterMapping.getAbsoluteOffset(column); - return Math.round(this._charWidth * charOffset); + return this._charWidth * charOffset; } public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number { @@ -463,7 +463,7 @@ class RenderedViewLine implements IRenderedViewLine { /** * This is a map that is used only when the line is guaranteed to have no RTL text. */ - private readonly _pixelOffsetCache: Int32Array | null; + private readonly _pixelOffsetCache: Float32Array | null; constructor(domNode: FastDomNode | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType) { this.domNode = domNode; @@ -475,7 +475,7 @@ class RenderedViewLine implements IRenderedViewLine { this._pixelOffsetCache = null; if (!containsRTL || this._characterMapping.length === 0 /* the line is empty */) { - this._pixelOffsetCache = new Int32Array(Math.max(2, this._characterMapping.length + 1)); + this._pixelOffsetCache = new Float32Array(Math.max(2, this._characterMapping.length + 1)); for (let column = 0, len = this._characterMapping.length; column <= len; column++) { this._pixelOffsetCache[column] = -1; } @@ -511,42 +511,42 @@ class RenderedViewLine implements IRenderedViewLine { /** * Visible ranges for a model range */ - public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { + public getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null { if (!this.domNode) { return null; } if (this._pixelOffsetCache !== null) { // the text is LTR - const startOffset = this._readPixelOffset(this.domNode, startColumn, context); + const startOffset = this._readPixelOffset(this.domNode, lineNumber, startColumn, context); if (startOffset === -1) { return null; } - const endOffset = this._readPixelOffset(this.domNode, endColumn, context); + const endOffset = this._readPixelOffset(this.domNode, lineNumber, endColumn, context); if (endOffset === -1) { return null; } - return [new HorizontalRange(startOffset, endOffset - startOffset)]; + return [new FloatHorizontalRange(startOffset, endOffset - startOffset)]; } - return this._readVisibleRangesForRange(this.domNode, startColumn, endColumn, context); + return this._readVisibleRangesForRange(this.domNode, lineNumber, startColumn, endColumn, context); } - protected _readVisibleRangesForRange(domNode: FastDomNode, startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { + protected _readVisibleRangesForRange(domNode: FastDomNode, lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null { if (startColumn === endColumn) { - const pixelOffset = this._readPixelOffset(domNode, startColumn, context); + const pixelOffset = this._readPixelOffset(domNode, lineNumber, startColumn, context); if (pixelOffset === -1) { return null; } else { - return [new HorizontalRange(pixelOffset, 0)]; + return [new FloatHorizontalRange(pixelOffset, 0)]; } } else { return this._readRawVisibleRangesForRange(domNode, startColumn, endColumn, context); } } - protected _readPixelOffset(domNode: FastDomNode, column: number, context: DomReadingContext): number { + protected _readPixelOffset(domNode: FastDomNode, lineNumber: number, column: number, context: DomReadingContext): number { if (this._characterMapping.length === 0) { // This line has no content if (this._containsForeignElements === ForeignElementType.None) { @@ -578,15 +578,15 @@ class RenderedViewLine implements IRenderedViewLine { return cachedPixelOffset; } - const result = this._actualReadPixelOffset(domNode, column, context); + const result = this._actualReadPixelOffset(domNode, lineNumber, column, context); this._pixelOffsetCache[column] = result; return result; } - return this._actualReadPixelOffset(domNode, column, context); + return this._actualReadPixelOffset(domNode, lineNumber, column, context); } - private _actualReadPixelOffset(domNode: FastDomNode, column: number, context: DomReadingContext): number { + private _actualReadPixelOffset(domNode: FastDomNode, lineNumber: number, column: number, context: DomReadingContext): number { if (this._characterMapping.length === 0) { // This line has no content const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), 0, 0, 0, 0, context.clientRectDeltaLeft, context.endNode); @@ -618,12 +618,12 @@ class RenderedViewLine implements IRenderedViewLine { return result; } - private _readRawVisibleRangesForRange(domNode: FastDomNode, startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { + private _readRawVisibleRangesForRange(domNode: FastDomNode, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null { if (startColumn === 1 && endColumn === this._characterMapping.length) { // This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line - return [new HorizontalRange(0, this.getWidth())]; + return [new FloatHorizontalRange(0, this.getWidth())]; } const startDomPosition = this._characterMapping.getDomPosition(startColumn); @@ -649,8 +649,8 @@ class RenderedViewLine implements IRenderedViewLine { } class WebKitRenderedViewLine extends RenderedViewLine { - protected override _readVisibleRangesForRange(domNode: FastDomNode, startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { - const output = super._readVisibleRangesForRange(domNode, startColumn, endColumn, context); + protected override _readVisibleRangesForRange(domNode: FastDomNode, lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null { + const output = super._readVisibleRangesForRange(domNode, lineNumber, startColumn, endColumn, context); if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) { return output; @@ -661,7 +661,7 @@ class WebKitRenderedViewLine extends RenderedViewLine { if (!this.input.containsRTL) { // This is an attempt to patch things up // Find position of last column - const endPixelOffset = this._readPixelOffset(domNode, endColumn, context); + const endPixelOffset = this._readPixelOffset(domNode, lineNumber, endColumn, context); if (endPixelOffset !== -1) { const lastRange = output[output.length - 1]; if (lastRange.left < endPixelOffset) { diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index ac5cb7e403..aa22747c95 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { IViewLines, LineVisibleRanges, VisibleRanges, HorizontalPosition } from 'vs/editor/common/view/renderingContext'; +import { IViewLines, LineVisibleRanges, VisibleRanges, HorizontalPosition, HorizontalRange } from 'vs/editor/common/view/renderingContext'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -423,7 +423,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, const startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1; const endColumn = lineNumber === range.endLineNumber ? range.endColumn : this._context.model.getLineMaxColumn(lineNumber); - const visibleRangesForLine = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(startColumn, endColumn, domReadingContext); + const visibleRangesForLine = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(lineNumber, startColumn, endColumn, domReadingContext); if (!visibleRangesForLine) { continue; @@ -438,7 +438,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, } } - visibleRanges[visibleRangesLen++] = new LineVisibleRanges(visibleRangesForLine.outsideRenderedLine, lineNumber, visibleRangesForLine.ranges); + visibleRanges[visibleRangesLen++] = new LineVisibleRanges(visibleRangesForLine.outsideRenderedLine, lineNumber, HorizontalRange.from(visibleRangesForLine.ranges)); } if (visibleRangesLen === 0) { @@ -459,7 +459,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, return null; } - return this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(startColumn, endColumn, new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot)); + return this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(lineNumber, startColumn, endColumn, new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot)); } public visibleRangeForPosition(position: Position): HorizontalPosition | null { @@ -723,8 +723,8 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, return null; } for (const visibleRange of visibleRanges.ranges) { - boxStartX = Math.min(boxStartX, visibleRange.left); - boxEndX = Math.max(boxEndX, visibleRange.left + visibleRange.width); + boxStartX = Math.min(boxStartX, Math.round(visibleRange.left)); + boxEndX = Math.max(boxEndX, Math.round(visibleRange.left + visibleRange.width)); } } else { for (const selection of horizontalRevealRequest.selections) { @@ -736,8 +736,8 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, return null; } for (const visibleRange of visibleRanges.ranges) { - boxStartX = Math.min(boxStartX, visibleRange.left); - boxEndX = Math.max(boxEndX, visibleRange.left + visibleRange.width); + boxStartX = Math.min(boxStartX, Math.round(visibleRange.left)); + boxEndX = Math.max(boxEndX, Math.round(visibleRange.left + visibleRange.width)); } } } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 86579a1e82..1cada47725 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -25,7 +25,7 @@ import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/v import { ViewContext, EditorTheme } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewLineData, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel'; -import { minimapSelection, scrollbarShadow, minimapBackground, minimapSliderBackground, minimapSliderHoverBackground, minimapSliderActiveBackground } from 'vs/platform/theme/common/colorRegistry'; +import { minimapSelection, scrollbarShadow, minimapBackground, minimapSliderBackground, minimapSliderHoverBackground, minimapSliderActiveBackground, minimapForegroundOpacity } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'; import { Selection } from 'vs/editor/common/core/selection'; @@ -98,7 +98,12 @@ class MinimapOptions { public readonly minimapCharWidth: number; public readonly charRenderer: () => MinimapCharRenderer; + public readonly defaultBackgroundColor: RGBA8; public readonly backgroundColor: RGBA8; + /** + * foreground alpha: integer in [0-255] + */ + public readonly foregroundAlpha: number; constructor(configuration: IConfiguration, theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker) { const options = configuration.options; @@ -132,15 +137,25 @@ class MinimapOptions { this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale; this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); - this.backgroundColor = MinimapOptions._getMinimapBackground(theme, tokensColorTracker); + this.defaultBackgroundColor = tokensColorTracker.getColor(ColorId.DefaultBackground); + this.backgroundColor = MinimapOptions._getMinimapBackground(theme, this.defaultBackgroundColor); + this.foregroundAlpha = MinimapOptions._getMinimapForegroundOpacity(theme); } - private static _getMinimapBackground(theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker): RGBA8 { + private static _getMinimapBackground(theme: EditorTheme, defaultBackgroundColor: RGBA8): RGBA8 { const themeColor = theme.getColor(minimapBackground); if (themeColor) { - return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, themeColor.rgba.a); + return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a)); } - return tokensColorTracker.getColor(ColorId.DefaultBackground); + return defaultBackgroundColor; + } + + private static _getMinimapForegroundOpacity(theme: EditorTheme): number { + const themeColor = theme.getColor(minimapForegroundOpacity); + if (themeColor) { + return RGBA8._clamp(Math.round(255 * themeColor.rgba.a)); + } + return 255; } public equals(other: MinimapOptions): boolean { @@ -164,7 +179,9 @@ class MinimapOptions { && this.fontScale === other.fontScale && this.minimapLineHeight === other.minimapLineHeight && this.minimapCharWidth === other.minimapCharWidth + && this.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor) && this.backgroundColor && this.backgroundColor.equals(other.backgroundColor) + && this.foregroundAlpha === other.foregroundAlpha ); } } @@ -467,6 +484,7 @@ class MinimapBuffers { const backgroundR = background.r; const backgroundG = background.g; const backgroundB = background.b; + const backgroundA = background.a; const result = new Uint8ClampedArray(WIDTH * HEIGHT * 4); let offset = 0; @@ -475,7 +493,7 @@ class MinimapBuffers { result[offset] = backgroundR; result[offset + 1] = backgroundG; result[offset + 2] = backgroundB; - result[offset + 3] = 255; + result[offset + 3] = backgroundA; offset += 4; } } @@ -491,6 +509,7 @@ export interface IMinimapModel { getLineCount(): number; getRealLineCount(): number; getLineContent(lineNumber: number): string; + getLineMaxColumn(lineNumber: number): number; getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[]; getSelections(): Selection[]; getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[]; @@ -951,6 +970,13 @@ export class Minimap extends ViewPart implements IMinimapModel { return this._context.model.getLineContent(lineNumber); } + public getLineMaxColumn(lineNumber: number): number { + if (this._samplingState) { + return this._context.model.getLineMaxColumn(this._samplingState.minimapLines[lineNumber - 1]); + } + return this._context.model.getLineMaxColumn(lineNumber); + } + public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[] { if (this._samplingState) { let result: (ViewLineData | null)[] = []; @@ -1363,10 +1389,8 @@ class InnerMinimap extends Disposable { this._slider.setHeight(layout.sliderHeight); // Compute horizontal slider coordinates - const scrollLeftChars = renderingCtx.scrollLeft / this._model.options.typicalHalfwidthCharacterWidth; - const horizontalSliderLeft = Math.min(this._model.options.minimapWidth, Math.round(scrollLeftChars * this._model.options.minimapCharWidth / this._model.options.pixelRatio)); - this._sliderHorizontal.setLeft(horizontalSliderLeft); - this._sliderHorizontal.setWidth(this._model.options.minimapWidth - horizontalSliderLeft); + this._sliderHorizontal.setLeft(0); + this._sliderHorizontal.setWidth(this._model.options.minimapWidth); this._sliderHorizontal.setTop(0); this._sliderHorizontal.setHeight(layout.sliderHeight); @@ -1378,7 +1402,10 @@ class InnerMinimap extends Disposable { if (this._renderDecorations) { this._renderDecorations = false; const selections = this._model.getSelections(); + selections.sort(Range.compareRangesUsingStarts); + const decorations = this._model.getMinimapDecorationsInViewport(layout.startLineNumber, layout.endLineNumber); + decorations.sort((a, b) => (a.options.zIndex || 0) - (b.options.zIndex || 0)); const { canvasInnerWidth, canvasInnerHeight } = this._model.options; const lineHeight = this._model.options.minimapLineHeight; @@ -1388,44 +1415,197 @@ class InnerMinimap extends Disposable { canvasContext.clearRect(0, 0, canvasInnerWidth, canvasInnerHeight); - const lineOffsetMap = new Map(); - for (let i = 0; i < selections.length; i++) { - const selection = selections[i]; + // We first need to render line highlights and then render decorations on top of those. + // But we need to pick a single color for each line, and use that as a line highlight. + // This needs to be the color of the decoration with the highest `zIndex`, but priority + // is given to the selection. - for (let line = selection.startLineNumber; line <= selection.endLineNumber; line++) { - this.renderDecorationOnLine(canvasContext, lineOffsetMap, selection, this._selectionColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth); - } + const highlightedLines = new ContiguousLineMap(layout.startLineNumber, layout.endLineNumber, false); + this._renderSelectionLineHighlights(canvasContext, selections, highlightedLines, layout, lineHeight); + this._renderDecorationsLineHighlights(canvasContext, decorations, highlightedLines, layout, lineHeight); + + const lineOffsetMap = new ContiguousLineMap(layout.startLineNumber, layout.endLineNumber, null); + this._renderSelectionsHighlights(canvasContext, selections, lineOffsetMap, layout, lineHeight, tabSize, characterWidth, canvasInnerWidth); + this._renderDecorationsHighlights(canvasContext, decorations, lineOffsetMap, layout, lineHeight, tabSize, characterWidth, canvasInnerWidth); + } + } + + private _renderSelectionLineHighlights( + canvasContext: CanvasRenderingContext2D, + selections: Selection[], + highlightedLines: ContiguousLineMap, + layout: MinimapLayout, + lineHeight: number + ): void { + if (!this._selectionColor || this._selectionColor.isTransparent()) { + return; + } + + canvasContext.fillStyle = this._selectionColor.transparent(0.5).toString(); + + let y1 = 0; + let y2 = 0; + + for (const selection of selections) { + const startLineNumber = Math.max(layout.startLineNumber, selection.startLineNumber); + const endLineNumber = Math.min(layout.endLineNumber, selection.endLineNumber); + if (startLineNumber > endLineNumber) { + // entirely outside minimap's viewport + continue; } - // Loop over decorations, ignoring those that don't have the minimap property set and rendering rectangles for each line the decoration spans - for (let i = 0; i < decorations.length; i++) { - const decoration = decorations[i]; + for (let line = startLineNumber; line <= endLineNumber; line++) { + highlightedLines.set(line, true); + } - if (!decoration.options.minimap) { + const yy1 = (startLineNumber - layout.startLineNumber) * lineHeight; + const yy2 = (endLineNumber - layout.startLineNumber) * lineHeight + lineHeight; + + if (y2 >= yy1) { + // merge into previous + y2 = yy2; + } else { + if (y2 > y1) { + // flush + canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y1, canvasContext.canvas.width, y2 - y1); + } + y1 = yy1; + y2 = yy2; + } + } + + if (y2 > y1) { + // flush + canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y1, canvasContext.canvas.width, y2 - y1); + } + } + + private _renderDecorationsLineHighlights( + canvasContext: CanvasRenderingContext2D, + decorations: ViewModelDecoration[], + highlightedLines: ContiguousLineMap, + layout: MinimapLayout, + lineHeight: number + ): void { + + const highlightColors = new Map(); + + // Loop backwards to hit first decorations with higher `zIndex` + for (let i = decorations.length - 1; i >= 0; i--) { + const decoration = decorations[i]; + + const minimapOptions = decoration.options.minimap; + if (!minimapOptions || minimapOptions.position !== MinimapPosition.Inline) { + continue; + } + + const startLineNumber = Math.max(layout.startLineNumber, decoration.range.startLineNumber); + const endLineNumber = Math.min(layout.endLineNumber, decoration.range.endLineNumber); + if (startLineNumber > endLineNumber) { + // entirely outside minimap's viewport + continue; + } + + const decorationColor = minimapOptions.getColor(this._theme); + if (!decorationColor || decorationColor.isTransparent()) { + continue; + } + + let highlightColor = highlightColors.get(decorationColor.toString()); + if (!highlightColor) { + highlightColor = decorationColor.transparent(0.5).toString(); + highlightColors.set(decorationColor.toString(), highlightColor); + } + + canvasContext.fillStyle = highlightColor; + for (let line = startLineNumber; line <= endLineNumber; line++) { + if (highlightedLines.has(line)) { continue; } + highlightedLines.set(line, true); + const y = (startLineNumber - layout.startLineNumber) * lineHeight; + canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y, canvasContext.canvas.width, lineHeight); + } + } + } - const decorationColor = (decoration.options.minimap).getColor(this._theme); - for (let line = decoration.range.startLineNumber; line <= decoration.range.endLineNumber; line++) { - switch (decoration.options.minimap.position) { + private _renderSelectionsHighlights( + canvasContext: CanvasRenderingContext2D, + selections: Selection[], + lineOffsetMap: ContiguousLineMap, + layout: MinimapLayout, + lineHeight: number, + tabSize: number, + characterWidth: number, + canvasInnerWidth: number + ): void { + if (!this._selectionColor || this._selectionColor.isTransparent()) { + return; + } + for (const selection of selections) { + const startLineNumber = Math.max(layout.startLineNumber, selection.startLineNumber); + const endLineNumber = Math.min(layout.endLineNumber, selection.endLineNumber); + if (startLineNumber > endLineNumber) { + // entirely outside minimap's viewport + continue; + } - case MinimapPosition.Inline: - this.renderDecorationOnLine(canvasContext, lineOffsetMap, decoration.range, decorationColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth); - continue; + for (let line = startLineNumber; line <= endLineNumber; line++) { + this.renderDecorationOnLine(canvasContext, lineOffsetMap, selection, this._selectionColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth, canvasInnerWidth); + } + } + } - case MinimapPosition.Gutter: - const y = (line - layout.startLineNumber) * lineHeight; - const x = 2; - this.renderDecoration(canvasContext, decorationColor, x, y, GUTTER_DECORATION_WIDTH, lineHeight); - continue; - } + private _renderDecorationsHighlights( + canvasContext: CanvasRenderingContext2D, + decorations: ViewModelDecoration[], + lineOffsetMap: ContiguousLineMap, + layout: MinimapLayout, + lineHeight: number, + tabSize: number, + characterWidth: number, + canvasInnerWidth: number + ): void { + // Loop forwards to hit first decorations with lower `zIndex` + for (const decoration of decorations) { + + const minimapOptions = decoration.options.minimap; + if (!minimapOptions) { + continue; + } + + const startLineNumber = Math.max(layout.startLineNumber, decoration.range.startLineNumber); + const endLineNumber = Math.min(layout.endLineNumber, decoration.range.endLineNumber); + if (startLineNumber > endLineNumber) { + // entirely outside minimap's viewport + continue; + } + + const decorationColor = minimapOptions.getColor(this._theme); + if (!decorationColor || decorationColor.isTransparent()) { + continue; + } + + for (let line = startLineNumber; line <= endLineNumber; line++) { + switch (minimapOptions.position) { + + case MinimapPosition.Inline: + this.renderDecorationOnLine(canvasContext, lineOffsetMap, decoration.range, decorationColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth, canvasInnerWidth); + continue; + + case MinimapPosition.Gutter: + const y = (line - layout.startLineNumber) * lineHeight; + const x = 2; + this.renderDecoration(canvasContext, decorationColor, x, y, GUTTER_DECORATION_WIDTH, lineHeight); + continue; } } } } - private renderDecorationOnLine(canvasContext: CanvasRenderingContext2D, - lineOffsetMap: Map, + private renderDecorationOnLine( + canvasContext: CanvasRenderingContext2D, + lineOffsetMap: ContiguousLineMap, decorationRange: Range, decorationColor: Color | undefined, layout: MinimapLayout, @@ -1433,7 +1613,9 @@ class InnerMinimap extends Disposable { height: number, lineHeight: number, tabSize: number, - charWidth: number): void { + charWidth: number, + canvasInnerWidth: number + ): void { const y = (lineNumber - layout.startLineNumber) * lineHeight; // Skip rendering the line if it's vertically outside our viewport @@ -1441,12 +1623,41 @@ class InnerMinimap extends Disposable { return; } + const { startLineNumber, endLineNumber } = decorationRange; + const startColumn = (startLineNumber === lineNumber ? decorationRange.startColumn : 1); + const endColumn = (endLineNumber === lineNumber ? decorationRange.endColumn : this._model.getLineMaxColumn(lineNumber)); + + const x1 = this.getXOffsetForPosition(lineOffsetMap, lineNumber, startColumn, tabSize, charWidth, canvasInnerWidth); + const x2 = this.getXOffsetForPosition(lineOffsetMap, lineNumber, endColumn, tabSize, charWidth, canvasInnerWidth); + + this.renderDecoration(canvasContext, decorationColor, x1, y, x2 - x1, height); + } + + private getXOffsetForPosition( + lineOffsetMap: ContiguousLineMap, + lineNumber: number, + column: number, + tabSize: number, + charWidth: number, + canvasInnerWidth: number + ): number { + if (column === 1) { + return MINIMAP_GUTTER_WIDTH; + } + + const minimumXOffset = (column - 1) * charWidth; + if (minimumXOffset >= canvasInnerWidth) { + // there is no need to look at actual characters, + // as this column is certainly after the minimap width + return canvasInnerWidth; + } + // Cache line offset data so that it is only read once per line let lineIndexToXOffset = lineOffsetMap.get(lineNumber); - const isFirstDecorationForLine = !lineIndexToXOffset; if (!lineIndexToXOffset) { const lineData = this._model.getLineContent(lineNumber); lineIndexToXOffset = [MINIMAP_GUTTER_WIDTH]; + let prevx = MINIMAP_GUTTER_WIDTH; for (let i = 1; i < lineData.length + 1; i++) { const charCode = lineData.charCodeAt(i - 1); const dx = charCode === CharCode.Tab @@ -1455,33 +1666,25 @@ class InnerMinimap extends Disposable { ? 2 * charWidth : charWidth; - lineIndexToXOffset[i] = lineIndexToXOffset[i - 1] + dx; + const x = prevx + dx; + if (x >= canvasInnerWidth) { + // no need to keep on going, as we've hit the canvas width + lineIndexToXOffset[i] = canvasInnerWidth; + break; + } + + lineIndexToXOffset[i] = x; + prevx = x; } lineOffsetMap.set(lineNumber, lineIndexToXOffset); } - const { startColumn, endColumn, startLineNumber, endLineNumber } = decorationRange; - const x = startLineNumber === lineNumber ? lineIndexToXOffset[startColumn - 1] : MINIMAP_GUTTER_WIDTH; - - const endColumnForLine = endLineNumber > lineNumber ? lineIndexToXOffset.length - 1 : endColumn - 1; - - if (endColumnForLine > 0) { - // If the decoration starts at the last character of the column and spans over it, ensure it has a width - const width = lineIndexToXOffset[endColumnForLine] - x || 2; - - this.renderDecoration(canvasContext, decorationColor, x, y, width, height); + if (column - 1 < lineIndexToXOffset.length) { + return lineIndexToXOffset[column - 1]; } - - if (isFirstDecorationForLine) { - this.renderLineHighlight(canvasContext, decorationColor, y, height); - } - - } - - private renderLineHighlight(canvasContext: CanvasRenderingContext2D, decorationColor: Color | undefined, y: number, height: number): void { - canvasContext.fillStyle = decorationColor && decorationColor.transparent(0.5).toString() || ''; - canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y, canvasContext.canvas.width, height); + // goes over the canvas width + return canvasInnerWidth; } private renderDecoration(canvasContext: CanvasRenderingContext2D, decorationColor: Color | undefined, x: number, y: number, width: number, height: number) { @@ -1521,7 +1724,9 @@ class InnerMinimap extends Disposable { // Fetch rendering info from view model for rest of lines that need rendering. const lineInfo = this._model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed); const tabSize = this._model.getOptions().tabSize; + const defaultBackground = this._model.options.defaultBackgroundColor; const background = this._model.options.backgroundColor; + const foregroundAlpha = this._model.options.foregroundAlpha; const tokensColorTracker = this._model.tokensColorTracker; const useLighterFont = tokensColorTracker.backgroundIsLight(); const renderMinimap = this._model.options.renderMinimap; @@ -1534,17 +1739,26 @@ class InnerMinimap extends Disposable { const innerLinePadding = (minimapLineHeight > renderMinimapLineHeight ? Math.floor((minimapLineHeight - renderMinimapLineHeight) / 2) : 0); // Render the rest of lines + const backgroundA = background.a / 255; + const renderBackground = new RGBA8( + Math.round((background.r - defaultBackground.r) * backgroundA + defaultBackground.r), + Math.round((background.g - defaultBackground.g) * backgroundA + defaultBackground.g), + Math.round((background.b - defaultBackground.b) * backgroundA + defaultBackground.b), + 255 + ); let dy = 0; const renderedLines: MinimapLine[] = []; for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) { if (needed[lineIndex]) { InnerMinimap._renderLine( imageData, - background, + renderBackground, + background.a, useLighterFont, renderMinimap, minimapCharWidth, tokensColorTracker, + foregroundAlpha, charRenderer, dy, innerLinePadding, @@ -1669,10 +1883,12 @@ class InnerMinimap extends Disposable { private static _renderLine( target: ImageData, backgroundColor: RGBA8, + backgroundAlpha: number, useLighterFont: boolean, renderMinimap: RenderMinimap, charWidth: number, colorTracker: MinimapTokensColorTracker, + foregroundAlpha: number, minimapCharRenderer: MinimapCharRenderer, dy: number, innerLinePadding: number, @@ -1716,9 +1932,9 @@ class InnerMinimap extends Disposable { for (let i = 0; i < count; i++) { if (renderMinimap === RenderMinimap.Blocks) { - minimapCharRenderer.blockRenderChar(target, dx, dy + innerLinePadding, tokenColor, backgroundColor, useLighterFont, force1pxHeight); + minimapCharRenderer.blockRenderChar(target, dx, dy + innerLinePadding, tokenColor, foregroundAlpha, backgroundColor, backgroundAlpha, force1pxHeight); } else { // RenderMinimap.Text - minimapCharRenderer.renderChar(target, dx, dy + innerLinePadding, charCode, tokenColor, backgroundColor, fontScale, useLighterFont, force1pxHeight); + minimapCharRenderer.renderChar(target, dx, dy + innerLinePadding, charCode, tokenColor, foregroundAlpha, backgroundColor, backgroundAlpha, fontScale, useLighterFont, force1pxHeight); } dx += charWidth; @@ -1734,11 +1950,43 @@ class InnerMinimap extends Disposable { } } -registerThemingParticipant((theme, collector) => { - const minimapBackgroundValue = theme.getColor(minimapBackground); - if (minimapBackgroundValue) { - collector.addRule(`.monaco-editor .minimap > canvas { opacity: ${minimapBackgroundValue.rgba.a}; will-change: opacity; }`); +class ContiguousLineMap { + + private readonly _startLineNumber: number; + private readonly _endLineNumber: number; + private readonly _defaultValue: T; + private readonly _values: T[]; + + constructor(startLineNumber: number, endLineNumber: number, defaultValue: T) { + this._startLineNumber = startLineNumber; + this._endLineNumber = endLineNumber; + this._defaultValue = defaultValue; + this._values = []; + for (let i = 0, count = this._endLineNumber - this._startLineNumber + 1; i < count; i++) { + this._values[i] = defaultValue; + } } + + public has(lineNumber: number): boolean { + return (this.get(lineNumber) !== this._defaultValue); + } + + public set(lineNumber: number, value: T): void { + if (lineNumber < this._startLineNumber || lineNumber > this._endLineNumber) { + return; + } + this._values[lineNumber - this._startLineNumber] = value; + } + + public get(lineNumber: number): T { + if (lineNumber < this._startLineNumber || lineNumber > this._endLineNumber) { + return this._defaultValue; + } + return this._values[lineNumber - this._startLineNumber]; + } +} + +registerThemingParticipant((theme, collector) => { const sliderBackground = theme.getColor(minimapSliderBackground); if (sliderBackground) { collector.addRule(`.monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${sliderBackground}; }`); diff --git a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts index 9de5def7da..5711c24c90 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts @@ -32,7 +32,9 @@ export class MinimapCharRenderer { dy: number, chCode: number, color: RGBA8, + foregroundAlpha: number, backgroundColor: RGBA8, + backgroundAlpha: number, fontScale: number, useLighterFont: boolean, force1pxHeight: boolean @@ -58,6 +60,8 @@ export class MinimapCharRenderer { const deltaG = color.g - backgroundG; const deltaB = color.b - backgroundB; + const destAlpha = Math.max(foregroundAlpha, backgroundAlpha); + const dest = target.data; let sourceOffset = charIndex * charWidth * charHeight; @@ -65,11 +69,11 @@ export class MinimapCharRenderer { for (let y = 0; y < renderHeight; y++) { let column = row; for (let x = 0; x < charWidth; x++) { - const c = charData[sourceOffset++] / 255; + const c = (charData[sourceOffset++] / 255) * (foregroundAlpha / 255); dest[column++] = backgroundR + deltaR * c; dest[column++] = backgroundG + deltaG * c; dest[column++] = backgroundB + deltaB * c; - column++; + dest[column++] = destAlpha; } row += destWidth; @@ -81,8 +85,9 @@ export class MinimapCharRenderer { dx: number, dy: number, color: RGBA8, + foregroundAlpha: number, backgroundColor: RGBA8, - useLighterFont: boolean, + backgroundAlpha: number, force1pxHeight: boolean ): void { const charWidth = Constants.BASE_CHAR_WIDTH * this.scale; @@ -95,7 +100,7 @@ export class MinimapCharRenderer { const destWidth = target.width * Constants.RGBA_CHANNELS_CNT; - const c = 0.5; + const c = 0.5 * (foregroundAlpha / 255); const backgroundR = backgroundColor.r; const backgroundG = backgroundColor.g; @@ -109,6 +114,8 @@ export class MinimapCharRenderer { const colorG = backgroundG + deltaG * c; const colorB = backgroundB + deltaB * c; + const destAlpha = Math.max(foregroundAlpha, backgroundAlpha); + const dest = target.data; let row = dy * destWidth + dx * Constants.RGBA_CHANNELS_CNT; @@ -118,7 +125,7 @@ export class MinimapCharRenderer { dest[column++] = colorR; dest[column++] = colorG; dest[column++] = colorB; - column++; + dest[column++] = destAlpha; } row += destWidth; diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 96dd9ccae9..4668ca5cfe 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -15,6 +15,7 @@ import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/v import { ViewContext, EditorTheme } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { OverviewRulerDecorationsGroup } from 'vs/editor/common/viewModel/viewModel'; class Settings { @@ -340,24 +341,22 @@ export class DecorationsOverviewRuler extends ViewPart { const x = this._settings.x; const w = this._settings.w; - // Avoid flickering by always rendering the colors in the same order - // colors that don't use transparency will be sorted last (they start with #) - const colors = Object.keys(decorations); - colors.sort(); - for (let cIndex = 0, cLen = colors.length; cIndex < cLen; cIndex++) { - const color = colors[cIndex]; - const colorDecorations = decorations[color]; + decorations.sort(OverviewRulerDecorationsGroup.cmp); + + for (const decorationGroup of decorations) { + const color = decorationGroup.color; + const decorationGroupData = decorationGroup.data; canvasCtx.fillStyle = color; let prevLane = 0; let prevY1 = 0; let prevY2 = 0; - for (let i = 0, len = colorDecorations.length; i < len; i++) { - const lane = colorDecorations[3 * i]; - const startLineNumber = colorDecorations[3 * i + 1]; - const endLineNumber = colorDecorations[3 * i + 2]; + for (let i = 0, len = decorationGroupData.length / 3; i < len; i++) { + const lane = decorationGroupData[3 * i]; + const startLineNumber = decorationGroupData[3 * i + 1]; + const endLineNumber = decorationGroupData[3 * i + 2]; let y1 = (viewLayout.getVerticalOffsetForLineNumber(startLineNumber) * heightRatio) | 0; let y2 = ((viewLayout.getVerticalOffsetForLineNumber(endLineNumber) + lineHeight) * heightRatio) | 0; diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 54c8faf8ea..675a455ecc 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -18,12 +18,14 @@ import { IWhitespaceChangeAccessor, IEditorWhitespace } from 'vs/editor/common/v export interface IMyViewZone { whitespaceId: string; delegate: IViewZone; + isInHiddenArea: boolean; isVisible: boolean; domNode: FastDomNode; marginDomNode: FastDomNode | null; } interface IComputedViewZoneProps { + isInHiddenArea: boolean; afterViewLineNumber: number; heightInPx: number; minWidthInPx: number; @@ -86,6 +88,7 @@ export class ViewZones extends ViewPart { const id = keys[i]; const zone = this._zones[id]; const props = this._computeWhitespaceProps(zone.delegate); + zone.isInHiddenArea = props.isInHiddenArea; const oldWhitespace = oldWhitespaces.get(id); if (oldWhitespace && (oldWhitespace.afterLineNumber !== props.afterViewLineNumber || oldWhitespace.height !== props.heightInPx)) { whitespaceAccessor.changeOneWhitespace(id, props.afterViewLineNumber, props.heightInPx); @@ -146,6 +149,7 @@ export class ViewZones extends ViewPart { private _computeWhitespaceProps(zone: IViewZone): IComputedViewZoneProps { if (zone.afterLineNumber === 0) { return { + isInHiddenArea: false, afterViewLineNumber: 0, heightInPx: this._heightInPixels(zone), minWidthInPx: this._minWidthInPixels(zone) @@ -186,6 +190,7 @@ export class ViewZones extends ViewPart { const viewPosition = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(zoneAfterModelPosition); const isVisible = this._context.model.coordinatesConverter.modelPositionIsVisible(zoneBeforeModelPosition); return { + isInHiddenArea: !isVisible, afterViewLineNumber: viewPosition.lineNumber, heightInPx: (isVisible ? this._heightInPixels(zone) : 0), minWidthInPx: this._minWidthInPixels(zone) @@ -234,6 +239,7 @@ export class ViewZones extends ViewPart { const myZone: IMyViewZone = { whitespaceId: whitespaceId, delegate: zone, + isInHiddenArea: props.isInHiddenArea, isVisible: false, domNode: createFastDomNode(zone.domNode), marginDomNode: zone.marginDomNode ? createFastDomNode(zone.marginDomNode) : null @@ -290,6 +296,7 @@ export class ViewZones extends ViewPart { if (this._zones.hasOwnProperty(id)) { const zone = this._zones[id]; const props = this._computeWhitespaceProps(zone.delegate); + zone.isInHiddenArea = props.isInHiddenArea; // const newOrdinal = this._getZoneOrdinal(zone.delegate); whitespaceAccessor.changeOneWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx); // TODO@Alex: change `newOrdinal` too @@ -356,8 +363,11 @@ export class ViewZones extends ViewPart { const visibleZones: { [id: string]: IViewWhitespaceViewportData; } = {}; let hasVisibleZone = false; - for (let i = 0, len = visibleWhitespaces.length; i < len; i++) { - visibleZones[visibleWhitespaces[i].id] = visibleWhitespaces[i]; + for (const visibleWhitespace of visibleWhitespaces) { + if (this._zones[visibleWhitespace.id].isInHiddenArea) { + continue; + } + visibleZones[visibleWhitespace.id] = visibleWhitespace; hasVisibleZone = true; } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index dccaafc6a8..361cf10fb8 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -207,6 +207,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onDidChangeViewZones: Emitter = this._register(new Emitter()); public readonly onDidChangeViewZones: Event = this._onDidChangeViewZones.event; + + private readonly _onDidChangeHiddenAreas: Emitter = this._register(new Emitter()); + public readonly onDidChangeHiddenAreas: Event = this._onDidChangeHiddenAreas.event; //#endregion public readonly isSimpleWidget: boolean; @@ -1101,7 +1104,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (source === 'keyboard') { this._onDidPaste.fire({ range: new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), - mode: mode + languageId: mode }); } } @@ -1495,7 +1498,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const listenersToRemove: IDisposable[] = []; - this._domElement.setAttribute('data-mode-id', model.getLanguageIdentifier().language); + this._domElement.setAttribute('data-mode-id', model.getLanguageId()); this._configuration.setIsDominatedByLongLines(model.isDominatedByLongLines()); this._configuration.setMaxLineNumber(model.getLineCount()); @@ -1512,7 +1515,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE listenersToRemove.push(model.onDidChangeDecorations((e) => this._onDidChangeModelDecorations.fire(e))); listenersToRemove.push(model.onDidChangeLanguage((e) => { - this._domElement.setAttribute('data-mode-id', model.getLanguageIdentifier().language); + this._domElement.setAttribute('data-mode-id', model.getLanguageId()); this._onDidChangeModelLanguage.fire(e); })); listenersToRemove.push(model.onDidChangeLanguageConfiguration((e) => this._onDidChangeModelLanguageConfiguration.fire(e))); @@ -1535,6 +1538,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE case OutgoingViewModelEventKind.ViewZonesChanged: this._onDidChangeViewZones.fire(); break; + case OutgoingViewModelEventKind.HiddenAreasChanged: + this._onDidChangeHiddenAreas.fire(); + break; case OutgoingViewModelEventKind.ReadOnlyEditAttempt: this._onDidAttemptReadOnlyEdit.fire(); break; @@ -1955,7 +1961,7 @@ export class EditorModeContext extends Disposable { return; } this._contextKeyService.bufferChangeEvents(() => { - this._langId.set(model.getLanguageIdentifier().language); + this._langId.set(model.getLanguageId()); this._hasCompletionItemProvider.set(modes.CompletionProviderRegistry.has(model)); this._hasCodeActionsProvider.set(modes.CodeActionProviderRegistry.has(model)); this._hasCodeLensProvider.set(modes.CodeLensProviderRegistry.has(model)); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 4ae817711a..d1bb301a4f 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -19,7 +19,7 @@ import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffReview } from 'vs/editor/browser/widget/diffReview'; -import { IDiffEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption } from 'vs/editor/common/config/editorOptions'; +import { IDiffEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption, ValidDiffEditorBaseOptions, clampedInt } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; @@ -138,7 +138,10 @@ class VisualEditorState { if (newDecorations.zones[i].diff && viewZone.marginDomNode) { viewZone.suppressMouseDown = false; - this._inlineDiffMargins.push(new InlineDiffMargin(zoneId, viewZone.marginDomNode, editor, newDecorations.zones[i].diff!, this._contextMenuService, this._clipboardService)); + if (newDecorations.zones[i].diff?.originalModel.getValueLength() !== 0) { + // do not contribute diff margin actions for newly created files + this._inlineDiffMargins.push(new InlineDiffMargin(zoneId, viewZone.marginDomNode, editor, newDecorations.zones[i].diff!, this._contextMenuService, this._clipboardService)); + } } } }); @@ -208,16 +211,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _isVisible: boolean; private _isHandlingScrollEvent: boolean; - private _ignoreTrimWhitespace: boolean; - private _originalIsEditable: boolean; - private _diffCodeLens: boolean; - private _diffWordWrap: 'off' | 'on' | 'inherit'; + private _options: ValidDiffEditorBaseOptions; - private _renderSideBySide: boolean; - private _maxComputationTime: number; - private _renderIndicators: boolean; - private _enableSplitViewResizing: boolean; - private _renderOverviewRuler: boolean; private _strategy!: DiffEditorWidgetStyle; private readonly _updateDecorationsRunner: RunOnceScheduler; @@ -230,7 +225,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private readonly _notificationService: INotificationService; private readonly _reviewPane: DiffReview; - private _options: IDiffEditorOptions; // {{SQL CARBON EDIT}} constructor( domElement: HTMLElement, @@ -256,8 +250,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._themeService = themeService; this._notificationService = notificationService; - this._options = options; // {{SQL CARBON EDIT}} - this._id = (++DIFF_EDITOR_ID); this._state = editorBrowser.DiffEditorState.Idle; this._updatingDiffProgress = null; @@ -265,33 +257,19 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._domElement = domElement; options = options || {}; - // renderSideBySide - this._renderSideBySide = true; - if (typeof options.renderSideBySide !== 'undefined') { - this._renderSideBySide = options.renderSideBySide; - } - - // maxComputationTime - this._maxComputationTime = 5000; - if (typeof options.maxComputationTime !== 'undefined') { - this._maxComputationTime = options.maxComputationTime; - } - - // ignoreTrimWhitespace - this._ignoreTrimWhitespace = true; - if (typeof options.ignoreTrimWhitespace !== 'undefined') { - this._ignoreTrimWhitespace = options.ignoreTrimWhitespace; - } - - // renderIndicators - this._renderIndicators = true; - if (typeof options.renderIndicators !== 'undefined') { - this._renderIndicators = options.renderIndicators; - } - - this._originalIsEditable = validateBooleanOption(options.originalEditable, false); - this._diffCodeLens = validateBooleanOption(options.diffCodeLens, false); - this._diffWordWrap = validateDiffWordWrap(options.diffWordWrap, 'inherit'); + let diffOptions: any = { + enableSplitViewResizing: true, + renderSideBySide: true, + maxComputationTime: 5000, + maxFileSize: 50, + ignoreTrimWhitespace: true, + renderIndicators: true, + originalEditable: false, + diffCodeLens: false, + renderOverviewRuler: true, + diffWordWrap: 'inherit' + }; + this._options = validateDiffEditorOptions(options, diffOptions); if (typeof options.isInEmbeddedEditor !== 'undefined') { this._contextKeyService.createKey('isInEmbeddedDiffEditor', options.isInEmbeddedEditor); @@ -299,15 +277,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._contextKeyService.createKey('isInEmbeddedDiffEditor', false); } - this._renderOverviewRuler = true; - if (typeof options.renderOverviewRuler !== 'undefined') { - this._renderOverviewRuler = Boolean(options.renderOverviewRuler); - } - this._updateDecorationsRunner = this._register(new RunOnceScheduler(() => this._updateDecorations(), 0)); this._containerDomElement = document.createElement('div'); - this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._renderSideBySide); + this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._options.renderSideBySide); this._containerDomElement.style.position = 'relative'; this._containerDomElement.style.height = '100%'; this._domElement.appendChild(this._containerDomElement); @@ -325,7 +298,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._register(dom.addStandardDisposableListener(this._overviewDomElement, 'mousedown', (e) => { this._modifiedEditor.delegateVerticalScrollbarMouseDown(e); })); - if (this._renderOverviewRuler) { + if (this._options.renderOverviewRuler) { this._containerDomElement.appendChild(this._overviewDomElement); } @@ -366,28 +339,22 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._originalOverviewRuler = null; this._modifiedOverviewRuler = null; - this._reviewPane = new DiffReview(this); + this._reviewPane = instantiationService.createInstance(DiffReview, this); this._containerDomElement.appendChild(this._reviewPane.domNode.domNode); this._containerDomElement.appendChild(this._reviewPane.shadow.domNode); this._containerDomElement.appendChild(this._reviewPane.actionBarContainer.domNode); - // enableSplitViewResizing - this._enableSplitViewResizing = true; - if (typeof options.enableSplitViewResizing !== 'undefined') { - this._enableSplitViewResizing = options.enableSplitViewResizing; - } - - if (this._renderSideBySide) { - this._setStrategy(new DiffEditorWidgetSideBySide(this._createDataSource(), this._enableSplitViewResizing)); + if (this._options.renderSideBySide) { + this._setStrategy(new DiffEditorWidgetSideBySide(this._createDataSource(), this._options.enableSplitViewResizing)); } else { - this._setStrategy(new DiffEditorWidgetInline(this._createDataSource(), this._enableSplitViewResizing)); + this._setStrategy(new DiffEditorWidgetInline(this._createDataSource(), this._options.enableSplitViewResizing)); } this._register(themeService.onDidColorThemeChange(t => { if (this._strategy && this._strategy.applyColors(t)) { this._updateDecorationsRunner.schedule(); } - this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._renderSideBySide); + this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._options.renderSideBySide); })); const contributions: IDiffEditorContributionDescription[] = EditorExtensionsRegistry.getDiffEditorContributions(); @@ -403,19 +370,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } public get ignoreTrimWhitespace(): boolean { - return this._ignoreTrimWhitespace; - } - - public get renderSideBySide(): boolean { - return this._renderSideBySide; + return this._options.ignoreTrimWhitespace; } public get maxComputationTime(): number { - return this._maxComputationTime; - } - - public get renderIndicators(): boolean { - return this._renderIndicators; + return this._options.maxComputationTime; } public getContentHeight(): number { @@ -464,7 +423,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _recreateOverviewRulers(): void { - if (!this._renderOverviewRuler) { + if (!this._options.renderOverviewRuler) { return; } @@ -526,6 +485,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } })); + this._register(editor.onDidChangeHiddenAreas(() => { + this._updateDecorationsRunner.cancel(); + this._updateDecorations(); + })); + this._register(editor.onDidChangeModelContent(() => { if (this._isVisible) { this._beginUpdateDecorationsSoon(); @@ -588,6 +552,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } })); + this._register(editor.onDidChangeHiddenAreas(() => { + this._updateDecorationsRunner.cancel(); + this._updateDecorations(); + })); + this._register(editor.onDidChangeModelContent(() => { if (this._isVisible) { this._beginUpdateDecorationsSoon(); @@ -642,7 +611,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._modifiedOverviewRuler.dispose(); } this._overviewDomElement.removeChild(this._overviewViewportDomElement.domNode); - if (this._renderOverviewRuler) { + if (this._options.renderOverviewRuler) { this._containerDomElement.removeChild(this._overviewDomElement); } @@ -695,73 +664,40 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._modifiedEditor; } - public updateOptions(newOptions: Readonly): void { + public updateOptions(_newOptions: Readonly): void { + const newOptions = validateDiffEditorOptions(_newOptions, this._options); + const changed = changedDiffEditorOptions(this._options, newOptions); + this._options = newOptions; - // Handle side by side - let renderSideBySideChanged = false; - if (typeof newOptions.renderSideBySide !== 'undefined') { - if (this._renderSideBySide !== newOptions.renderSideBySide) { - this._renderSideBySide = newOptions.renderSideBySide; - renderSideBySideChanged = true; - } - } - - if (typeof newOptions.maxComputationTime !== 'undefined') { - this._maxComputationTime = newOptions.maxComputationTime; - if (this._isVisible) { - this._beginUpdateDecorationsSoon(); - } - } - - let beginUpdateDecorations = false; - - if (typeof newOptions.ignoreTrimWhitespace !== 'undefined') { - if (this._ignoreTrimWhitespace !== newOptions.ignoreTrimWhitespace) { - this._ignoreTrimWhitespace = newOptions.ignoreTrimWhitespace; - // Begin comparing - beginUpdateDecorations = true; - } - } - - if (typeof newOptions.renderIndicators !== 'undefined') { - if (this._renderIndicators !== newOptions.renderIndicators) { - this._renderIndicators = newOptions.renderIndicators; - beginUpdateDecorations = true; - } - } + const beginUpdateDecorations = (changed.ignoreTrimWhitespace || changed.renderIndicators); + const beginUpdateDecorationsSoon = (this._isVisible && (changed.maxComputationTime || changed.maxFileSize)); if (beginUpdateDecorations) { this._beginUpdateDecorations(); + } else if (beginUpdateDecorationsSoon) { + this._beginUpdateDecorationsSoon(); } - this._originalIsEditable = validateBooleanOption(newOptions.originalEditable, this._originalIsEditable); - this._diffCodeLens = validateBooleanOption(newOptions.diffCodeLens, this._diffCodeLens); - this._diffWordWrap = validateDiffWordWrap(newOptions.diffWordWrap, this._diffWordWrap); - - this._modifiedEditor.updateOptions(this._adjustOptionsForRightHandSide(newOptions)); - this._originalEditor.updateOptions(this._adjustOptionsForLeftHandSide(newOptions)); + this._modifiedEditor.updateOptions(this._adjustOptionsForRightHandSide(_newOptions)); + this._originalEditor.updateOptions(this._adjustOptionsForLeftHandSide(_newOptions)); // enableSplitViewResizing - if (typeof newOptions.enableSplitViewResizing !== 'undefined') { - this._enableSplitViewResizing = newOptions.enableSplitViewResizing; - } - this._strategy.setEnableSplitViewResizing(this._enableSplitViewResizing); + this._strategy.setEnableSplitViewResizing(this._options.enableSplitViewResizing); // renderSideBySide - if (renderSideBySideChanged) { - if (this._renderSideBySide) { - this._setStrategy(new DiffEditorWidgetSideBySide(this._createDataSource(), this._enableSplitViewResizing)); + if (changed.renderSideBySide) { + if (this._options.renderSideBySide) { + this._setStrategy(new DiffEditorWidgetSideBySide(this._createDataSource(), this._options.enableSplitViewResizing)); } else { - this._setStrategy(new DiffEditorWidgetInline(this._createDataSource(), this._enableSplitViewResizing)); + this._setStrategy(new DiffEditorWidgetInline(this._createDataSource(), this._options.enableSplitViewResizing)); } // Update class name - this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._renderSideBySide); + this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._options.renderSideBySide); } // renderOverviewRuler - if (typeof newOptions.renderOverviewRuler !== 'undefined' && this._renderOverviewRuler !== newOptions.renderOverviewRuler) { - this._renderOverviewRuler = newOptions.renderOverviewRuler; - if (this._renderOverviewRuler) { + if (changed.renderOverviewRuler) { + if (this._options.renderOverviewRuler) { this._containerDomElement.appendChild(this._overviewDomElement); } else { this._containerDomElement.removeChild(this._overviewDomElement); @@ -996,7 +932,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _layoutOverviewRulers(): void { - if (!this._renderOverviewRuler) { + if (!this._options.renderOverviewRuler) { return; } @@ -1068,9 +1004,14 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE // yet supported, so using tokens for now. this._diffComputationToken++; const currentToken = this._diffComputationToken; - this._setState(editorBrowser.DiffEditorState.ComputingDiff); - if (!this._editorWorkerService.canComputeDiff(currentOriginalModel.uri, currentModifiedModel.uri)) { + const diffLimit = this._options.maxFileSize * 1024 * 1024; // MB + const canSyncModelForDiff = (model: ITextModel): boolean => { + const bufferTextLength = model.getValueLength(); + return (diffLimit === 0 || bufferTextLength <= diffLimit); + }; + + if (!canSyncModelForDiff(currentOriginalModel) || !canSyncModelForDiff(currentModifiedModel)) { if ( !DiffEditorWidget._equals(currentOriginalModel.uri, this._lastOriginalWarning) || !DiffEditorWidget._equals(currentModifiedModel.uri, this._lastModifiedWarning) @@ -1082,7 +1023,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return; } - this._editorWorkerService.computeDiff(currentOriginalModel.uri, currentModifiedModel.uri, this._ignoreTrimWhitespace, this._maxComputationTime).then((result) => { + this._setState(editorBrowser.DiffEditorState.ComputingDiff); + this._editorWorkerService.computeDiff(currentOriginalModel.uri, currentModifiedModel.uri, this._options.ignoreTrimWhitespace, this._options.maxComputationTime).then((result) => { if (currentToken === this._diffComputationToken && currentOriginalModel === this._originalEditor.getModel() && currentModifiedModel === this._modifiedEditor.getModel() @@ -1120,7 +1062,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE const foreignModified = this._modifiedEditorState.getForeignViewZones(this._modifiedEditor.getWhitespaces()); // {{SQL CARBON EDIT}} - const diffDecorations = this._strategy.getEditorsDiffDecorations(lineChanges, this._ignoreTrimWhitespace, this._renderIndicators, foreignOriginal, foreignModified, this._originalEditor, this._modifiedEditor, this._options.reverse); + const diffDecorations = this._strategy.getEditorsDiffDecorations(lineChanges, this._options.ignoreTrimWhitespace, this._options.renderIndicators, foreignOriginal, foreignModified, this._originalEditor, this._modifiedEditor, this._options.reverse); try { this._currentlyChangingViewZones = true; @@ -1139,7 +1081,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE clonedOptions.scrollbar = { ...(clonedOptions.scrollbar || {}) }; clonedOptions.scrollbar.vertical = 'visible'; clonedOptions.folding = false; - clonedOptions.codeLens = this._diffCodeLens; + clonedOptions.codeLens = this._options.diffCodeLens; clonedOptions.fixedOverflowWidgets = true; // clonedOptions.lineDecorationsWidth = '2ch'; // Clone minimap options before changing them @@ -1150,16 +1092,16 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _adjustOptionsForLeftHandSide(options: Readonly): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); - if (!this._renderSideBySide) { + if (!this._options.renderSideBySide) { // never wrap hidden editor result.wordWrapOverride1 = 'off'; } else { - result.wordWrapOverride1 = this._diffWordWrap; + result.wordWrapOverride1 = this._options.diffWordWrap; } if (options.originalAriaLabel) { result.ariaLabel = options.originalAriaLabel; } - result.readOnly = !this._originalIsEditable; + result.readOnly = !this._options.originalEditable; result.extraEditorClassName = 'original-in-monaco-diff-editor'; return { ...result, @@ -1176,7 +1118,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE result.ariaLabel = options.modifiedAriaLabel; } - result.wordWrapOverride1 = this._diffWordWrap; + result.wordWrapOverride1 = this._options.diffWordWrap; result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; result.scrollbar!.verticalHasArrows = false; result.extraEditorClassName = 'modified-in-monaco-diff-editor'; @@ -1215,7 +1157,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._overviewViewportDomElement.setHeight(30); this._originalEditor.layout({ width: splitPoint, height: (height - reviewHeight) }); - this._modifiedEditor.layout({ width: width - splitPoint - (this._renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0), height: (height - reviewHeight) }); + this._modifiedEditor.layout({ width: width - splitPoint - (this._options.renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0), height: (height - reviewHeight) }); if (this._originalOverviewRuler || this._modifiedOverviewRuler) { this._layoutOverviewRulers(); @@ -1271,7 +1213,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE getOptions: () => { return { - renderOverviewRuler: this._renderOverviewRuler + renderOverviewRuler: this._options.renderOverviewRuler }; }, @@ -1434,7 +1376,7 @@ abstract class DiffEditorWidgetStyle extends Disposable { return hasChanges; } - // {{SQL CARBON EDIT}} + // {{SQL CARBON EDIT}} - add reverse parameter public getEditorsDiffDecorations(lineChanges: editorCommon.ILineChange[], ignoreTrimWhitespace: boolean, renderIndicators: boolean, originalWhitespaces: IEditorWhitespace[], modifiedWhitespaces: IEditorWhitespace[], originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor, reverse?: boolean): IEditorsDiffDecorationsWithZones { // Get view zones modifiedWhitespaces = modifiedWhitespaces.sort((a, b) => { @@ -2562,6 +2504,37 @@ function getViewRange(model: ITextModel, viewModel: IViewModel, startLineNumber: )); } +function validateDiffEditorOptions(options: Readonly, defaults: ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions { + let outOptions: any = { + enableSplitViewResizing: validateBooleanOption(options.enableSplitViewResizing, defaults.enableSplitViewResizing), + renderSideBySide: validateBooleanOption(options.renderSideBySide, defaults.renderSideBySide), + maxComputationTime: clampedInt(options.maxComputationTime, defaults.maxComputationTime, 0, Constants.MAX_SAFE_SMALL_INTEGER), + maxFileSize: clampedInt(options.maxFileSize, defaults.maxFileSize, 0, Constants.MAX_SAFE_SMALL_INTEGER), + ignoreTrimWhitespace: validateBooleanOption(options.ignoreTrimWhitespace, defaults.ignoreTrimWhitespace), + renderIndicators: validateBooleanOption(options.renderIndicators, defaults.renderIndicators), + originalEditable: validateBooleanOption(options.originalEditable, defaults.originalEditable), + diffCodeLens: validateBooleanOption(options.diffCodeLens, defaults.diffCodeLens), + renderOverviewRuler: validateBooleanOption(options.renderOverviewRuler, defaults.renderOverviewRuler), + diffWordWrap: validateDiffWordWrap(options.diffWordWrap, defaults.diffWordWrap), + }; + return outOptions; +} + +function changedDiffEditorOptions(a: ValidDiffEditorBaseOptions, b: ValidDiffEditorBaseOptions) { + return { + enableSplitViewResizing: (a.enableSplitViewResizing !== b.enableSplitViewResizing), + renderSideBySide: (a.renderSideBySide !== b.renderSideBySide), + maxComputationTime: (a.maxComputationTime !== b.maxComputationTime), + maxFileSize: (a.maxFileSize !== b.maxFileSize), + ignoreTrimWhitespace: (a.ignoreTrimWhitespace !== b.ignoreTrimWhitespace), + renderIndicators: (a.renderIndicators !== b.renderIndicators), + originalEditable: (a.originalEditable !== b.originalEditable), + diffCodeLens: (a.diffCodeLens !== b.diffCodeLens), + renderOverviewRuler: (a.renderOverviewRuler !== b.renderOverviewRuler), + diffWordWrap: (a.diffWordWrap !== b.diffWordWrap), + }; +} + registerThemingParticipant((theme, collector) => { const added = theme.getColor(diffInserted); if (added) { diff --git a/src/vs/editor/browser/widget/diffNavigator.ts b/src/vs/editor/browser/widget/diffNavigator.ts index c427d3e502..ca546412bc 100644 --- a/src/vs/editor/browser/widget/diffNavigator.ts +++ b/src/vs/editor/browser/widget/diffNavigator.ts @@ -132,24 +132,25 @@ export class DiffNavigator extends Disposable implements IDiffNavigator { }); } else { - this.ranges.push({ - rhs: true, - range: new Range(lineChange.modifiedStartLineNumber, 1, lineChange.modifiedStartLineNumber, 1) - }); + if (lineChange.modifiedEndLineNumber === 0) { + // a deletion + this.ranges.push({ + rhs: true, + range: new Range(lineChange.modifiedStartLineNumber, 1, lineChange.modifiedStartLineNumber + 1, 1) + }); + } else { + // an insertion or modification + this.ranges.push({ + rhs: true, + range: new Range(lineChange.modifiedStartLineNumber, 1, lineChange.modifiedEndLineNumber + 1, 1) + }); + } } }); } // sort - this.ranges.sort((left, right) => { - if (left.range.getStartPosition().isBeforeOrEqual(right.range.getStartPosition())) { - return -1; - } else if (right.range.getStartPosition().isBeforeOrEqual(left.range.getStartPosition())) { - return 1; - } else { - return 0; - } - }); + this.ranges.sort((left, right) => Range.compareRangesUsingStarts(left.range, right.range)); this._onDidUpdate.fire(this); } @@ -203,7 +204,7 @@ export class DiffNavigator extends Disposable implements IDiffNavigator { try { let pos = info.range.getStartPosition(); this._editor.setPosition(pos); - this._editor.revealPositionInCenter(pos, scrollType); + this._editor.revealRangeInCenter(info.range, scrollType); } finally { this.ignoreSelectionChange = false; } diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 56230f8b9c..e838b03586 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -32,6 +32,8 @@ import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/ import { Constants } from 'vs/base/common/uint'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { ILanguageIdCodec } from 'vs/editor/common/modes'; +import { IModeService } from 'vs/editor/common/services/modeService'; const DIFF_LINES_PADDING = 3; @@ -92,7 +94,10 @@ export class DiffReview extends Disposable { private _diffs: Diff[]; private _currentDiff: Diff | null; - constructor(diffEditor: DiffEditorWidget) { + constructor( + diffEditor: DiffEditorWidget, + @IModeService private readonly _modeService: IModeService + ) { super(); this._diffEditor = diffEditor; this._isVisible = false; @@ -624,7 +629,7 @@ export class DiffReview extends Disposable { let modLine = minModifiedLine; for (let i = 0, len = diffs.length; i < len; i++) { const diffEntry = diffs[i]; - DiffReview._renderSection(container, diffEntry, modLine, lineHeight, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts); + DiffReview._renderSection(container, diffEntry, modLine, lineHeight, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts, this._modeService.languageIdCodec); if (diffEntry.modifiedLineStart !== 0) { modLine = diffEntry.modifiedLineEnd; } @@ -638,7 +643,8 @@ export class DiffReview extends Disposable { private static _renderSection( dest: HTMLElement, diffEntry: DiffEntry, modLine: number, lineHeight: number, width: number, originalOptions: IComputedEditorOptions, originalModel: ITextModel, originalModelOpts: TextModelResolvedOptions, - modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions + modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions, + languageIdCodec: ILanguageIdCodec ): void { const type = diffEntry.getType(); @@ -732,14 +738,14 @@ export class DiffReview extends Disposable { let lineContent: string; if (modifiedLine !== 0) { - let html: string | TrustedHTML = this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine); + let html: string | TrustedHTML = this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine, languageIdCodec); if (DiffReview._ttPolicy) { html = DiffReview._ttPolicy.createHTML(html as string); } cell.insertAdjacentHTML('beforeend', html as string); lineContent = modifiedModel.getLineContent(modifiedLine); } else { - let html: string | TrustedHTML = this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine); + let html: string | TrustedHTML = this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine, languageIdCodec); if (DiffReview._ttPolicy) { html = DiffReview._ttPolicy.createHTML(html as string); } @@ -773,10 +779,10 @@ export class DiffReview extends Disposable { } } - private static _renderLine(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number): string { + private static _renderLine(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number, languageIdCodec: ILanguageIdCodec): string { const lineContent = model.getLineContent(lineNumber); const fontInfo = options.get(EditorOption.fontInfo); - const lineTokens = LineTokens.createEmpty(lineContent); + const lineTokens = LineTokens.createEmpty(lineContent, languageIdCodec); 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/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 317ec06140..2e9d096873 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as objects from 'vs/base/common/objects'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; @@ -70,7 +70,7 @@ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { constructor( domElement: HTMLElement, - options: IDiffEditorOptions, + options: Readonly, parentEditor: ICodeEditor, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IContextKeyService contextKeyService: IContextKeyService, diff --git a/src/vs/editor/browser/widget/inlineDiffMargin.ts b/src/vs/editor/browser/widget/inlineDiffMargin.ts index 64db84deda..1d3c5d5961 100644 --- a/src/vs/editor/browser/widget/inlineDiffMargin.ts +++ b/src/vs/editor/browser/widget/inlineDiffMargin.ts @@ -14,7 +14,7 @@ import { Range } from 'vs/editor/common/core/range'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Codicon } from 'vs/base/common/codicons'; -import { ITextModel } from 'vs/editor/common/model'; +import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model'; export interface IDiffLinesChange { readonly originalStartLineNumber: number; @@ -71,13 +71,18 @@ export class InlineDiffMargin extends Disposable { this._marginDomNode.appendChild(this._diffActions); const actions: Action[] = []; + const isDeletion = diff.modifiedEndLineNumber === 0; // default action actions.push(new Action( 'diff.clipboard.copyDeletedContent', - diff.originalEndLineNumber > diff.modifiedStartLineNumber - ? nls.localize('diff.clipboard.copyDeletedLinesContent.label', "Copy deleted lines") - : nls.localize('diff.clipboard.copyDeletedLinesContent.single.label', "Copy deleted line"), + isDeletion + ? (diff.originalEndLineNumber > diff.modifiedStartLineNumber + ? nls.localize('diff.clipboard.copyDeletedLinesContent.label', "Copy deleted lines") + : nls.localize('diff.clipboard.copyDeletedLinesContent.single.label', "Copy deleted line")) + : (diff.originalEndLineNumber > diff.modifiedStartLineNumber + ? nls.localize('diff.clipboard.copyChangedLinesContent.label', "Copy changed lines") + : nls.localize('diff.clipboard.copyChangedLinesContent.single.label', "Copy changed line")), undefined, true, async () => { @@ -92,12 +97,20 @@ export class InlineDiffMargin extends Disposable { if (diff.originalEndLineNumber > diff.modifiedStartLineNumber) { copyLineAction = new Action( 'diff.clipboard.copyDeletedLineContent', - nls.localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", diff.originalStartLineNumber), + isDeletion + ? nls.localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", diff.originalStartLineNumber) + : nls.localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", diff.originalStartLineNumber), undefined, true, async () => { const lineContent = diff.originalModel.getLineContent(diff.originalStartLineNumber + currentLineNumberOffset); - await this._clipboardService.writeText(lineContent); + if (lineContent === '') { + // empty line + const eof = diff.originalModel.getEndOfLineSequence(); + await this._clipboardService.writeText(eof === EndOfLineSequence.LF ? '\n' : '\r\n'); + } else { + await this._clipboardService.writeText(lineContent); + } } ); @@ -141,7 +154,10 @@ export class InlineDiffMargin extends Disposable { }, getActions: () => { if (copyLineAction) { - copyLineAction.label = nls.localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", diff.originalStartLineNumber + currentLineNumberOffset); + copyLineAction.label = + isDeletion + ? nls.localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", diff.originalStartLineNumber + currentLineNumberOffset) + : nls.localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", diff.originalStartLineNumber + currentLineNumberOffset); } return actions; }, diff --git a/src/vs/editor/common/commands/replaceCommand.ts b/src/vs/editor/common/commands/replaceCommand.ts index 26d159892d..c666b48eeb 100644 --- a/src/vs/editor/common/commands/replaceCommand.ts +++ b/src/vs/editor/common/commands/replaceCommand.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; +import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; @@ -27,12 +27,7 @@ export class ReplaceCommand implements ICommand { public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { let inverseEditOperations = helper.getInverseEditOperations(); let srcRange = inverseEditOperations[0].range; - return new Selection( - srcRange.endLineNumber, - srcRange.endColumn, - srcRange.endLineNumber, - srcRange.endColumn - ); + return Selection.fromPositions(srcRange.getEndPosition()); } } @@ -53,7 +48,7 @@ export class ReplaceCommandThatSelectsText implements ICommand { public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { const inverseEditOperations = helper.getInverseEditOperations(); const srcRange = inverseEditOperations[0].range; - return new Selection(srcRange.startLineNumber, srcRange.startColumn, srcRange.endLineNumber, srcRange.endColumn); + return Selection.fromRange(srcRange, SelectionDirection.LTR); } } @@ -76,12 +71,7 @@ export class ReplaceCommandWithoutChangingPosition implements ICommand { public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { let inverseEditOperations = helper.getInverseEditOperations(); let srcRange = inverseEditOperations[0].range; - return new Selection( - srcRange.startLineNumber, - srcRange.startColumn, - srcRange.startLineNumber, - srcRange.startColumn - ); + return Selection.fromPositions(srcRange.getStartPosition()); } } @@ -108,12 +98,7 @@ export class ReplaceCommandWithOffsetCursorState implements ICommand { public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { let inverseEditOperations = helper.getInverseEditOperations(); let srcRange = inverseEditOperations[0].range; - return new Selection( - srcRange.endLineNumber + this._lineNumberDeltaOffset, - srcRange.endColumn + this._columnDeltaOffset, - srcRange.endLineNumber + this._lineNumberDeltaOffset, - srcRange.endColumn + this._columnDeltaOffset - ); + return Selection.fromPositions(srcRange.getEndPosition().delta(this._lineNumberDeltaOffset, this._columnDeltaOffset)); } } diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 979943a753..c2a2332204 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -271,6 +271,21 @@ function migrateOptions(options: IEditorOptions): void { } else if (matchBrackets === false) { options.matchBrackets = 'never'; } + + const { renderIndentGuides, highlightActiveIndentGuide } = options as any as { + renderIndentGuides: boolean; + highlightActiveIndentGuide: boolean; + }; + if (!options.guides) { + options.guides = {}; + } + + if (renderIndentGuides !== undefined) { + options.guides.indentation = !!renderIndentGuides; + } + if (highlightActiveIndentGuide !== undefined) { + options.guides.highlightActiveIndentation = !!highlightActiveIndentGuide; + } } function deepCloneAndMigrateOptions(_options: Readonly): IEditorOptions { @@ -532,11 +547,52 @@ const editorConfiguration: IConfigurationNode = { default: 20_000, description: nls.localize('maxTokenizationLineLength', "Lines above this length will not be tokenized for performance reasons") }, + 'editor.language.brackets': { + type: 'array', + default: false, // We want to distinguish the empty array from not configured. + description: nls.localize('schema.brackets', 'Defines the bracket symbols that increase or decrease the indentation.'), + items: { + type: 'array', + items: [ + { + type: 'string', + description: nls.localize('schema.openBracket', 'The opening bracket character or string sequence.') + }, + { + type: 'string', + description: nls.localize('schema.closeBracket', 'The closing bracket character or string sequence.') + } + ] + } + }, + 'editor.language.colorizedBracketPairs': { + type: 'array', + default: false, // We want to distinguish the empty array from not configured. + description: nls.localize('schema.colorizedBracketPairs', 'Defines the bracket pairs that are colorized by their nesting level if bracket pair colorization is enabled.'), + items: { + type: 'array', + items: [ + { + type: 'string', + description: nls.localize('schema.openBracket', 'The opening bracket character or string sequence.') + }, + { + type: 'string', + description: nls.localize('schema.closeBracket', 'The closing bracket character or string sequence.') + } + ] + } + }, 'diffEditor.maxComputationTime': { type: 'number', default: 5000, description: nls.localize('maxComputationTime', "Timeout in milliseconds after which diff computation is cancelled. Use 0 for no timeout.") }, + 'diffEditor.maxFileSize': { + type: 'number', + default: 50, + description: nls.localize('maxFileSize', "Maximum file size in MB for which to compute diffs. Use 0 for no limit.") + }, 'diffEditor.renderSideBySide': { type: 'boolean', default: true, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 3d033c0539..e7688dae6b 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -12,7 +12,6 @@ import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import product from 'vs/platform/product/common/product'; //#region typed options @@ -579,16 +578,6 @@ export interface IEditorOptions { * Defaults to false. */ renderControlCharacters?: boolean; - /** - * Enable rendering of indent guides. - * Defaults to true. - */ - renderIndentGuides?: boolean; - /** - * Enable highlighting of the active indent guide. - * Defaults to true. - */ - highlightActiveIndentGuide?: boolean; /** * Enable rendering of current line highlight. * Defaults to all. @@ -649,6 +638,10 @@ export interface IEditorOptions { * Control if the editor should use shadow DOM. */ useShadowDOM?: boolean; + /** + * Controls the behavior of editor guides. + */ + guides?: IGuidesOptions; } /** @@ -657,10 +650,7 @@ export interface IEditorOptions { */ export const MINIMAP_GUTTER_WIDTH = 8; -/** - * Configuration options for the diff editor. - */ -export interface IDiffEditorOptions extends IEditorOptions { +export interface IDiffEditorBaseOptions { /** * Allow the user to resize the diff editor split view. * Defaults to true. @@ -676,6 +666,11 @@ export interface IDiffEditorOptions extends IEditorOptions { * Defaults to 5000. */ maxComputationTime?: number; + /** + * Maximum supported file size in MB. + * Defaults to 50. + */ + maxFileSize?: number; /** * Compute the diff by ignoring leading/trailing whitespace * Defaults to true. @@ -701,11 +696,6 @@ export interface IDiffEditorOptions extends IEditorOptions { * Defaults to false. */ diffCodeLens?: boolean; - /** - * Is the diff editor inside another editor - * Defaults to false - */ - isInEmbeddedEditor?: boolean; /** * Is the diff editor should render overview ruler * Defaults to true @@ -715,16 +705,19 @@ export interface IDiffEditorOptions extends IEditorOptions { * Control the wrapping of the diff editor. */ diffWordWrap?: 'off' | 'on' | 'inherit'; - /** - * Aria label for original editor. - */ - originalAriaLabel?: string; - /** - * Aria label for modified editor. - */ - modifiedAriaLabel?: string; } +/** + * Configuration options for the diff editor. + */ +export interface IDiffEditorOptions extends IEditorOptions, IDiffEditorBaseOptions { +} + +/** + * @internal + */ +export type ValidDiffEditorBaseOptions = Readonly>; + //#endregion /** @@ -927,19 +920,26 @@ class EditorBooleanOption extends SimpleEditorOption(value: any, defaultValue: T, minimum: number, maximum: number): number | T { + if (typeof value === 'undefined') { + return defaultValue; + } + let r = parseInt(value, 10); + if (isNaN(r)) { + return defaultValue; + } + r = Math.max(minimum, r); + r = Math.min(maximum, r); + return r | 0; +} + class EditorIntOption extends SimpleEditorOption { public static clampedInt(value: any, defaultValue: T, minimum: number, maximum: number): number | T { - if (typeof value === 'undefined') { - return defaultValue; - } - let r = parseInt(value, 10); - if (isNaN(r)) { - return defaultValue; - } - r = Math.max(minimum, r); - r = Math.min(maximum, r); - return r | 0; + return clampedInt(value, defaultValue, minimum, maximum); } public readonly minimum: number; @@ -1157,6 +1157,9 @@ export interface IEditorCommentsOptions { ignoreEmptyLines?: boolean; } +/** + * @internal + */ export type EditorCommentsOptions = Readonly>; class EditorComments extends BaseEditorOption { @@ -1387,6 +1390,9 @@ export interface IEditorFindOptions { loop?: boolean; } +/** + * @internal + */ export type EditorFindOptions = Readonly>; class EditorFind extends BaseEditorOption { @@ -1639,6 +1645,9 @@ export interface IGotoLocationOptions { alternativeReferenceCommand?: string; } +/** + * @internal + */ export type GoToLocationOptions = Readonly>; class EditorGoToLocation extends BaseEditorOption { @@ -1772,8 +1781,16 @@ export interface IEditorHoverOptions { * Defaults to true. */ sticky?: boolean; + /** + * Should the hover be shown above the line if possible? + * Defaults to false. + */ + above?: boolean; } +/** + * @internal + */ export type EditorHoverOptions = Readonly>; class EditorHover extends BaseEditorOption { @@ -1782,7 +1799,8 @@ class EditorHover extends BaseEditorOption>; class EditorLightbulb extends BaseEditorOption { @@ -2451,6 +2478,9 @@ export interface IEditorInlayHintsOptions { fontFamily?: string; } +/** + * @internal + */ export type EditorInlayHintsOptions = Readonly>; class EditorInlayHints extends BaseEditorOption { @@ -2468,12 +2498,12 @@ class EditorInlayHints extends BaseEditorOption>; class EditorMinimap extends BaseEditorOption { @@ -2672,10 +2705,10 @@ export interface IEditorPaddingOptions { bottom?: number; } -export interface InternalEditorPaddingOptions { - readonly top: number; - readonly bottom: number; -} +/** + * @internal + */ +export type InternalEditorPaddingOptions = Readonly>; class EditorPadding extends BaseEditorOption { @@ -2733,6 +2766,9 @@ export interface IEditorParameterHintOptions { cycle?: boolean; } +/** + * @internal + */ export type InternalParameterHintOptions = Readonly>; class EditorParameterHints extends BaseEditorOption { @@ -2799,6 +2835,9 @@ export interface IQuickSuggestionsOptions { strings?: boolean; } +/** + * @internal + */ export type ValidQuickSuggestionsOptions = boolean | Readonly>; class EditorQuickSuggestions extends BaseEditorOption { @@ -3231,6 +3270,9 @@ export interface IInlineSuggestOptions { mode?: 'prefix' | 'subword' | 'subwordSmart'; } +/** + * @internal + */ export type InternalInlineSuggestOptions = Readonly>; /** @@ -3250,18 +3292,7 @@ class InlineEditorSuggest extends BaseEditorOption>; /** @@ -3306,7 +3340,7 @@ class BracketPairColorization extends BaseEditorOption>; + +/** + * Configuration options for inline suggestions + */ +class GuideOptions extends BaseEditorOption { + constructor() { + const defaults: InternalGuidesOptions = { + bracketPairs: false, + bracketPairsHorizontal: 'active', + highlightActiveBracketPair: true, + + indentation: true, + highlightActiveIndentation: true + }; + + super( + EditorOption.guides, 'guides', defaults, + { + 'editor.guides.bracketPairs': { + type: ['boolean', 'string'], + enum: [true, 'active', false], + enumDescriptions: [ + nls.localize('editor.guides.bracketPairs.true', "Enables bracket pair guides."), + nls.localize('editor.guides.bracketPairs.active', "Enables bracket pair guides only for the active bracket pair."), + nls.localize('editor.guides.bracketPairs.false', "Disables bracket pair guides."), + ], + default: defaults.bracketPairs, + description: nls.localize('editor.guides.bracketPairs', "Controls whether bracket pair guides are enabled or not.") + }, + 'editor.guides.bracketPairsHorizontal': { + type: ['boolean', 'string'], + enum: [true, 'active', false], + enumDescriptions: [ + nls.localize('editor.guides.bracketPairsHorizontal.true', "Enables horizontal guides as addition to vertical bracket pair guides."), + nls.localize('editor.guides.bracketPairsHorizontal.active', "Enables horizontal guides only for the active bracket pair."), + nls.localize('editor.guides.bracketPairsHorizontal.false', "Disables horizontal bracket pair guides."), + ], + default: defaults.bracketPairsHorizontal, + description: nls.localize('editor.guides.bracketPairsHorizontal', "Controls whether horizontal bracket pair guides are enabled or not.") + }, + 'editor.guides.highlightActiveBracketPair': { + type: 'boolean', + default: defaults.highlightActiveBracketPair, + description: nls.localize('editor.guides.highlightActiveBracketPair', "Controls whether bracket pair guides are enabled or not.") + }, + 'editor.guides.indentation': { + type: 'boolean', + default: defaults.indentation, + description: nls.localize('editor.guides.indentation', "Controls whether the editor should render indent guides.") + }, + 'editor.guides.highlightActiveIndentation': { + type: 'boolean', + default: defaults.highlightActiveIndentation, + description: nls.localize('editor.guides.highlightActiveIndentation', "Controls whether the editor should highlight the active indent guide.") + } + } + ); + } + + public validate(_input: any): InternalGuidesOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IGuidesOptions; + return { + bracketPairs: primitiveSet(input.bracketPairs, this.defaultValue.bracketPairs, [true, false, 'active']), + bracketPairsHorizontal: primitiveSet(input.bracketPairsHorizontal, this.defaultValue.bracketPairsHorizontal, [true, false, 'active']), + highlightActiveBracketPair: boolean(input.highlightActiveBracketPair, this.defaultValue.highlightActiveBracketPair), + + indentation: boolean(input.indentation, this.defaultValue.indentation), + highlightActiveIndentation: boolean(input.highlightActiveIndentation, this.defaultValue.highlightActiveIndentation), + }; + } +} + +function primitiveSet(value: unknown, defaultValue: T, allowedValues: T[]): T { + const idx = allowedValues.indexOf(value as any); + if (idx === -1) { + return defaultValue; + } + return allowedValues[idx]; +} + +//#endregion + //#region suggest /** @@ -3485,6 +3642,9 @@ export interface ISuggestOptions { showSnippets?: boolean; } +/** + * @internal + */ export type InternalSuggestOptions = Readonly>; class EditorSuggest extends BaseEditorOption { @@ -3578,17 +3738,6 @@ class EditorSuggest extends BaseEditorOption>; class SmartSelect extends BaseEditorOption { @@ -3950,7 +4102,7 @@ export const EDITOR_MODEL_DEFAULTS = { detectIndentation: true, trimAutoWhitespace: true, largeFileOptimizations: true, - bracketPairColorizationOptions: { enabled: product.quality !== 'stable' } + bracketPairColorizationOptions: { enabled: false } }; /** @@ -3977,6 +4129,7 @@ export const enum EditorOption { automaticLayout, autoSurround, bracketPairColorization, + guides, codeLens, codeLensFontFamily, codeLensFontSize, @@ -4015,7 +4168,6 @@ export const enum EditorOption { glyphMargin, gotoLocation, hideCursorInOverviewRuler, - highlightActiveIndentGuide, hover, inDiffEditor, inlineSuggest, @@ -4047,7 +4199,6 @@ export const enum EditorOption { readOnly, renameOnType, renderControlCharacters, - renderIndentGuides, renderFinalNewline, renderLineHighlight, renderLineHighlightOnlyWhenFocus, @@ -4099,21 +4250,6 @@ export const enum EditorOption { wrappingInfo, } -/** - * WORKAROUND: TS emits "any" for complex editor options values (anything except string, bool, enum, etc. ends up being "any") - * @monacodtsreplace - * /accessibilitySupport, any/accessibilitySupport, AccessibilitySupport/ - * /comments, any/comments, EditorCommentsOptions/ - * /find, any/find, EditorFindOptions/ - * /fontInfo, any/fontInfo, FontInfo/ - * /gotoLocation, any/gotoLocation, GoToLocationOptions/ - * /hover, any/hover, EditorHoverOptions/ - * /lightbulb, any/lightbulb, EditorLightbulbOptions/ - * /minimap, any/minimap, EditorMinimapOptions/ - * /parameterHints, any/parameterHints, InternalParameterHintOptions/ - * /quickSuggestions, any/quickSuggestions, ValidQuickSuggestionsOptions/ - * /suggest, any/suggest, InternalSuggestOptions/ - */ export const EditorOptions = { acceptSuggestionOnCommitCharacter: register(new EditorBooleanOption( EditorOption.acceptSuggestionOnCommitCharacter, 'acceptSuggestionOnCommitCharacter', true, @@ -4228,6 +4364,7 @@ export const EditorOptions = { } )), bracketPairColorization: register(new BracketPairColorization()), + bracketPairGuides: register(new GuideOptions()), stickyTabStops: register(new EditorBooleanOption( EditorOption.stickyTabStops, 'stickyTabStops', false, { description: nls.localize('stickyTabStops', "Emulate selection behavior of tab characters when using spaces for indentation. Selection will stick to tab stops.") } @@ -4382,10 +4519,6 @@ export const EditorOptions = { EditorOption.hideCursorInOverviewRuler, 'hideCursorInOverviewRuler', false, { description: nls.localize('hideCursorInOverviewRuler', "Controls whether the cursor should be hidden in the overview ruler.") } )), - highlightActiveIndentGuide: register(new EditorBooleanOption( - EditorOption.highlightActiveIndentGuide, 'highlightActiveIndentGuide', true, - { description: nls.localize('highlightActiveIndentGuide', "Controls whether the editor should highlight the active indent guide.") } - )), hover: register(new EditorHover()), inDiffEditor: register(new EditorBooleanOption( EditorOption.inDiffEditor, 'inDiffEditor', false @@ -4452,7 +4585,7 @@ export const EditorOptions = { '- `ctrlCmd` refers to a value the setting can take and should not be localized.', '- `Control` and `Command` refer to the modifier keys Ctrl or Cmd on the keyboard and can be localized.' ] - }, "The modifier to be used to add multiple cursors with the mouse. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier. [Read more](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).") + }, "The modifier to be used to add multiple cursors with the mouse. The Go to Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier. [Read more](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).") } )), multiCursorPaste: register(new EditorStringEnumOption( @@ -4514,10 +4647,6 @@ export const EditorOptions = { EditorOption.renderControlCharacters, 'renderControlCharacters', false, { description: nls.localize('renderControlCharacters', "Controls whether the editor should render control characters.") } )), - renderIndentGuides: register(new EditorBooleanOption( - EditorOption.renderIndentGuides, 'renderIndentGuides', true, - { description: nls.localize('renderIndentGuides', "Controls whether the editor should render indent guides.") } - )), renderFinalNewline: register(new EditorBooleanOption( EditorOption.renderFinalNewline, 'renderFinalNewline', true, { description: nls.localize('renderFinalNewline', "Render last line number when the file ends with a newline.") } diff --git a/src/vs/editor/common/config/fontInfo.ts b/src/vs/editor/common/config/fontInfo.ts index 4ace44b391..160a98e068 100644 --- a/src/vs/editor/common/config/fontInfo.ts +++ b/src/vs/editor/common/config/fontInfo.ts @@ -122,17 +122,24 @@ export class BareFontInfo { /** * @internal */ - public getMassagedFontFamily(): string { - if (/[,"']/.test(this.fontFamily)) { - // Looks like the font family might be already escaped - return this.fontFamily; - } - if (/[+ ]/.test(this.fontFamily)) { - // Wrap a font family using + or with quotes - return `"${this.fontFamily}"`; + public getMassagedFontFamily(fallbackFontFamily: string | null): string { + const fontFamily = BareFontInfo._wrapInQuotes(this.fontFamily); + if (fallbackFontFamily && this.fontFamily !== fallbackFontFamily) { + return `${fontFamily}, ${fallbackFontFamily}`; } + return fontFamily; + } - return this.fontFamily; + private static _wrapInQuotes(fontFamily: string): string { + if (/[,"']/.test(fontFamily)) { + // Looks like the font family might be already escaped + return fontFamily; + } + if (/[+ ]/.test(fontFamily)) { + // Wrap a font family using + or with quotes + return `"${fontFamily}"`; + } + return fontFamily; } } diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index ed2f5d796c..6a052df3d4 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -331,15 +331,19 @@ export class CursorsController extends Disposable { public onModelContentChanged(eventsCollector: ViewModelEventsCollector, e: ModelRawContentChangedEvent | ModelInjectedTextChangedEvent): void { if (e instanceof ModelInjectedTextChangedEvent) { // If injected texts change, the view positions of all cursors need to be updated. - const selectionsFromMarkers = this._cursors.readSelectionFromMarkers(); - const newState = CursorState.fromModelSelections(selectionsFromMarkers); - - if (didStateChange(this.getCursorStates(), newState || [])) { - // setStates might remove markers, which could trigger a decoration change. - // If there are injected text decorations for that line, `onModelContentChanged` is emitted again - // and an endless recursion happens. - // This is why we only call setStates if we really need to (this fixes recursion). - this.setStates(eventsCollector, 'modelChange', CursorChangeReason.RecoverFromMarkers, newState); + if (this._isHandling) { + // The view positions will be updated when handling finishes + return; + } + // setStates might remove markers, which could trigger a decoration change. + // If there are injected text decorations for that line, `onModelContentChanged` is emitted again + // and an endless recursion happens. + // _isHandling prevents that. + this._isHandling = true; + try { + this.setStates(eventsCollector, 'modelChange', CursorChangeReason.NotSet, this.getCursorStates()); + } finally { + this._isHandling = false; } } else { this._knownModelVersionId = e.versionId; @@ -727,28 +731,6 @@ export class CursorsController extends Disposable { } } -function didStateChange(currentStates: CursorState[], newStates: PartialCursorState[]): boolean { - if (currentStates.length !== newStates.length) { - return true; - } - - for (let i = 0; i < currentStates.length; i++) { - const curState = currentStates[i]; - const newState = newStates[i]; - if (newState.modelState) { - if (!newState.modelState.equals(curState.modelState)) { - return true; - } - } - if (newState.viewState) { - if (!newState.viewState.equals(curState.viewState)) { - return true; - } - } - } - return false; -} - interface IExecContext { readonly model: ITextModel; readonly selectionsBefore: Selection[]; diff --git a/src/vs/editor/common/controller/cursorColumns.ts b/src/vs/editor/common/controller/cursorColumns.ts new file mode 100644 index 0000000000..42a56a02f9 --- /dev/null +++ b/src/vs/editor/common/controller/cursorColumns.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CharCode } from 'vs/base/common/charCode'; +import * as strings from 'vs/base/common/strings'; +import { Constants } from 'vs/base/common/uint'; +import { CursorConfiguration, ICursorSimpleModel } from 'vs/editor/common/controller/cursorCommon'; +import { Position } from 'vs/editor/common/core/position'; + +/** + * Common operations that work and make sense both on the model and on the view model. + */ +export class CursorColumns { + public static visibleColumnFromColumn(lineContent: string, column: number, tabSize: number): number { + const lineContentLength = lineContent.length; + const endOffset = column - 1 < lineContentLength ? column - 1 : lineContentLength; + + let result = 0; + let i = 0; + while (i < endOffset) { + const codePoint = strings.getNextCodePoint(lineContent, endOffset, i); + i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); + + if (codePoint === CharCode.Tab) { + result = CursorColumns.nextRenderTabStop(result, 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); + graphemeBreakType = nextGraphemeBreakType; + } + if (strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint)) { + result = result + 2; + } else { + result = result + 1; + } + } + } + return result; + } + + /** + * Returns an array that maps one based columns to one based visible columns. The entry at position 0 is -1. + */ + public static visibleColumnsByColumns(lineContent: string, tabSize: number): number[] { + const endOffset = lineContent.length; + + let result = new Array(); + 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; + + let result = 0; + let i = 0; + while (i < endOffset) { + const codePoint = strings.getNextCodePoint(lineContent, endOffset, i); + i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); + + if (codePoint === CharCode.Tab) { + result = CursorColumns.nextRenderTabStop(result, tabSize); + } else { + result = result + 1; + } + } + + return result + 1; + } + + public static visibleColumnFromColumn2(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): number { + return this.visibleColumnFromColumn(model.getLineContent(position.lineNumber), position.column, config.tabSize); + } + + public static columnFromVisibleColumn(lineContent: string, visibleColumn: number, tabSize: number): number { + if (visibleColumn <= 0) { + return 1; + } + + const lineLength = lineContent.length; + + let beforeVisibleColumn = 0; + let beforeColumn = 1; + let i = 0; + while (i < lineLength) { + const codePoint = strings.getNextCodePoint(lineContent, lineLength, i); + i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); + + let afterVisibleColumn: number; + if (codePoint === CharCode.Tab) { + afterVisibleColumn = CursorColumns.nextRenderTabStop(beforeVisibleColumn, tabSize); + } else { + let graphemeBreakType = strings.getGraphemeBreakType(codePoint); + while (i < lineLength) { + const nextCodePoint = strings.getNextCodePoint(lineContent, lineLength, i); + const nextGraphemeBreakType = strings.getGraphemeBreakType(nextCodePoint); + if (strings.breakBetweenGraphemeBreakType(graphemeBreakType, nextGraphemeBreakType)) { + break; + } + i += (nextCodePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); + graphemeBreakType = nextGraphemeBreakType; + } + if (strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint)) { + afterVisibleColumn = beforeVisibleColumn + 2; + } else { + afterVisibleColumn = beforeVisibleColumn + 1; + } + } + const afterColumn = i + 1; + + if (afterVisibleColumn >= visibleColumn) { + const beforeDelta = visibleColumn - beforeVisibleColumn; + const afterDelta = afterVisibleColumn - visibleColumn; + if (afterDelta < beforeDelta) { + return afterColumn; + } else { + return beforeColumn; + } + } + + beforeVisibleColumn = afterVisibleColumn; + beforeColumn = afterColumn; + } + + // walked the entire string + return lineLength + 1; + } + + public static columnFromVisibleColumn2(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, visibleColumn: number): number { + let result = this.columnFromVisibleColumn(model.getLineContent(lineNumber), visibleColumn, config.tabSize); + + let minColumn = model.getLineMinColumn(lineNumber); + if (result < minColumn) { + return minColumn; + } + + let maxColumn = model.getLineMaxColumn(lineNumber); + if (result > maxColumn) { + return maxColumn; + } + + return result; + } + + /** + * ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns) + */ + public static nextRenderTabStop(visibleColumn: number, tabSize: number): number { + return visibleColumn + tabSize - visibleColumn % tabSize; + } + + /** + * ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns) + */ + public static nextIndentTabStop(visibleColumn: number, indentSize: number): number { + return visibleColumn + indentSize - visibleColumn % indentSize; + } + + /** + * ATTENTION: This works with 0-based columns (as opposed to the regular 1-based columns) + */ + public static prevRenderTabStop(column: number, tabSize: number): number { + 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 Math.max(0, column - 1 - (column - 1) % indentSize); + } +} diff --git a/src/vs/editor/common/controller/cursorCommon.ts b/src/vs/editor/common/controller/cursorCommon.ts index 56c4b49209..7446fe4b1a 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -3,21 +3,18 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CharCode } from 'vs/base/common/charCode'; import { onUnexpectedError } from 'vs/base/common/errors'; -import * as strings from 'vs/base/common/strings'; -import { EditorAutoClosingStrategy, EditorAutoSurroundStrategy, ConfigurationChangedEvent, EditorAutoClosingEditStrategy, EditorOption, EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; +import { ConfigurationChangedEvent, EditorAutoClosingEditStrategy, EditorAutoClosingStrategy, EditorAutoIndentStrategy, EditorAutoSurroundStrategy, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { ICommand, IConfiguration } from 'vs/editor/common/editorCommon'; import { ITextModel, PositionAffinity, TextModelResolvedOptions } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; import { AutoClosingPairs, IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; -import { Constants } from 'vs/base/common/uint'; +export { CursorColumns } from './cursorColumns'; export interface IColumnSelectData { isReal: boolean; @@ -83,7 +80,7 @@ export class CursorConfiguration { public readonly surroundingPairs: CharacterMap; public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean, bracket: (ch: string) => boolean }; - private readonly _languageIdentifier: LanguageIdentifier; + private readonly _languageId: string; private _electricChars: { [key: string]: boolean; } | null; public static shouldRecreate(e: ConfigurationChangedEvent): boolean { @@ -105,11 +102,11 @@ export class CursorConfiguration { } constructor( - languageIdentifier: LanguageIdentifier, + languageId: string, modelOptions: TextModelResolvedOptions, configuration: IConfiguration ) { - this._languageIdentifier = languageIdentifier; + this._languageId = languageId; const options = configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); @@ -138,13 +135,13 @@ export class CursorConfiguration { this._electricChars = null; this.shouldAutoCloseBefore = { - quote: CursorConfiguration._getShouldAutoClose(languageIdentifier, this.autoClosingQuotes), - bracket: CursorConfiguration._getShouldAutoClose(languageIdentifier, this.autoClosingBrackets) + quote: CursorConfiguration._getShouldAutoClose(languageId, this.autoClosingQuotes), + bracket: CursorConfiguration._getShouldAutoClose(languageId, this.autoClosingBrackets) }; - this.autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageIdentifier.id); + this.autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageId); - let surroundingPairs = CursorConfiguration._getSurroundingPairs(languageIdentifier); + let surroundingPairs = CursorConfiguration._getSurroundingPairs(languageId); if (surroundingPairs) { for (const pair of surroundingPairs) { this.surroundingPairs[pair.open] = pair.close; @@ -155,7 +152,7 @@ export class CursorConfiguration { public get electricChars() { if (!this._electricChars) { this._electricChars = {}; - let electricChars = CursorConfiguration._getElectricCharacters(this._languageIdentifier); + let electricChars = CursorConfiguration._getElectricCharacters(this._languageId); if (electricChars) { for (const char of electricChars) { this._electricChars[char] = true; @@ -169,21 +166,21 @@ export class CursorConfiguration { return TextModel.normalizeIndentation(str, this.indentSize, this.insertSpaces); } - private static _getElectricCharacters(languageIdentifier: LanguageIdentifier): string[] | null { + private static _getElectricCharacters(languageId: string): string[] | null { try { - return LanguageConfigurationRegistry.getElectricCharacters(languageIdentifier.id); + return LanguageConfigurationRegistry.getElectricCharacters(languageId); } catch (e) { onUnexpectedError(e); return null; } } - private static _getShouldAutoClose(languageIdentifier: LanguageIdentifier, autoCloseConfig: EditorAutoClosingStrategy): (ch: string) => boolean { + private static _getShouldAutoClose(languageId: string, autoCloseConfig: EditorAutoClosingStrategy): (ch: string) => boolean { switch (autoCloseConfig) { case 'beforeWhitespace': return autoCloseBeforeWhitespace; case 'languageDefined': - return CursorConfiguration._getLanguageDefinedShouldAutoClose(languageIdentifier); + return CursorConfiguration._getLanguageDefinedShouldAutoClose(languageId); case 'always': return autoCloseAlways; case 'never': @@ -191,9 +188,9 @@ export class CursorConfiguration { } } - private static _getLanguageDefinedShouldAutoClose(languageIdentifier: LanguageIdentifier): (ch: string) => boolean { + private static _getLanguageDefinedShouldAutoClose(languageId: string): (ch: string) => boolean { try { - const autoCloseBeforeSet = LanguageConfigurationRegistry.getAutoCloseBeforeSet(languageIdentifier.id); + const autoCloseBeforeSet = LanguageConfigurationRegistry.getAutoCloseBeforeSet(languageId); return c => autoCloseBeforeSet.indexOf(c) !== -1; } catch (e) { onUnexpectedError(e); @@ -201,9 +198,9 @@ export class CursorConfiguration { } } - private static _getSurroundingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] | null { + private static _getSurroundingPairs(languageId: string): IAutoClosingPair[] | null { try { - return LanguageConfigurationRegistry.getSurroundingPairs(languageIdentifier.id); + return LanguageConfigurationRegistry.getSurroundingPairs(languageId); } catch (e) { onUnexpectedError(e); return null; @@ -290,31 +287,11 @@ export class SingleCursorState { } private static _computeSelection(selectionStart: Range, position: Position): Selection { - let startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number; - if (selectionStart.isEmpty()) { - startLineNumber = selectionStart.startLineNumber; - startColumn = selectionStart.startColumn; - endLineNumber = position.lineNumber; - endColumn = position.column; + if (selectionStart.isEmpty() || !position.isBeforeOrEqual(selectionStart.getStartPosition())) { + return Selection.fromPositions(selectionStart.getStartPosition(), position); } else { - if (position.isBeforeOrEqual(selectionStart.getStartPosition())) { - startLineNumber = selectionStart.endLineNumber; - startColumn = selectionStart.endColumn; - endLineNumber = position.lineNumber; - endColumn = position.column; - } else { - startLineNumber = selectionStart.startLineNumber; - startColumn = selectionStart.startColumn; - endLineNumber = position.lineNumber; - endColumn = position.column; - } + return Selection.fromPositions(selectionStart.getEndPosition(), position); } - return new Selection( - startLineNumber, - startColumn, - endLineNumber, - endColumn - ); } } @@ -368,13 +345,11 @@ export class CursorState { } public static fromModelSelection(modelSelection: ISelection): PartialModelCursorState { - const selectionStartLineNumber = modelSelection.selectionStartLineNumber; - const selectionStartColumn = modelSelection.selectionStartColumn; - const positionLineNumber = modelSelection.positionLineNumber; - const positionColumn = modelSelection.positionColumn; + const selection = Selection.liftSelection(modelSelection); const modelState = new SingleCursorState( - new Range(selectionStartLineNumber, selectionStartColumn, selectionStartLineNumber, selectionStartColumn), 0, - new Position(positionLineNumber, positionColumn), 0 + Range.fromPositions(selection.getSelectionStart()), + 0, + selection.getPosition(), 0 ); return CursorState.fromModelState(modelState); } @@ -423,216 +398,6 @@ export class EditOperationResult { } } -/** - * Common operations that work and make sense both on the model and on the view model. - */ -export class CursorColumns { - - public static visibleColumnFromColumn(lineContent: string, column: number, tabSize: number): number { - const lineContentLength = lineContent.length; - const endOffset = column - 1 < lineContentLength ? column - 1 : lineContentLength; - - let result = 0; - let i = 0; - while (i < endOffset) { - const codePoint = strings.getNextCodePoint(lineContent, endOffset, i); - i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); - - if (codePoint === CharCode.Tab) { - result = CursorColumns.nextRenderTabStop(result, 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); - graphemeBreakType = nextGraphemeBreakType; - } - if (strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint)) { - result = result + 2; - } else { - result = result + 1; - } - } - } - return result; - } - - /** - * Returns an array that maps one based columns to one based visible columns. The entry at position 0 is -1. - */ - public static visibleColumnsByColumns(lineContent: string, tabSize: number): number[] { - const endOffset = lineContent.length; - - let result = new Array(); - 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; - - let result = 0; - let i = 0; - while (i < endOffset) { - const codePoint = strings.getNextCodePoint(lineContent, endOffset, i); - i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); - - if (codePoint === CharCode.Tab) { - result = CursorColumns.nextRenderTabStop(result, tabSize); - } else { - result = result + 1; - } - } - - return result + 1; - } - - public static visibleColumnFromColumn2(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): number { - return this.visibleColumnFromColumn(model.getLineContent(position.lineNumber), position.column, config.tabSize); - } - - public static columnFromVisibleColumn(lineContent: string, visibleColumn: number, tabSize: number): number { - if (visibleColumn <= 0) { - return 1; - } - - const lineLength = lineContent.length; - - let beforeVisibleColumn = 0; - let beforeColumn = 1; - let i = 0; - while (i < lineLength) { - const codePoint = strings.getNextCodePoint(lineContent, lineLength, i); - i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); - - let afterVisibleColumn: number; - if (codePoint === CharCode.Tab) { - afterVisibleColumn = CursorColumns.nextRenderTabStop(beforeVisibleColumn, tabSize); - } else { - let graphemeBreakType = strings.getGraphemeBreakType(codePoint); - while (i < lineLength) { - const nextCodePoint = strings.getNextCodePoint(lineContent, lineLength, i); - const nextGraphemeBreakType = strings.getGraphemeBreakType(nextCodePoint); - if (strings.breakBetweenGraphemeBreakType(graphemeBreakType, nextGraphemeBreakType)) { - break; - } - i += (nextCodePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1); - graphemeBreakType = nextGraphemeBreakType; - } - if (strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint)) { - afterVisibleColumn = beforeVisibleColumn + 2; - } else { - afterVisibleColumn = beforeVisibleColumn + 1; - } - } - const afterColumn = i + 1; - - if (afterVisibleColumn >= visibleColumn) { - const beforeDelta = visibleColumn - beforeVisibleColumn; - const afterDelta = afterVisibleColumn - visibleColumn; - if (afterDelta < beforeDelta) { - return afterColumn; - } else { - return beforeColumn; - } - } - - beforeVisibleColumn = afterVisibleColumn; - beforeColumn = afterColumn; - } - - // walked the entire string - return lineLength + 1; - } - - public static columnFromVisibleColumn2(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, visibleColumn: number): number { - let result = this.columnFromVisibleColumn(model.getLineContent(lineNumber), visibleColumn, config.tabSize); - - let minColumn = model.getLineMinColumn(lineNumber); - if (result < minColumn) { - return minColumn; - } - - let maxColumn = model.getLineMaxColumn(lineNumber); - if (result > maxColumn) { - return maxColumn; - } - - return result; - } - - /** - * ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns) - */ - public static nextRenderTabStop(visibleColumn: number, tabSize: number): number { - return visibleColumn + tabSize - visibleColumn % tabSize; - } - - /** - * ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns) - */ - public static nextIndentTabStop(visibleColumn: number, indentSize: number): number { - return visibleColumn + indentSize - visibleColumn % indentSize; - } - - /** - * ATTENTION: This works with 0-based columns (as opposed to the regular 1-based columns) - */ - public static prevRenderTabStop(column: number, tabSize: number): number { - 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 Math.max(0, column - 1 - (column - 1) % indentSize); - } -} - export function isQuote(ch: string): boolean { return (ch === '\'' || ch === '"' || ch === '`'); } diff --git a/src/vs/editor/common/controller/cursorDeleteOperations.ts b/src/vs/editor/common/controller/cursorDeleteOperations.ts index 892afd6958..45d19500ef 100644 --- a/src/vs/editor/common/controller/cursorDeleteOperations.ts +++ b/src/vs/editor/common/controller/cursorDeleteOperations.ts @@ -212,6 +212,8 @@ export class DeleteOperations { public static cut(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): EditOperationResult { let commands: Array = []; + let lastCutRange: Range | null = null; + selections.sort((a, b) => Position.compare(a.getStartPosition(), b.getEndPosition())); for (let i = 0, len = selections.length; i < len; i++) { const selection = selections[i]; @@ -232,8 +234,8 @@ export class DeleteOperations { startColumn = 1; endLineNumber = position.lineNumber + 1; endColumn = 1; - } else if (position.lineNumber > 1) { - // Cutting the last line & there are more than 1 lines in the model + } else if (position.lineNumber > 1 && lastCutRange?.endLineNumber !== position.lineNumber) { + // Cutting the last line & there are more than 1 lines in the model & a previous cut operation does not touch the current cut operation startLineNumber = position.lineNumber - 1; startColumn = model.getLineMaxColumn(position.lineNumber - 1); endLineNumber = position.lineNumber; @@ -252,6 +254,7 @@ export class DeleteOperations { endLineNumber, endColumn ); + lastCutRange = deleteSelection; if (!deleteSelection.isEmpty()) { commands[i] = new ReplaceCommand(deleteSelection, ''); diff --git a/src/vs/editor/common/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index 841632b8d6..59c4085f72 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -381,14 +381,16 @@ export class CursorMoveCommands { return new CursorState(cursor.modelState, cursor.viewState); } else { + let newViewLineNumber: number; if (viewLineNumber > visibleViewRange.endLineNumber - 1) { - viewLineNumber = visibleViewRange.endLineNumber - 1; + newViewLineNumber = visibleViewRange.endLineNumber - 1; + } else if (viewLineNumber < visibleViewRange.startLineNumber) { + newViewLineNumber = visibleViewRange.startLineNumber; + } else { + newViewLineNumber = viewLineNumber; } - if (viewLineNumber < visibleViewRange.startLineNumber) { - viewLineNumber = visibleViewRange.startLineNumber; - } - const viewColumn = viewModel.getLineFirstNonWhitespaceColumn(viewLineNumber); - return this._moveToViewPosition(viewModel, cursor, inSelectionMode, viewLineNumber, viewColumn); + const position = MoveOperations.vertical(viewModel.cursorConfig, viewModel, viewLineNumber, cursor.viewState.position.column, cursor.viewState.leftoverVisibleColumns, newViewLineNumber, false); + return CursorState.fromViewState(cursor.viewState.move(inSelectionMode, position.lineNumber, position.column, position.leftoverVisibleColumns)); } } diff --git a/src/vs/editor/common/controller/cursorMoveOperations.ts b/src/vs/editor/common/controller/cursorMoveOperations.ts index 3397e3aa70..aa7842c2b5 100644 --- a/src/vs/editor/common/controller/cursorMoveOperations.ts +++ b/src/vs/editor/common/controller/cursorMoveOperations.ts @@ -153,15 +153,24 @@ export class MoveOperations { return cursor.move(inSelectionMode, lineNumber, column, 0); } - public static down(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnLastLine: boolean): CursorPosition { + public static vertical(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, newLineNumber: number, allowMoveOnEdgeLine: boolean): CursorPosition { const currentVisibleColumn = CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize) + leftoverVisibleColumns; const lineCount = model.getLineCount(); + const wasOnFirstPosition = (lineNumber === 1 && column === 1); const wasOnLastPosition = (lineNumber === lineCount && column === model.getLineMaxColumn(lineNumber)); + const wasAtEdgePosition = (newLineNumber < lineNumber ? wasOnFirstPosition : wasOnLastPosition); - lineNumber = lineNumber + count; - if (lineNumber > lineCount) { + lineNumber = newLineNumber; + if (lineNumber < 1) { + lineNumber = 1; + if (allowMoveOnEdgeLine) { + column = model.getLineMinColumn(lineNumber); + } else { + column = Math.min(model.getLineMaxColumn(lineNumber), column); + } + } else if (lineNumber > lineCount) { lineNumber = lineCount; - if (allowMoveOnLastLine) { + if (allowMoveOnEdgeLine) { column = model.getLineMaxColumn(lineNumber); } else { column = Math.min(model.getLineMaxColumn(lineNumber), column); @@ -170,7 +179,7 @@ export class MoveOperations { column = CursorColumns.columnFromVisibleColumn2(config, model, lineNumber, currentVisibleColumn); } - if (wasOnLastPosition) { + if (wasAtEdgePosition) { leftoverVisibleColumns = 0; } else { leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize); @@ -179,6 +188,10 @@ export class MoveOperations { return new CursorPosition(lineNumber, column, leftoverVisibleColumns); } + public static down(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnLastLine: boolean): CursorPosition { + return this.vertical(config, model, lineNumber, column, leftoverVisibleColumns, lineNumber + count, allowMoveOnLastLine); + } + public static moveDown(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, linesCount: number): SingleCursorState { let lineNumber: number, column: number; @@ -212,28 +225,7 @@ export class MoveOperations { } public static up(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnFirstLine: boolean): CursorPosition { - const currentVisibleColumn = CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize) + leftoverVisibleColumns; - const wasOnFirstPosition = (lineNumber === 1 && column === 1); - - lineNumber = lineNumber - count; - if (lineNumber < 1) { - lineNumber = 1; - if (allowMoveOnFirstLine) { - column = model.getLineMinColumn(lineNumber); - } else { - column = Math.min(model.getLineMaxColumn(lineNumber), column); - } - } else { - column = CursorColumns.columnFromVisibleColumn2(config, model, lineNumber, currentVisibleColumn); - } - - if (wasOnFirstPosition) { - leftoverVisibleColumns = 0; - } else { - leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize); - } - - return new CursorPosition(lineNumber, column, leftoverVisibleColumns); + return this.vertical(config, model, lineNumber, column, leftoverVisibleColumns, lineNumber - count, allowMoveOnFirstLine); } public static moveUp(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, linesCount: number): SingleCursorState { diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index f60d2f5d58..f51b208320 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -598,7 +598,7 @@ export class TypeOperations { } // Do not auto-close ' or " after a word character - if (autoClosingPair.open.length === 1 && chIsQuote && autoCloseConfig !== 'always') { + if (autoClosingPair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') { const wordSeparators = getMapForWordSeparators(config.wordSeparators); if (insertOpenCharacter && position.column > 1 && wordSeparators.get(lineText.charCodeAt(position.column - 2)) === WordCharacterClass.Regular) { return null; @@ -739,7 +739,7 @@ export class TypeOperations { if (electricAction.matchOpenBracket) { let endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1; - let match = model.findMatchingBracketUp(electricAction.matchOpenBracket, { + let match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, { lineNumber: position.lineNumber, column: endColumn }); diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index 07c9c93a6b..0f7d0fff81 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -6,7 +6,7 @@ import { CursorContext, CursorState, ICursorSimpleModel, SingleCursorState } from 'vs/editor/common/controller/cursorCommon'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; +import { Selection } from 'vs/editor/common/core/selection'; import { PositionAffinity, TrackedRangeStickiness } from 'vs/editor/common/model'; /** @@ -63,10 +63,7 @@ export class Cursor { public readSelectionFromMarkers(context: CursorContext): Selection { const range = context.model._getTrackedRange(this._selTrackedRange!)!; - if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { - return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); - } - return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); + return Selection.fromRange(range, this.modelState.selection.getDirection()); } public ensureValidState(context: CursorContext): void { diff --git a/src/vs/editor/common/core/lineTokens.ts b/src/vs/editor/common/core/lineTokens.ts index 13f123bd29..6e8383eb17 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, FontStyle, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; +import { ColorId, FontStyle, ILanguageIdCodec, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; export interface IViewLineTokens { equals(other: IViewLineTokens): boolean; @@ -21,6 +21,7 @@ export class LineTokens implements IViewLineTokens { private readonly _tokens: Uint32Array; private readonly _tokensCount: number; private readonly _text: string; + private readonly _languageIdCodec: ILanguageIdCodec; public static defaultTokenMetadata = ( (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) @@ -28,20 +29,21 @@ export class LineTokens implements IViewLineTokens { | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) ) >>> 0; - public static createEmpty(lineContent: string): LineTokens { + public static createEmpty(lineContent: string, decoder: ILanguageIdCodec): LineTokens { const defaultMetadata = LineTokens.defaultTokenMetadata; const tokens = new Uint32Array(2); tokens[0] = lineContent.length; tokens[1] = defaultMetadata; - return new LineTokens(tokens, lineContent); + return new LineTokens(tokens, lineContent, decoder); } - constructor(tokens: Uint32Array, text: string) { + constructor(tokens: Uint32Array, text: string, decoder: ILanguageIdCodec) { this._tokens = tokens; this._tokensCount = (this._tokens.length >>> 1); this._text = text; + this._languageIdCodec = decoder; } public equals(other: IViewLineTokens): boolean { @@ -88,9 +90,10 @@ export class LineTokens implements IViewLineTokens { return metadata; } - public getLanguageId(tokenIndex: number): LanguageId { + public getLanguageId(tokenIndex: number): string { const metadata = this._tokens[(tokenIndex << 1) + 1]; - return TokenMetadata.getLanguageId(metadata); + const languageId = TokenMetadata.getLanguageId(metadata); + return this._languageIdCodec.decodeLanguageId(languageId); } public getStandardTokenType(tokenIndex: number): StandardTokenType { @@ -212,7 +215,7 @@ export class LineTokens implements IViewLineTokens { } } - return new LineTokens(new Uint32Array(newTokens), text); + return new LineTokens(new Uint32Array(newTokens), text, this._languageIdCodec); } } diff --git a/src/vs/editor/common/core/rgba.ts b/src/vs/editor/common/core/rgba.ts index e7ac408a35..6fb2eb4976 100644 --- a/src/vs/editor/common/core/rgba.ts +++ b/src/vs/editor/common/core/rgba.ts @@ -45,7 +45,7 @@ export class RGBA8 { ); } - private static _clamp(c: number): number { + public static _clamp(c: number): number { if (c < 0) { return 0; } diff --git a/src/vs/editor/common/core/selection.ts b/src/vs/editor/common/core/selection.ts index a0cea7fe6e..7e95d68e14 100644 --- a/src/vs/editor/common/core/selection.ts +++ b/src/vs/editor/common/core/selection.ts @@ -128,6 +128,13 @@ export class Selection extends Range { return new Position(this.positionLineNumber, this.positionColumn); } + /** + * Get the position at the start of the selection. + */ + public getSelectionStart(): Position { + return new Position(this.selectionStartLineNumber, this.selectionStartColumn); + } + /** * Create a new selection with a different `selectionStartLineNumber` and `selectionStartColumn`. */ @@ -147,6 +154,17 @@ export class Selection extends Range { return new Selection(start.lineNumber, start.column, end.lineNumber, end.column); } + /** + * Creates a `Selection` from a range, given a direction. + */ + public static fromRange(range: Range, direction: SelectionDirection): Selection { + if (direction === SelectionDirection.LTR) { + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } else { + return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); + } + } + /** * Create a `Selection` from an `ISelection`. */ diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index e6beb116f8..ebb2e8c37e 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -13,11 +13,12 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelInjectedTextChangedEvent, ModelRawContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { SearchData } from 'vs/editor/common/model/textModelSearch'; -import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes'; +import { FormattingOptions } from 'vs/editor/common/modes'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { MultilineTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore'; import { TextChange } from 'vs/editor/common/model/textChange'; import { equals } from 'vs/base/common/objects'; +import { IBracketPairs } from 'vs/editor/common/model/bracketPairs/bracketPairs'; /** * Vertical Lane in the overview ruler of the editor. @@ -112,7 +113,8 @@ export interface IModelDecorationOptions { collapseOnReplaceEdit?: boolean; /** * Specifies the stack order of a decoration. - * A decoration with greater stack order is always in front of a decoration with a lower stack order. + * A decoration with greater stack order is always in front of a decoration with + * a lower stack order when the decorations are on the same line. */ zIndex?: number; /** @@ -527,16 +529,6 @@ export class FindMatch { } } -/** - * @internal - */ -export interface IFoundBracket { - range: Range; - open: string[]; - close: string[]; - isOpen: boolean; -} - /** * Describes the behavior of decorations when typing/editing near their edges. * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` @@ -936,27 +928,21 @@ export interface ITextModel { /** * Get the language associated with this model. - * @internal */ - getLanguageIdentifier(): LanguageIdentifier; - - /** - * Get the language associated with this model. - */ - getModeId(): string; + getLanguageId(): string; /** * Set the current language mode associated with the model. * @internal */ - setMode(languageIdentifier: LanguageIdentifier): void; + setMode(languageId: string): void; /** * Returns the real (inner-most) language mode at a given position. * The result might be inaccurate. Use `forceTokenization` to ensure accurate tokens. * @internal */ - getLanguageIdAtPosition(lineNumber: number, column: number): LanguageId; + getLanguageIdAtPosition(lineNumber: number, column: number): string; /** * Get the word under or besides `position`. @@ -972,46 +958,6 @@ export interface ITextModel { */ getWordUntilPosition(position: IPosition): IWordAtPosition; - /** - * Find the matching bracket of `request` up, counting brackets. - * @param request The bracket we're searching for - * @param position The position at which to start the search. - * @return The range of the matching bracket, or null if the bracket match was not found. - * @internal - */ - findMatchingBracketUp(bracket: string, position: IPosition): Range | null; - - /** - * Find the first bracket in the model before `position`. - * @param position The position at which to start the search. - * @return The info for the first bracket before `position`, or null if there are no more brackets before `positions`. - * @internal - */ - findPrevBracket(position: IPosition): IFoundBracket | null; - - /** - * Find the first bracket in the model after `position`. - * @param position The position at which to start the search. - * @return The info for the first bracket after `position`, or null if there are no more brackets after `positions`. - * @internal - */ - findNextBracket(position: IPosition): IFoundBracket | null; - - /** - * Find the enclosing brackets that contain `position`. - * @param position The position at which to start the search. - * @internal - */ - findEnclosingBrackets(position: IPosition, maxDuration?: number): [Range, Range] | null; - - /** - * Given a `position`, if the position is on top or near a bracket, - * find the matching bracket of that bracket and return the ranges of both brackets. - * @param position The position at which to look for a bracket. - * @internal - */ - matchBracket(position: IPosition): [Range, Range] | null; - /** * @internal */ @@ -1022,6 +968,11 @@ export interface ITextModel { */ getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[]; + /** + * @internal + */ + getLinesBracketGuides(startLineNumber: number, endLineNumber: number, activePosition: IPosition | null, options: BracketGuideOptions): IndentGuide[][]; + /** * Change the decorations. The callback will be called with a change accessor * that becomes invalid as soon as the callback finishes executing. @@ -1280,8 +1231,7 @@ export interface ITextModel { onWillDispose(listener: () => void): IDisposable; /** - * Destroy this model. This will unbind the model from the mode - * and make all necessary clean-up to release this object to the GC. + * Destroy this model. */ dispose(): void; @@ -1325,6 +1275,55 @@ export interface ITextModel { * @internal */ getLineIndentColumn(lineNumber: number): number; + + /** + * Returns an object that can be used to query brackets. + * @internal + */ + get bracketPairs(): IBracketPairs; +} + +/** + * @internal + */ +export enum HorizontalGuidesState { + Disabled, + EnabledForActive, + Enabled +} + +/** + * @internal + */ +export interface BracketGuideOptions { + includeInactive: boolean, + horizontalGuides: HorizontalGuidesState, + highlightActive: boolean, +} + +/** + * @internal + */ +export class IndentGuide { + constructor( + public readonly visibleColumn: number, + public readonly className: string, + /** + * If set, this indent guide is a horizontal guide (no vertical part). + * It starts at visibleColumn and continues until endColumn. + */ + public readonly horizontalLine: IndentGuideHorizontalLine | null, + ) { } +} + +/** + * @internal + */ +export class IndentGuideHorizontalLine { + constructor( + public readonly top: boolean, + public readonly endColumn: number, + ) { } } /** diff --git a/src/vs/editor/common/model/bracketPairColorizer/ast.ts b/src/vs/editor/common/model/bracketPairColorizer/ast.ts deleted file mode 100644 index 71de2baf0c..0000000000 --- a/src/vs/editor/common/model/bracketPairColorizer/ast.ts +++ /dev/null @@ -1,480 +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 { tail } from 'vs/base/common/arrays'; -import { DenseKeyProvider, SmallImmutableSet } from './smallImmutableSet'; -import { lengthAdd, lengthZero, Length, lengthHash } from './length'; - -export const enum AstNodeKind { - Text = 0, - Bracket = 1, - Pair = 2, - UnexpectedClosingBracket = 3, - List = 4, -} - -export type AstNode = PairAstNode | ListAstNode | BracketAstNode | InvalidBracketAstNode | TextAstNode; - -abstract class BaseAstNode { - abstract readonly kind: AstNodeKind; - abstract readonly children: readonly AstNode[]; - abstract readonly unopenedBrackets: SmallImmutableSet; - - /** - * In case of a list, determines the height of the (2,3) tree. - */ - abstract readonly listHeight: number; - - abstract canBeReused( - expectedClosingCategories: SmallImmutableSet, - endLineDidChange: boolean - ): boolean; - - /** - * Flattenes all lists in this AST. Only for debugging. - */ - abstract flattenLists(): AstNode; - - /** - * Creates a deep clone. - */ - abstract clone(): AstNode; - - protected _length: Length; - - get length(): Length { - return this._length; - } - - constructor(length: Length) { - this._length = length; - } -} - -export class PairAstNode extends BaseAstNode { - public static create( - category: number, - openingBracket: BracketAstNode, - child: AstNode | null, - closingBracket: BracketAstNode | null - ) { - const length = computeLength(openingBracket, child, closingBracket); - - const children = new Array(1); - children[0] = openingBracket; - if (child) { - children.push(child); - } - if (closingBracket) { - children.push(closingBracket); - } - - return new PairAstNode(length, category, children, child ? child.unopenedBrackets : SmallImmutableSet.getEmpty()); - } - - get kind(): AstNodeKind.Pair { - return AstNodeKind.Pair; - } - get listHeight() { - return 0; - } - - canBeReused( - expectedClosingCategories: SmallImmutableSet, - endLineDidChange: boolean - ) { - if (this.closingBracket === null) { - // Unclosed pair ast nodes only - // end at the end of the document - // or when a parent node is closed. - - // This could be improved: - // Only return false if some next token is neither "undefined" nor a bracket that closes a parent. - - return false; - } - - if (expectedClosingCategories.intersects(this.unopenedBrackets)) { - return false; - } - - return true; - } - - flattenLists(): PairAstNode { - return PairAstNode.create( - this.category, - this.openingBracket.flattenLists(), - this.child && this.child.flattenLists(), - this.closingBracket && this.closingBracket.flattenLists() - ); - } - - get openingBracket(): BracketAstNode { - return this.children[0] as BracketAstNode; - } - - get child(): AstNode | null { - if (this.children.length <= 1) { - return null; - } - if (this.children[1].kind === AstNodeKind.Bracket) { - return null; - } - return this.children[1] || null; - } - - get closingBracket(): BracketAstNode | null { - if (this.children.length <= 1) { - return null; - } - if (this.children[1].kind === AstNodeKind.Bracket) { - return this.children[1] || null; - } - return (this.children[2] as BracketAstNode) || null; - } - - private constructor( - length: Length, - public readonly category: number, - public readonly children: readonly AstNode[], - public readonly unopenedBrackets: SmallImmutableSet - ) { - super(length); - } - - clone(): PairAstNode { - return new PairAstNode( - this.length, - this.category, - clone(this.children), - this.unopenedBrackets - ); - } -} - -function computeLength(openingBracket: BracketAstNode, child: AstNode | null, closingBracket: BracketAstNode | null): Length { - let length = openingBracket.length; - if (child) { - length = lengthAdd(length, child.length); - } - if (closingBracket) { - length = lengthAdd(length, closingBracket.length); - } - return length; -} - -export class ListAstNode extends BaseAstNode { - public static create(items: AstNode[]) { - if (items.length === 0) { - return new ListAstNode(lengthZero, 0, items, SmallImmutableSet.getEmpty()); - } else { - let length = items[0].length; - let unopenedBrackets = items[0].unopenedBrackets; - for (let i = 1; i < items.length; i++) { - length = lengthAdd(length, items[i].length); - unopenedBrackets = unopenedBrackets.merge(items[i].unopenedBrackets); - } - return new ListAstNode(length, items[0].listHeight + 1, items, unopenedBrackets); - } - } - - get kind(): AstNodeKind.List { - return AstNodeKind.List; - } - get children(): readonly AstNode[] { - return this._items; - } - get unopenedBrackets(): SmallImmutableSet { - return this._unopenedBrackets; - } - - private constructor( - length: Length, - public readonly listHeight: number, - private readonly _items: AstNode[], - private _unopenedBrackets: SmallImmutableSet - ) { - super(length); - } - - canBeReused( - expectedClosingCategories: SmallImmutableSet, - endLineDidChange: boolean - ): boolean { - if (this._items.length === 0) { - // might not be very helpful - return true; - } - - if (expectedClosingCategories.intersects(this.unopenedBrackets)) { - return false; - } - - let lastChild: AstNode = this; - while (lastChild.children.length > 0 && lastChild.kind === AstNodeKind.List) { - lastChild = tail(lastChild.children); - } - - return lastChild.canBeReused( - expectedClosingCategories, - endLineDidChange - ); - } - - flattenLists(): ListAstNode { - const items = new Array(); - for (const c of this.children) { - const normalized = c.flattenLists(); - if (normalized.kind === AstNodeKind.List) { - items.push(...normalized._items); - } else { - items.push(normalized); - } - } - return ListAstNode.create(items); - } - - clone(): ListAstNode { - return new ListAstNode(this.length, this.listHeight, clone(this._items), this.unopenedBrackets); - } - - private handleChildrenChanged(): void { - const items = this._items; - if (items.length === 0) { - return; - } - - let length = items[0].length; - let unopenedBrackets = items[0].unopenedBrackets; - for (let i = 1; i < items.length; i++) { - length = lengthAdd(length, items[i].length); - unopenedBrackets = unopenedBrackets.merge(items[i].unopenedBrackets); - } - this._length = length; - this._unopenedBrackets = unopenedBrackets; - } - - /** - * Appends the given node to the end of this (2,3) tree. - * Returns the new root. - */ - append(nodeToAppend: AstNode): AstNode { - const newNode = this._append(nodeToAppend); - if (newNode) { - return ListAstNode.create([this, newNode]); - } - return this; - } - - /** - * @returns Additional node after tree - */ - private _append(nodeToAppend: AstNode): AstNode | undefined { - // assert nodeToInsert.listHeight <= tree.listHeight - - if (nodeToAppend.listHeight === this.listHeight) { - return nodeToAppend; - } - - const lastItem = this._items[this._items.length - 1]; - const newNodeAfter = (lastItem.kind === AstNodeKind.List) ? lastItem._append(nodeToAppend) : nodeToAppend; - - if (!newNodeAfter) { - this.handleChildrenChanged(); - return undefined; - } - - // Can we take the element? - if (this._items.length >= 3) { - // assert tree.items.length === 3 - - // we need to split to maintain (2,3)-tree property. - // Send the third element + the new element to the parent. - const third = this._items.pop()!; - this.handleChildrenChanged(); - return ListAstNode.create([third, newNodeAfter]); - } else { - this._items.push(newNodeAfter); - this.handleChildrenChanged(); - return undefined; - } - } - - /** - * Prepends the given node to the end of this (2,3) tree. - * Returns the new root. - */ - prepend(nodeToPrepend: AstNode): AstNode { - const newNode = this._prepend(nodeToPrepend); - if (newNode) { - return ListAstNode.create([newNode, this]); - } - return this; - } - - /** - * @returns Additional node before tree - */ - private _prepend(nodeToPrepend: AstNode): AstNode | undefined { - // assert nodeToInsert.listHeight <= tree.listHeight - - if (nodeToPrepend.listHeight === this.listHeight) { - return nodeToPrepend; - } - - if (this.kind !== AstNodeKind.List) { - throw new Error('unexpected'); - } - - const first = this._items[0]; - const newNodeBefore = (first.kind === AstNodeKind.List) ? first._prepend(nodeToPrepend) : nodeToPrepend; - - if (!newNodeBefore) { - this.handleChildrenChanged(); - return undefined; - } - - if (this._items.length >= 3) { - // assert this.items.length === 3 - - // we need to split to maintain (2,3)-this property. - const first = this._items.shift()!; - this.handleChildrenChanged(); - return ListAstNode.create([newNodeBefore, first]); - } else { - this._items.unshift(newNodeBefore); - this.handleChildrenChanged(); - return undefined; - } - } -} - -function clone(arr: readonly AstNode[]): AstNode[] { - const result = new Array(arr.length); - for (let i = 0; i < arr.length; i++) { - result[i] = arr[i].clone(); - } - return result; -} - -const emptyArray: readonly AstNode[] = []; - -export class TextAstNode extends BaseAstNode { - get kind(): AstNodeKind.Text { - return AstNodeKind.Text; - } - get listHeight() { - return 0; - } - get children(): readonly AstNode[] { - return emptyArray; - } - get unopenedBrackets(): SmallImmutableSet { - return SmallImmutableSet.getEmpty(); - } - - canBeReused( - expectedClosingCategories: SmallImmutableSet, - endLineDidChange: boolean - ) { - // Don't reuse text from a line that got changed. - // Otherwise, long brackes might not be detected. - return !endLineDidChange; - } - - flattenLists(): TextAstNode { - return this; - } - clone(): TextAstNode { - return this; - } -} - -export class BracketAstNode extends BaseAstNode { - private static cacheByLength = new Map(); - - public static create(length: Length): BracketAstNode { - const lengthKey = lengthHash(length); - const cached = BracketAstNode.cacheByLength.get(lengthKey); - if (cached) { - return cached; - } - - const node = new BracketAstNode(length); - BracketAstNode.cacheByLength.set(lengthKey, node); - return node; - } - - private constructor(length: Length) { - super(length); - } - - get kind(): AstNodeKind.Bracket { - return AstNodeKind.Bracket; - } - get listHeight() { - return 0; - } - get children(): readonly AstNode[] { - return emptyArray; - } - - get unopenedBrackets(): SmallImmutableSet { - return SmallImmutableSet.getEmpty(); - } - - canBeReused( - expectedClosingCategories: SmallImmutableSet, - endLineDidChange: boolean - ) { - // These nodes could be reused, - // but not in a general way. - // Their parent may be reused. - return false; - } - - flattenLists(): BracketAstNode { - return this; - } - - clone(): BracketAstNode { - return this; - } -} - -export class InvalidBracketAstNode extends BaseAstNode { - get kind(): AstNodeKind.UnexpectedClosingBracket { - return AstNodeKind.UnexpectedClosingBracket; - } - get listHeight() { - return 0; - } - get children(): readonly AstNode[] { - return emptyArray; - } - - public readonly unopenedBrackets: SmallImmutableSet; - - constructor(category: number, length: Length, denseKeyProvider: DenseKeyProvider) { - super(length); - this.unopenedBrackets = SmallImmutableSet.getEmpty().add(category, denseKeyProvider); - } - - canBeReused( - expectedClosingCategories: SmallImmutableSet, - endLineDidChange: boolean - ) { - return !expectedClosingCategories.intersects(this.unopenedBrackets); - } - - flattenLists(): InvalidBracketAstNode { - return this; - } - - clone(): InvalidBracketAstNode { - return this; - } -} diff --git a/src/vs/editor/common/model/bracketPairColorizer/bracketPairColorizer.ts b/src/vs/editor/common/model/bracketPairColorizer/bracketPairColorizer.ts deleted file mode 100644 index ce2392a0ab..0000000000 --- a/src/vs/editor/common/model/bracketPairColorizer/bracketPairColorizer.ts +++ /dev/null @@ -1,300 +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 { Color } from 'vs/base/common/color'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; -import { Range } from 'vs/editor/common/core/range'; -import { IModelDecoration } from 'vs/editor/common/model'; -import { DenseKeyProvider } from 'vs/editor/common/model/bracketPairColorizer/smallImmutableSet'; -import { DecorationProvider } from 'vs/editor/common/model/decorationProvider'; -import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel'; -import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { LanguageId } from 'vs/editor/common/modes'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { - editorBracketHighlightingForeground1, editorBracketHighlightingForeground2, editorBracketHighlightingForeground3, editorBracketHighlightingForeground4, editorBracketHighlightingForeground5, editorBracketHighlightingForeground6, editorBracketHighlightingUnexpectedBracketForeground -} from 'vs/editor/common/view/editorColorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { AstNode, AstNodeKind } from './ast'; -import { TextEditInfo } from './beforeEditPositionMapper'; -import { LanguageAgnosticBracketTokens } from './brackets'; -import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './length'; -import { parseDocument } from './parser'; -import { FastTokenizer, TextBufferTokenizer } from './tokenizer'; - -export class BracketPairColorizer extends Disposable implements DecorationProvider { - private readonly didChangeDecorationsEmitter = new Emitter(); - private readonly cache = this._register(new MutableDisposable>()); - - get isDocumentSupported() { - const maxSupportedDocumentLength = /* max lines */ 50_000 * /* average column count */ 100; - return this.textModel.getValueLength() <= maxSupportedDocumentLength; - } - - constructor(private readonly textModel: TextModel) { - super(); - - this._register(LanguageConfigurationRegistry.onDidChange((e) => { - if (this.cache.value?.object.didLanguageChange(e.languageIdentifier.id)) { - this.cache.clear(); - this.updateCache(); - } - })); - - this._register(textModel.onDidChangeOptions(e => { - this.cache.clear(); - this.updateCache(); - })); - - this._register(textModel.onDidChangeAttached(() => { - this.updateCache(); - })); - } - - private updateCache() { - const options = this.textModel.getOptions().bracketPairColorizationOptions; - if (this.textModel.isAttachedToEditor() && this.isDocumentSupported && options.enabled) { - if (!this.cache.value) { - const store = new DisposableStore(); - this.cache.value = createDisposableRef(store.add(new BracketPairColorizerImpl(this.textModel)), store); - store.add(this.cache.value.object.onDidChangeDecorations(e => this.didChangeDecorationsEmitter.fire(e))); - this.didChangeDecorationsEmitter.fire(); - } - } else { - this.cache.clear(); - this.didChangeDecorationsEmitter.fire(); - } - } - - handleContentChanged(change: IModelContentChangedEvent) { - this.cache.value?.object.handleContentChanged(change); - } - - getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { - if (ownerId === undefined) { - return []; - } - return this.cache.value?.object.getDecorationsInRange(range, ownerId, filterOutValidation) || []; - } - - getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { - if (ownerId === undefined) { - return []; - } - return this.cache.value?.object.getAllDecorations(ownerId, filterOutValidation) || []; - } - - onDidChangeDecorations(listener: () => void): IDisposable { - return this.didChangeDecorationsEmitter.event(listener); - } -} - -function createDisposableRef(object: T, disposable?: IDisposable): IReference { - return { - object, - dispose: () => disposable?.dispose(), - }; -} - -class BracketPairColorizerImpl extends Disposable implements DecorationProvider { - private readonly didChangeDecorationsEmitter = new Emitter(); - private readonly colorProvider = new ColorProvider(); - - /* - There are two trees: - * The initial tree that has no token information and is used for performant initial bracket colorization. - * The tree that used token information to detect bracket pairs. - - To prevent flickering, we only switch from the initial tree to tree with token information - when tokenization completes. - Since the text can be edited while background tokenization is in progress, we need to update both trees. - */ - private initialAstWithoutTokens: AstNode | undefined; - private astWithTokens: AstNode | undefined; - - private readonly brackets = new LanguageAgnosticBracketTokens([]); - private readonly denseKeyProvider = new DenseKeyProvider(); - - public didLanguageChange(languageId: LanguageId): boolean { - return this.brackets.didLanguageChange(languageId); - } - - constructor(private readonly textModel: TextModel) { - super(); - - this._register(textModel.onBackgroundTokenizationStateChanged(() => { - if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { - const wasUndefined = this.initialAstWithoutTokens === undefined; - // Clear the initial tree as we can use the tree with token information now. - this.initialAstWithoutTokens = undefined; - if (!wasUndefined) { - this.didChangeDecorationsEmitter.fire(); - } - } - })); - - this._register(textModel.onDidChangeTokens(({ ranges }) => { - const edits = ranges.map(r => - new TextEditInfo( - toLength(r.fromLineNumber - 1, 0), - toLength(r.toLineNumber, 0), - toLength(r.toLineNumber - r.fromLineNumber + 1, 0) - ) - ); - this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens); - if (!this.initialAstWithoutTokens) { - this.didChangeDecorationsEmitter.fire(); - } - })); - - if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) { - // There are no token information yet - const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageIdentifier().id); - const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets); - this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, this.denseKeyProvider); - this.astWithTokens = this.initialAstWithoutTokens.clone(); - } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { - // Skip the initial ast, as there is no flickering. - // Directly create the tree with token information. - this.initialAstWithoutTokens = undefined; - this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined); - } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.InProgress) { - this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined); - this.astWithTokens = this.initialAstWithoutTokens.clone(); - } - } - - handleContentChanged(change: IModelContentChangedEvent) { - const edits = change.changes.map(c => { - const range = Range.lift(c.range); - return new TextEditInfo( - positionToLength(range.getStartPosition()), - positionToLength(range.getEndPosition()), - lengthOfString(c.text) - ); - }).reverse(); - - this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens); - if (this.initialAstWithoutTokens) { - this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(edits, this.initialAstWithoutTokens); - } - } - - /** - * @pure (only if isPure = true) - */ - private parseDocumentFromTextBuffer(edits: TextEditInfo[], previousAst: AstNode | undefined): AstNode { - // Is much faster if `isPure = false`. - const isPure = false; - const previousAstClone = isPure ? previousAst?.clone() : previousAst; - const tokenizer = new TextBufferTokenizer(this.textModel, this.brackets); - const result = parseDocument(tokenizer, edits, previousAstClone, this.denseKeyProvider); - return result; - } - - getBracketsInRange(range: Range): BracketInfo[] { - const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1); - const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1); - const result = new Array(); - const node = this.initialAstWithoutTokens || this.astWithTokens!; - collectBrackets(node, lengthZero, node.length, startOffset, endOffset, result); - return result; - } - - getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { - const result = new Array(); - const bracketsInRange = this.getBracketsInRange(range); - for (const bracket of bracketsInRange) { - result.push({ - id: `bracket${bracket.hash()}`, - options: { description: 'BracketPairColorization', inlineClassName: this.colorProvider.getInlineClassName(bracket) }, - ownerId: 0, - range: bracket.range - }); - } - return result; - } - getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { - return this.getDecorationsInRange(new Range(1, 1, this.textModel.getLineCount(), 1), ownerId, filterOutValidation); - } - - readonly onDidChangeDecorations = this.didChangeDecorationsEmitter.event; -} - -function collectBrackets(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, result: BracketInfo[], level: number = 0): void { - if (node.kind === AstNodeKind.Bracket) { - const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); - result.push(new BracketInfo(range, level - 1, false)); - } else if (node.kind === AstNodeKind.UnexpectedClosingBracket) { - const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); - result.push(new BracketInfo(range, level - 1, true)); - } else { - if (node.kind === AstNodeKind.Pair) { - level++; - } - for (const child of node.children) { - nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); - if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { - collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); - } - nodeOffsetStart = nodeOffsetEnd; - } - } -} - -export class BracketInfo { - constructor( - public readonly range: Range, - /** 0-based level */ - public readonly level: number, - public readonly isInvalid: boolean, - ) { } - - hash(): string { - return `${this.range.toString()}-${this.level}`; - } -} - -class ColorProvider { - public readonly unexpectedClosingBracketClassName = 'unexpected-closing-bracket'; - - getInlineClassName(bracket: BracketInfo): string { - if (bracket.isInvalid) { - return this.unexpectedClosingBracketClassName; - } - return this.getInlineClassNameOfLevel(bracket.level); - } - - getInlineClassNameOfLevel(level: number): string { - // To support a dynamic amount of colors up to 6 colors, - // we use a number that is a lcm of all numbers from 1 to 6. - return `bracket-highlighting-${level % 30}`; - } -} - -registerThemingParticipant((theme, collector) => { - const colors = [ - editorBracketHighlightingForeground1, - editorBracketHighlightingForeground2, - editorBracketHighlightingForeground3, - editorBracketHighlightingForeground4, - editorBracketHighlightingForeground5, - editorBracketHighlightingForeground6 - ]; - const colorProvider = new ColorProvider(); - - collector.addRule(`.monaco-editor .${colorProvider.unexpectedClosingBracketClassName} { color: ${theme.getColor(editorBracketHighlightingUnexpectedBracketForeground)}; }`); - - let colorValues = colors - .map(c => theme.getColor(c)) - .filter((c): c is Color => !!c) - .filter(c => !c.isTransparent()); - - for (let level = 0; level < 30; level++) { - const color = colorValues[level % colorValues.length]; - collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level)} { color: ${color}; }`); - } -}); diff --git a/src/vs/editor/common/model/bracketPairColorizer/brackets.ts b/src/vs/editor/common/model/bracketPairColorizer/brackets.ts deleted file mode 100644 index 0044a4f316..0000000000 --- a/src/vs/editor/common/model/bracketPairColorizer/brackets.ts +++ /dev/null @@ -1,120 +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 { escapeRegExpCharacters } from 'vs/base/common/strings'; -import { LanguageId } from 'vs/editor/common/modes'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { BracketAstNode } from './ast'; -import { toLength } from './length'; -import { Token, TokenKind } from './tokenizer'; - -export class BracketTokens { - static createFromLanguage(languageId: LanguageId, customBracketPairs: readonly [string, string][]): BracketTokens { - const brackets = [...(LanguageConfigurationRegistry.getBracketsSupport(languageId)?.brackets || [])]; - - const tokens = new BracketTokens(); - - let idxOffset = 0; - for (const pair of brackets) { - const brackets = [ - ...pair.open.map((value, idx) => ({ value, kind: TokenKind.OpeningBracket, idx: idx + idxOffset })), - ...pair.close.map((value, idx) => ({ value, kind: TokenKind.ClosingBracket, idx: idx + idxOffset })), - ]; - - idxOffset += Math.max(pair.open.length, pair.close.length); - - for (const bracket of brackets) { - tokens.addBracket(languageId, bracket.value, bracket.kind, bracket.idx); - } - } - - for (const pair of customBracketPairs) { - idxOffset++; - tokens.addBracket(languageId, pair[0], TokenKind.OpeningBracket, idxOffset); - tokens.addBracket(languageId, pair[1], TokenKind.ClosingBracket, idxOffset); - } - - return tokens; - } - - private hasRegExp = false; - private _regExpGlobal: RegExp | null = null; - private readonly map = new Map(); - - private addBracket(languageId: LanguageId, value: string, kind: TokenKind, idx: number): void { - const length = toLength(0, value.length); - this.map.set(value, - new Token( - length, - kind, - // A language can have at most 1000 bracket pairs. - languageId * 1000 + idx, - languageId, - BracketAstNode.create(length) - ) - ); - } - - getRegExpStr(): string | null { - if (this.isEmpty) { - return null; - } else { - const keys = [...this.map.keys()]; - keys.sort(); - keys.reverse(); - return keys.map(k => escapeRegExpCharacters(k)).join('|'); - } - } - - /** - * Returns null if there is no such regexp (because there are no brackets). - */ - get regExpGlobal(): RegExp | null { - if (!this.hasRegExp) { - const regExpStr = this.getRegExpStr(); - this._regExpGlobal = regExpStr ? new RegExp(regExpStr, 'g') : null; - this.hasRegExp = true; - } - return this._regExpGlobal; - } - - getToken(value: string): Token | undefined { - return this.map.get(value); - } - - get isEmpty(): boolean { - return this.map.size === 0; - } -} - -export class LanguageAgnosticBracketTokens { - private readonly languageIdToBracketTokens: Map = new Map(); - - constructor(private readonly customBracketPairs: readonly [string, string][]) { - } - - public didLanguageChange(languageId: LanguageId): boolean { - const existing = this.languageIdToBracketTokens.get(languageId); - if (!existing) { - return false; - } - const newRegExpStr = BracketTokens.createFromLanguage(languageId, this.customBracketPairs).getRegExpStr(); - return existing.getRegExpStr() !== newRegExpStr; - } - - getSingleLanguageBracketTokens(languageId: LanguageId): BracketTokens { - let singleLanguageBracketTokens = this.languageIdToBracketTokens.get(languageId); - if (!singleLanguageBracketTokens) { - singleLanguageBracketTokens = BracketTokens.createFromLanguage(languageId, this.customBracketPairs); - this.languageIdToBracketTokens.set(languageId, singleLanguageBracketTokens); - } - return singleLanguageBracketTokens; - } - - getToken(value: string, languageId: LanguageId): Token | undefined { - const singleLanguageBracketTokens = this.getSingleLanguageBracketTokens(languageId); - return singleLanguageBracketTokens.getToken(value); - } -} diff --git a/src/vs/editor/common/model/bracketPairColorizer/concat23Trees.ts b/src/vs/editor/common/model/bracketPairColorizer/concat23Trees.ts deleted file mode 100644 index bd130237df..0000000000 --- a/src/vs/editor/common/model/bracketPairColorizer/concat23Trees.ts +++ /dev/null @@ -1,92 +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 { AstNode, ListAstNode } from './ast'; - -/** - * Concatenates a list of (2,3) AstNode's into a single (2,3) AstNode. - * This mutates the items of the input array! -*/ -export function concat23Trees(items: AstNode[]): AstNode | null { - if (items.length === 0) { - return null; - } - if (items.length === 1) { - return items[0]; - } - - if (allItemsHaveSameHeight(items)) { - return concatFast(items); - } - return concatSlow(items); -} - -/** - * @param items must be non empty. -*/ -function allItemsHaveSameHeight(items: AstNode[]): boolean { - const firstHeight = items[0].listHeight; - - for (const item of items) { - if (item.listHeight !== firstHeight) { - return false; - } - } - return true; -} - -function concatFast(items: AstNode[]): AstNode | null { - let length = items.length; - // All trees have same height, just create parent nodes. - while (length > 1) { - const newLength = length >> 1; - // Ideally, due to the slice, not a lot of memory is wasted. - const newItems = new Array(newLength); - for (let i = 0; i < newLength; i++) { - const j = i << 1; - newItems[i] = ListAstNode.create(items.slice(j, (j + 3 === length) ? length : j + 2)); - } - length = newLength; - items = newItems; - } - return items[0]; -} - -function heightDiff(node1: AstNode, node2: AstNode): number { - return Math.abs(node1.listHeight - node2.listHeight); -} - -function concatSlow(items: AstNode[]): AstNode | null { - // The items might not have the same height. - // We merge all items by using a binary concat operator. - let first = items[0]; - let second = items[1]; - - for (let i = 2; i < items.length; i++) { - const item = items[i]; - // Prefer concatenating smaller trees, as the runtime of concat depends on the tree height. - if (heightDiff(first, second) <= heightDiff(second, item)) { - first = concat(first, second); - second = item; - } else { - second = concat(second, item); - } - } - - const result = concat(first, second); - return result; -} - -function concat(node1: AstNode, node2: AstNode): AstNode { - if (node1.listHeight === node2.listHeight) { - return ListAstNode.create([node1, node2]); - } - else if (node1.listHeight > node2.listHeight) { - // node1 is the tree we want to insert into - return (node1 as ListAstNode).append(node2); - } else { - return (node2 as ListAstNode).prepend(node1); - } -} diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairs.ts b/src/vs/editor/common/model/bracketPairs/bracketPairs.ts new file mode 100644 index 0000000000..39848b6031 --- /dev/null +++ b/src/vs/editor/common/model/bracketPairs/bracketPairs.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IPosition } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; + +export interface IBracketPairs { + /** + * Is fired when bracket pairs change, either due to a text or a settings change. + */ + onDidChange: Event; + + /** + * Gets all bracket pairs that intersect the given position. + * The result is sorted by the start position. + */ + getBracketPairsInRange(range: IRange): BracketPairInfo[]; + + /** + * Gets all bracket pairs that intersect the given position. + * The result is sorted by the start position. + */ + getBracketPairsInRangeWithMinIndentation(range: IRange): BracketPairWithMinIndentationInfo[]; + + getBracketsInRange(range: IRange): BracketInfo[]; + + /** + * Find the matching bracket of `request` up, counting brackets. + * @param request The bracket we're searching for + * @param position The position at which to start the search. + * @return The range of the matching bracket, or null if the bracket match was not found. + */ + findMatchingBracketUp(bracket: string, position: IPosition): Range | null; + + /** + * Find the first bracket in the model before `position`. + * @param position The position at which to start the search. + * @return The info for the first bracket before `position`, or null if there are no more brackets before `positions`. + */ + findPrevBracket(position: IPosition): IFoundBracket | null; + + /** + * Find the first bracket in the model after `position`. + * @param position The position at which to start the search. + * @return The info for the first bracket after `position`, or null if there are no more brackets after `positions`. + */ + findNextBracket(position: IPosition): IFoundBracket | null; + + /** + * Find the enclosing brackets that contain `position`. + * @param position The position at which to start the search. + */ + findEnclosingBrackets(position: IPosition, maxDuration?: number): [Range, Range] | null; + + /** + * Given a `position`, if the position is on top or near a bracket, + * find the matching bracket of that bracket and return the ranges of both brackets. + * @param position The position at which to look for a bracket. + */ + matchBracket(position: IPosition): [Range, Range] | null; +} + +export interface IFoundBracket { + range: Range; + open: string[]; + close: string[]; + isOpen: boolean; +} + +export class BracketInfo { + constructor( + public readonly range: Range, + /** 0-based level */ + public readonly nestingLevel: number, + public readonly isInvalid: boolean, + ) { } +} + +export class BracketPairInfo { + constructor( + public readonly range: Range, + public readonly openingBracketRange: Range, + public readonly closingBracketRange: Range | undefined, + /** + * 0-based + */ + public readonly nestingLevel: number, + ) { } +} + +export class BracketPairWithMinIndentationInfo extends BracketPairInfo { + constructor( + range: Range, + openingBracketRange: Range, + closingBracketRange: Range | undefined, + /** + * 0-based + */ + nestingLevel: number, + /** + * -1 if not requested, otherwise the size of the minimum indentation in the bracket pair in terms of visible columns. + */ + public readonly minVisibleColumnIndentation: number, + ) { + super(range, openingBracketRange, closingBracketRange, nestingLevel); + } +} diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairsImpl.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsImpl.ts new file mode 100644 index 0000000000..f7c8a8cdfc --- /dev/null +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsImpl.ts @@ -0,0 +1,762 @@ +/*--------------------------------------------------------------------------------------------- + * 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, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; +import { LineTokens } from 'vs/editor/common/core/lineTokens'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { BracketPairsTree } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree'; +import { BracketInfo, BracketPairInfo, BracketPairWithMinIndentationInfo, IBracketPairs, IFoundBracket } from 'vs/editor/common/model/bracketPairs/bracketPairs'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { ignoreBracketsInToken } from 'vs/editor/common/modes/supports'; +import { RichEditBrackets, BracketsUtils, RichEditBracket } from 'vs/editor/common/modes/supports/richEditBrackets'; + +export class BracketPairs extends Disposable implements IBracketPairs { + private readonly bracketPairsTree = this._register(new MutableDisposable>()); + + private readonly onDidChangeEmitter = new Emitter(); + public readonly onDidChange = this.onDidChangeEmitter.event; + + private get isDocumentSupported() { + const maxSupportedDocumentLength = /* max lines */ 50_000 * /* average column count */ 100; + return this.textModel.getValueLength() <= maxSupportedDocumentLength; + } + + private bracketsRequested = false; + + public constructor( + private readonly textModel: TextModel, + private readonly languageConfigurationService: ILanguageConfigurationService + ) { + super(); + + this._register(textModel.onDidChangeOptions(e => { + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); + })); + + this._register(textModel.onDidChangeLanguage(e => { + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); + })); + + this._register( + this.languageConfigurationService.onDidChange(e => { + if (!e.languageId || this.bracketPairsTree.value?.object.didLanguageChange(e.languageId)) { + this.bracketPairsTree.clear(); + this.updateBracketPairsTree(); + } + }) + ); + } + + private updateBracketPairsTree() { + if (this.bracketsRequested && this.isDocumentSupported) { + if (!this.bracketPairsTree.value) { + const store = new DisposableStore(); + + this.bracketPairsTree.value = createDisposableRef( + store.add( + new BracketPairsTree(this.textModel, (languageId) => { + return this.languageConfigurationService.getLanguageConfiguration(languageId); + }) + ), + store + ); + store.add(this.bracketPairsTree.value.object.onDidChange(e => this.onDidChangeEmitter.fire(e))); + this.onDidChangeEmitter.fire(); + } + } else { + this.bracketPairsTree.clear(); + this.onDidChangeEmitter.fire(); + } + } + + public handleContentChanged(change: IModelContentChangedEvent) { + this.bracketPairsTree.value?.object.handleContentChanged(change); + } + + /** + * Returns all bracket pairs that intersect the given range. + * The result is sorted by the start position. + */ + public getBracketPairsInRange(range: Range): BracketPairInfo[] { + this.bracketsRequested = true; + this.updateBracketPairsTree(); + return this.bracketPairsTree.value?.object.getBracketPairsInRange(range, false) || []; + } + + public getBracketPairsInRangeWithMinIndentation(range: Range): BracketPairWithMinIndentationInfo[] { + this.bracketsRequested = true; + this.updateBracketPairsTree(); + return this.bracketPairsTree.value?.object.getBracketPairsInRange(range, true) || []; + } + + public getBracketsInRange(range: Range): BracketInfo[] { + this.bracketsRequested = true; + this.updateBracketPairsTree(); + return this.bracketPairsTree.value?.object.getBracketsInRange(range) || []; + } + + public findMatchingBracketUp(_bracket: string, _position: IPosition): Range | null { + let bracket = _bracket.toLowerCase(); + let position = this.textModel.validatePosition(_position); + + const languageId = this.textModel.getLanguageIdAtPosition(position.lineNumber, position.column); + let bracketsSupport = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + + if (!bracketsSupport) { + return null; + } + + let data = bracketsSupport.textIsBracket[bracket]; + + if (!data) { + return null; + } + + return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, null)); + } + + public matchBracket(position: IPosition): [Range, Range] | null { + return this._matchBracket(this.textModel.validatePosition(position)); + } + + private _establishBracketSearchOffsets(position: Position, lineTokens: LineTokens, modeBrackets: RichEditBrackets, tokenIndex: number) { + const tokenCount = lineTokens.getCount(); + const currentLanguageId = lineTokens.getLanguageId(tokenIndex); + + // limit search to not go before `maxBracketLength` + let searchStartOffset = Math.max(0, position.column - 1 - modeBrackets.maxBracketLength); + for (let i = tokenIndex - 1; i >= 0; i--) { + const tokenEndOffset = lineTokens.getEndOffset(i); + if (tokenEndOffset <= searchStartOffset) { + break; + } + if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) { + searchStartOffset = tokenEndOffset; + break; + } + } + + // limit search to not go after `maxBracketLength` + let searchEndOffset = Math.min(lineTokens.getLineContent().length, position.column - 1 + modeBrackets.maxBracketLength); + for (let i = tokenIndex + 1; i < tokenCount; i++) { + const tokenStartOffset = lineTokens.getStartOffset(i); + if (tokenStartOffset >= searchEndOffset) { + break; + } + if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) { + searchEndOffset = tokenStartOffset; + break; + } + } + + return { searchStartOffset, searchEndOffset }; + } + + private _matchBracket(position: Position): [Range, Range] | null { + const lineNumber = position.lineNumber; + const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineText = this.textModel.getLineContent(lineNumber); + + const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + if (tokenIndex < 0) { + return null; + } + const currentModeBrackets = this.languageConfigurationService.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).brackets; + + // check that the token is not to be ignored + if (currentModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))) { + + let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, currentModeBrackets, tokenIndex); + + // it might be the case that [currentTokenStart -> currentTokenEnd] contains multiple brackets + // `bestResult` will contain the most right-side result + let bestResult: [Range, Range] | null = null; + while (true) { + const foundBracket = BracketsUtils.findNextBracketInRange(currentModeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!foundBracket) { + // there are no more brackets in this text + break; + } + + // check that we didn't hit a bracket too far away from position + if (foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { + const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); + const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText], null); + if (r) { + if (r instanceof BracketSearchCanceled) { + return null; + } + bestResult = r; + } + } + + searchStartOffset = foundBracket.endColumn - 1; + } + + if (bestResult) { + return bestResult; + } + } + + // If position is in between two tokens, try also looking in the previous token + if (tokenIndex > 0 && lineTokens.getStartOffset(tokenIndex) === position.column - 1) { + const prevTokenIndex = tokenIndex - 1; + const prevModeBrackets = this.languageConfigurationService.getLanguageConfiguration(lineTokens.getLanguageId(prevTokenIndex)).brackets; + + // check that previous token is not to be ignored + if (prevModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(prevTokenIndex))) { + + let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, prevModeBrackets, prevTokenIndex); + + const foundBracket = BracketsUtils.findPrevBracketInRange(prevModeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + + // check that we didn't hit a bracket too far away from position + if (foundBracket && foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { + const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); + const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText], null); + if (r) { + if (r instanceof BracketSearchCanceled) { + return null; + } + return r; + } + } + } + } + + return null; + } + + private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null | BracketSearchCanceled { + if (!data) { + return null; + } + + const matched = ( + isOpen + ? this._findMatchingBracketDown(data, foundBracket.getEndPosition(), continueSearchPredicate) + : this._findMatchingBracketUp(data, foundBracket.getStartPosition(), continueSearchPredicate) + ); + + if (!matched) { + return null; + } + + if (matched instanceof BracketSearchCanceled) { + return matched; + } + + return [foundBracket, matched]; + } + + private _findMatchingBracketUp(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { + // console.log('_findMatchingBracketUp: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); + + const languageId = bracket.languageId; + const reversedBracketRegex = bracket.reversedRegex; + let count = -1; + + let totalCallCount = 0; + const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { + while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } + const r = BracketsUtils.findPrevBracketInRange(reversedBracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!r) { + break; + } + + const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); + if (bracket.isOpen(hitText)) { + count++; + } else if (bracket.isClose(hitText)) { + count--; + } + + if (count === 0) { + return r; + } + + searchEndOffset = r.startColumn - 1; + } + + return null; + }; + + for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = tokenCount - 1; + let searchStartOffset = lineText.length; + let searchEndOffset = lineText.length; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + } + + let prevSearchInToken = true; + for (; tokenIndex >= 0; tokenIndex--) { + const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchStartOffset + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + prevSearchInToken = searchInToken; + } + + if (prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + return null; + } + + private _findMatchingBracketDown(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { + // console.log('_findMatchingBracketDown: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); + + const languageId = bracket.languageId; + const bracketRegex = bracket.forwardRegex; + let count = 1; + + let totalCallCount = 0; + const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { + while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } + const r = BracketsUtils.findNextBracketInRange(bracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!r) { + break; + } + + const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); + if (bracket.isOpen(hitText)) { + count++; + } else if (bracket.isClose(hitText)) { + count--; + } + + if (count === 0) { + return r; + } + + searchStartOffset = r.endColumn - 1; + } + + return null; + }; + + const lineCount = this.textModel.getLineCount(); + for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = 0; + let searchStartOffset = 0; + let searchEndOffset = 0; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + } + + let prevSearchInToken = true; + for (; tokenIndex < tokenCount; tokenIndex++) { + const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchEndOffset + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + prevSearchInToken = searchInToken; + } + + if (prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + return null; + } + + public findPrevBracket(_position: IPosition): IFoundBracket | null { + const position = this.textModel.validatePosition(_position); + + let languageId: string | null = null; + let modeBrackets: RichEditBrackets | null = null; + for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = tokenCount - 1; + let searchStartOffset = lineText.length; + let searchEndOffset = lineText.length; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + if (languageId !== tokenLanguageId) { + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + } + } + + let prevSearchInToken = true; + for (; tokenIndex >= 0; tokenIndex--) { + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + + if (languageId !== tokenLanguageId) { + // language id change! + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + prevSearchInToken = false; + } + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + } + + const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchStartOffset + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + } + } + + prevSearchInToken = searchInToken; + } + + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + } + } + + return null; + } + + public findNextBracket(_position: IPosition): IFoundBracket | null { + const position = this.textModel.validatePosition(_position); + const lineCount = this.textModel.getLineCount(); + + let languageId: string | null = null; + let modeBrackets: RichEditBrackets | null = null; + for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = 0; + let searchStartOffset = 0; + let searchEndOffset = 0; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + if (languageId !== tokenLanguageId) { + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + } + } + + let prevSearchInToken = true; + for (; tokenIndex < tokenCount; tokenIndex++) { + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + + if (languageId !== tokenLanguageId) { + // language id change! + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + prevSearchInToken = false; + } + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + } + + const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchEndOffset + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + } + } + + prevSearchInToken = searchInToken; + } + + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return this._toFoundBracket(modeBrackets, r); + } + } + } + + return null; + } + + public findEnclosingBrackets(_position: IPosition, maxDuration?: number): [Range, Range] | null { + let continueSearchPredicate: ContinueBracketSearchPredicate; + if (typeof maxDuration === 'undefined') { + continueSearchPredicate = null; + } else { + const startTime = Date.now(); + continueSearchPredicate = () => { + return (Date.now() - startTime <= maxDuration); + }; + } + const position = this.textModel.validatePosition(_position); + const lineCount = this.textModel.getLineCount(); + const savedCounts = new Map(); + + let counts: number[] = []; + const resetCounts = (languageId: string, modeBrackets: RichEditBrackets | null) => { + if (!savedCounts.has(languageId)) { + let tmp = []; + for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) { + tmp[i] = 0; + } + savedCounts.set(languageId, tmp); + } + counts = savedCounts.get(languageId)!; + }; + + let totalCallCount = 0; + const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null | BracketSearchCanceled => { + while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!r) { + break; + } + + const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); + const bracket = modeBrackets.textIsBracket[hitText]; + if (bracket) { + if (bracket.isOpen(hitText)) { + counts[bracket.index]++; + } else if (bracket.isClose(hitText)) { + counts[bracket.index]--; + } + + if (counts[bracket.index] === -1) { + return this._matchFoundBracket(r, bracket, false, continueSearchPredicate); + } + } + + searchStartOffset = r.endColumn - 1; + } + return null; + }; + + let languageId: string | null = null; + let modeBrackets: RichEditBrackets | null = null; + for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + const lineTokens = this.textModel.getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this.textModel.getLineContent(lineNumber); + + let tokenIndex = 0; + let searchStartOffset = 0; + let searchEndOffset = 0; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + if (languageId !== tokenLanguageId) { + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + resetCounts(languageId, modeBrackets); + } + } + + let prevSearchInToken = true; + for (; tokenIndex < tokenCount; tokenIndex++) { + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + + if (languageId !== tokenLanguageId) { + // language id change! + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return stripBracketSearchCanceled(r); + } + prevSearchInToken = false; + } + languageId = tokenLanguageId; + modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + resetCounts(languageId, modeBrackets); + } + + const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchEndOffset + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return stripBracketSearchCanceled(r); + } + } + } + + prevSearchInToken = searchInToken; + } + + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return stripBracketSearchCanceled(r); + } + } + } + + return null; + } + + private _toFoundBracket(modeBrackets: RichEditBrackets, r: Range): IFoundBracket | null { + if (!r) { + return null; + } + + let text = this.textModel.getValueInRange(r); + text = text.toLowerCase(); + + let data = modeBrackets.textIsBracket[text]; + if (!data) { + return null; + } + + return { + range: r, + open: data.open, + close: data.close, + isOpen: modeBrackets.textIsOpenBracket[text] + }; + } +} + +function createDisposableRef(object: T, disposable?: IDisposable): IReference { + return { + object, + dispose: () => disposable?.dispose(), + }; +} + +type ContinueBracketSearchPredicate = null | (() => boolean); + +class BracketSearchCanceled { + public static INSTANCE = new BracketSearchCanceled(); + _searchCanceledBrand = undefined; + private constructor() { } +} + +function stripBracketSearchCanceled(result: T | null | BracketSearchCanceled): T | null { + if (result instanceof BracketSearchCanceled) { + return null; + } + return result; +} diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairsTree/ast.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/ast.ts new file mode 100644 index 0000000000..27bc72f555 --- /dev/null +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/ast.ts @@ -0,0 +1,684 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CursorColumns } from 'vs/editor/common/controller/cursorColumns'; +import { ITextModel } from 'vs/editor/common/model'; +import { Length, lengthAdd, lengthGetLineCount, lengthHash, lengthToObj, lengthZero } from './length'; +import { SmallImmutableSet } from './smallImmutableSet'; +import { OpeningBracketId } from './tokenizer'; + +export const enum AstNodeKind { + Text = 0, + Bracket = 1, + Pair = 2, + UnexpectedClosingBracket = 3, + List = 4, +} + +export type AstNode = PairAstNode | ListAstNode | BracketAstNode | InvalidBracketAstNode | TextAstNode; + +/** + * The base implementation for all AST nodes. +*/ +abstract class BaseAstNode { + public abstract readonly kind: AstNodeKind; + + public abstract readonly childrenLength: number; + + /** + * Might return null even if {@link idx} is smaller than {@link BaseAstNode.childrenLength}. + */ + public abstract getChild(idx: number): AstNode | null; + + /** + * Try to avoid using this property, as implementations might need to allocate the resulting array. + */ + public abstract readonly children: readonly AstNode[]; + + /** + * Represents the set of all (potentially) missing opening bracket ids in this node. + * E.g. in `{ ] ) }` that set is {`[`, `(` }. + */ + public abstract readonly missingOpeningBracketIds: SmallImmutableSet; + + /** + * In case of a list, determines the height of the (2,3) tree. + */ + public abstract readonly listHeight: number; + + protected _length: Length; + + /** + * The length of the entire node, which should equal the sum of lengths of all children. + */ + public get length(): Length { + return this._length; + } + + public constructor(length: Length) { + this._length = length; + } + + /** + * @param openBracketIds The set of all opening brackets that have not yet been closed. + */ + public abstract canBeReused( + openBracketIds: SmallImmutableSet + ): boolean; + + /** + * Flattens all lists in this AST. Only for debugging. + */ + public abstract flattenLists(): AstNode; + + /** + * Creates a deep clone. + */ + public abstract deepClone(): AstNode; + + public abstract computeMinIndentation(offset: Length, textModel: ITextModel): number; +} + +/** + * Represents a bracket pair including its child (e.g. `{ ... }`). + * Might be unclosed. + * Immutable, if all children are immutable. +*/ +export class PairAstNode extends BaseAstNode { + public static create( + openingBracket: BracketAstNode, + child: AstNode | null, + closingBracket: BracketAstNode | null + ) { + let length = openingBracket.length; + if (child) { + length = lengthAdd(length, child.length); + } + if (closingBracket) { + length = lengthAdd(length, closingBracket.length); + } + return new PairAstNode(length, openingBracket, child, closingBracket, child ? child.missingOpeningBracketIds : SmallImmutableSet.getEmpty()); + } + + public get kind(): AstNodeKind.Pair { + return AstNodeKind.Pair; + } + public get listHeight() { + return 0; + } + public get childrenLength(): number { + return 3; + } + public getChild(idx: number): AstNode | null { + switch (idx) { + case 0: return this.openingBracket; + case 1: return this.child; + case 2: return this.closingBracket; + } + throw new Error('Invalid child index'); + } + + /** + * Avoid using this property, it allocates an array! + */ + public get children() { + const result = new Array(); + result.push(this.openingBracket); + if (this.child) { + result.push(this.child); + } + if (this.closingBracket) { + result.push(this.closingBracket); + } + return result; + } + + private constructor( + length: Length, + public readonly openingBracket: BracketAstNode, + public readonly child: AstNode | null, + public readonly closingBracket: BracketAstNode | null, + public readonly missingOpeningBracketIds: SmallImmutableSet + ) { + super(length); + } + + public canBeReused(openBracketIds: SmallImmutableSet) { + if (this.closingBracket === null) { + // Unclosed pair ast nodes only + // end at the end of the document + // or when a parent node is closed. + + // This could be improved: + // Only return false if some next token is neither "undefined" nor a bracket that closes a parent. + + return false; + } + + if (openBracketIds.intersects(this.missingOpeningBracketIds)) { + return false; + } + + return true; + } + + public flattenLists(): PairAstNode { + return PairAstNode.create( + this.openingBracket.flattenLists(), + this.child && this.child.flattenLists(), + this.closingBracket && this.closingBracket.flattenLists() + ); + } + + public deepClone(): PairAstNode { + return new PairAstNode( + this.length, + this.openingBracket.deepClone(), + this.child && this.child.deepClone(), + this.closingBracket && this.closingBracket.deepClone(), + this.missingOpeningBracketIds + ); + } + + public computeMinIndentation(offset: Length, textModel: ITextModel): number { + return this.child ? this.child.computeMinIndentation(lengthAdd(offset, this.openingBracket.length), textModel) : Number.MAX_SAFE_INTEGER; + } +} + +export abstract class ListAstNode extends BaseAstNode { + /** + * This method uses more memory-efficient list nodes that can only store 2 or 3 children. + */ + public static create23(item1: AstNode, item2: AstNode, item3: AstNode | null, immutable: boolean = false): ListAstNode { + let length = item1.length; + let missingBracketIds = item1.missingOpeningBracketIds; + + if (item1.listHeight !== item2.listHeight) { + throw new Error('Invalid list heights'); + } + + length = lengthAdd(length, item2.length); + missingBracketIds = missingBracketIds.merge(item2.missingOpeningBracketIds); + + if (item3) { + if (item1.listHeight !== item3.listHeight) { + throw new Error('Invalid list heights'); + } + length = lengthAdd(length, item3.length); + missingBracketIds = missingBracketIds.merge(item3.missingOpeningBracketIds); + } + return immutable + ? new Immutable23ListAstNode(length, item1.listHeight + 1, item1, item2, item3, missingBracketIds) + : new TwoThreeListAstNode(length, item1.listHeight + 1, item1, item2, item3, missingBracketIds); + } + + public static create(items: AstNode[], immutable: boolean = false): ListAstNode { + if (items.length === 0) { + return this.getEmpty(); + } else { + let length = items[0].length; + let unopenedBrackets = items[0].missingOpeningBracketIds; + for (let i = 1; i < items.length; i++) { + length = lengthAdd(length, items[i].length); + unopenedBrackets = unopenedBrackets.merge(items[i].missingOpeningBracketIds); + } + return immutable + ? new ImmutableArrayListAstNode(length, items[0].listHeight + 1, items, unopenedBrackets) + : new ArrayListAstNode(length, items[0].listHeight + 1, items, unopenedBrackets); + } + } + + public static getEmpty() { + return new ImmutableArrayListAstNode(lengthZero, 0, [], SmallImmutableSet.getEmpty()); + } + + public get kind(): AstNodeKind.List { + return AstNodeKind.List; + } + + public get missingOpeningBracketIds(): SmallImmutableSet { + return this._missingOpeningBracketIds; + } + + private cachedMinIndentation: number = -1; + + /** + * Use ListAstNode.create. + */ + constructor( + length: Length, + public readonly listHeight: number, + private _missingOpeningBracketIds: SmallImmutableSet + ) { + super(length); + } + + protected throwIfImmutable(): void { + // NOOP + } + + protected abstract setChild(idx: number, child: AstNode): void; + + public makeLastElementMutable(): AstNode | undefined { + this.throwIfImmutable(); + const childCount = this.childrenLength; + if (childCount === 0) { + return undefined; + } + const lastChild = this.getChild(childCount - 1)!; + const mutable = lastChild.kind === AstNodeKind.List ? lastChild.toMutable() : lastChild; + if (lastChild !== mutable) { + this.setChild(childCount - 1, mutable); + } + return mutable; + } + + public makeFirstElementMutable(): AstNode | undefined { + this.throwIfImmutable(); + const childCount = this.childrenLength; + if (childCount === 0) { + return undefined; + } + const firstChild = this.getChild(0)!; + const mutable = firstChild.kind === AstNodeKind.List ? firstChild.toMutable() : firstChild; + if (firstChild !== mutable) { + this.setChild(0, mutable); + } + return mutable; + } + + public canBeReused(openBracketIds: SmallImmutableSet): boolean { + if (openBracketIds.intersects(this.missingOpeningBracketIds)) { + return false; + } + + let lastChild: ListAstNode = this; + let lastLength: number; + while (lastChild.kind === AstNodeKind.List && (lastLength = lastChild.childrenLength) > 0) { + lastChild = lastChild.getChild(lastLength! - 1) as ListAstNode; + } + + return lastChild.canBeReused(openBracketIds); + } + + public handleChildrenChanged(): void { + this.throwIfImmutable(); + + const count = this.childrenLength; + + let length = this.getChild(0)!.length; + let unopenedBrackets = this.getChild(0)!.missingOpeningBracketIds; + + for (let i = 1; i < count; i++) { + const child = this.getChild(i)!; + length = lengthAdd(length, child.length); + unopenedBrackets = unopenedBrackets.merge(child.missingOpeningBracketIds); + } + + this._length = length; + this._missingOpeningBracketIds = unopenedBrackets; + this.cachedMinIndentation = -1; + } + + public flattenLists(): ListAstNode { + const items = new Array(); + for (const c of this.children) { + const normalized = c.flattenLists(); + if (normalized.kind === AstNodeKind.List) { + items.push(...normalized.children); + } else { + items.push(normalized); + } + } + return ListAstNode.create(items); + } + + public computeMinIndentation(offset: Length, textModel: ITextModel): number { + if (this.cachedMinIndentation !== -1) { + return this.cachedMinIndentation; + } + + let minIndentation = Number.MAX_SAFE_INTEGER; + let childOffset = offset; + for (let i = 0; i < this.childrenLength; i++) { + const child = this.getChild(i); + if (child) { + minIndentation = Math.min(minIndentation, child.computeMinIndentation(childOffset, textModel)); + childOffset = lengthAdd(childOffset, child.length); + } + } + + this.cachedMinIndentation = minIndentation; + return minIndentation; + } + + /** + * Creates a shallow clone that is mutable, or itself if it is already mutable. + */ + public abstract toMutable(): ListAstNode; + + public abstract appendChildOfSameHeight(node: AstNode): void; + public abstract unappendChild(): AstNode | undefined; + public abstract prependChildOfSameHeight(node: AstNode): void; + public abstract unprependChild(): AstNode | undefined; +} + +class TwoThreeListAstNode extends ListAstNode { + public get childrenLength(): number { + return this._item3 !== null ? 3 : 2; + } + public getChild(idx: number): AstNode | null { + switch (idx) { + case 0: return this._item1; + case 1: return this._item2; + case 2: return this._item3; + } + throw new Error('Invalid child index'); + } + public setChild(idx: number, node: AstNode): void { + switch (idx) { + case 0: this._item1 = node; return; + case 1: this._item2 = node; return; + case 2: this._item3 = node; return; + } + throw new Error('Invalid child index'); + } + + public get children(): readonly AstNode[] { + return this._item3 ? [this._item1, this._item2, this._item3] : [this._item1, this._item2]; + } + + public get item1(): AstNode { + return this._item1; + } + public get item2(): AstNode { + return this._item2; + } + public get item3(): AstNode | null { + return this._item3; + } + + public constructor( + length: Length, + listHeight: number, + private _item1: AstNode, + private _item2: AstNode, + private _item3: AstNode | null, + missingOpeningBracketIds: SmallImmutableSet + ) { + super(length, listHeight, missingOpeningBracketIds); + } + + public deepClone(): ListAstNode { + return new TwoThreeListAstNode( + this.length, + this.listHeight, + this._item1.deepClone(), + this._item2.deepClone(), + this._item3 ? this._item3.deepClone() : null, + this.missingOpeningBracketIds + ); + } + + public appendChildOfSameHeight(node: AstNode): void { + if (this._item3) { + throw new Error('Cannot append to a full (2,3) tree node'); + } + this.throwIfImmutable(); + this._item3 = node; + this.handleChildrenChanged(); + } + + public unappendChild(): AstNode | undefined { + if (!this._item3) { + throw new Error('Cannot remove from a non-full (2,3) tree node'); + } + this.throwIfImmutable(); + const result = this._item3; + this._item3 = null; + this.handleChildrenChanged(); + return result; + } + + public prependChildOfSameHeight(node: AstNode): void { + if (this._item3) { + throw new Error('Cannot prepend to a full (2,3) tree node'); + } + this.throwIfImmutable(); + this._item3 = this._item2; + this._item2 = this._item1; + this._item1 = node; + this.handleChildrenChanged(); + } + + public unprependChild(): AstNode | undefined { + if (!this._item3) { + throw new Error('Cannot remove from a non-full (2,3) tree node'); + } + this.throwIfImmutable(); + const result = this._item1; + this._item1 = this._item2; + this._item2 = this._item3; + this._item3 = null; + + this.handleChildrenChanged(); + return result; + } + + override toMutable(): ListAstNode { + return this; + } +} + +/** + * Immutable, if all children are immutable. +*/ +class Immutable23ListAstNode extends TwoThreeListAstNode { + override toMutable(): ListAstNode { + return new TwoThreeListAstNode(this.length, this.listHeight, this.item1, this.item2, this.item3, this.missingOpeningBracketIds); + } + + protected override throwIfImmutable(): void { + throw new Error('this instance is immutable'); + } +} + +/** + * For debugging. +*/ +class ArrayListAstNode extends ListAstNode { + get childrenLength(): number { + return this._children.length; + } + getChild(idx: number): AstNode | null { + return this._children[idx]; + } + setChild(idx: number, child: AstNode): void { + this._children[idx] = child; + } + get children(): readonly AstNode[] { + return this._children; + } + + constructor( + length: Length, + listHeight: number, + private readonly _children: AstNode[], + missingOpeningBracketIds: SmallImmutableSet + ) { + super(length, listHeight, missingOpeningBracketIds); + } + + deepClone(): ListAstNode { + const children = new Array(this._children.length); + for (let i = 0; i < this._children.length; i++) { + children[i] = this._children[i].deepClone(); + } + return new ArrayListAstNode(this.length, this.listHeight, children, this.missingOpeningBracketIds); + } + + public appendChildOfSameHeight(node: AstNode): void { + this.throwIfImmutable(); + this._children.push(node); + this.handleChildrenChanged(); + } + + public unappendChild(): AstNode | undefined { + this.throwIfImmutable(); + const item = this._children.pop(); + this.handleChildrenChanged(); + return item; + } + + public prependChildOfSameHeight(node: AstNode): void { + this.throwIfImmutable(); + this._children.unshift(node); + this.handleChildrenChanged(); + } + + public unprependChild(): AstNode | undefined { + this.throwIfImmutable(); + const item = this._children.shift(); + this.handleChildrenChanged(); + return item; + } + + public override toMutable(): ListAstNode { + return this; + } +} + +/** + * Immutable, if all children are immutable. +*/ +class ImmutableArrayListAstNode extends ArrayListAstNode { + override toMutable(): ListAstNode { + return new ArrayListAstNode(this.length, this.listHeight, [...this.children], this.missingOpeningBracketIds); + } + + protected override throwIfImmutable(): void { + throw new Error('this instance is immutable'); + } +} + +const emptyArray: readonly AstNode[] = []; + +abstract class ImmutableLeafAstNode extends BaseAstNode { + public get listHeight() { + return 0; + } + public get childrenLength(): number { + return 0; + } + public getChild(idx: number): AstNode | null { + return null; + } + public get children(): readonly AstNode[] { + return emptyArray; + } + + public flattenLists(): this & AstNode { + return this as this & AstNode; + } + public deepClone(): this & AstNode { + return this as this & AstNode; + } +} + +export class TextAstNode extends ImmutableLeafAstNode { + public get kind(): AstNodeKind.Text { + return AstNodeKind.Text; + } + public get missingOpeningBracketIds(): SmallImmutableSet { + return SmallImmutableSet.getEmpty(); + } + + public canBeReused(_openedBracketIds: SmallImmutableSet) { + return true; + } + + public computeMinIndentation(offset: Length, textModel: ITextModel): number { + const start = lengthToObj(offset); + // Text ast nodes don't have partial indentation (ensured by the tokenizer). + // Thus, if this text node does not start at column 0, the first line cannot have any indentation at all. + const startLineNumber = (start.columnCount === 0 ? start.lineCount : start.lineCount + 1) + 1; + const endLineNumber = lengthGetLineCount(lengthAdd(offset, this.length)) + 1; + + let result = Number.MAX_SAFE_INTEGER; + + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + const firstNonWsColumn = textModel.getLineFirstNonWhitespaceColumn(lineNumber); + const lineContent = textModel.getLineContent(lineNumber); + if (firstNonWsColumn === 0) { + continue; + } + + const visibleColumn = CursorColumns.visibleColumnFromColumn(lineContent, firstNonWsColumn, textModel.getOptions().tabSize)!; + result = Math.min(result, visibleColumn); + } + + return result; + } +} + +export class BracketAstNode extends ImmutableLeafAstNode { + private static cacheByLength = new Map(); + + public static create(length: Length): BracketAstNode { + const lengthKey = lengthHash(length); + const cached = BracketAstNode.cacheByLength.get(lengthKey); + if (cached) { + return cached; + } + + const node = new BracketAstNode(length); + BracketAstNode.cacheByLength.set(lengthKey, node); + return node; + } + + public get kind(): AstNodeKind.Bracket { + return AstNodeKind.Bracket; + } + + public get missingOpeningBracketIds(): SmallImmutableSet { + return SmallImmutableSet.getEmpty(); + } + + private constructor(length: Length) { + super(length); + } + + public canBeReused(_openedBracketIds: SmallImmutableSet) { + // These nodes could be reused, + // but not in a general way. + // Their parent may be reused. + return false; + } + + public computeMinIndentation(offset: Length, textModel: ITextModel): number { + return Number.MAX_SAFE_INTEGER; + } +} + +export class InvalidBracketAstNode extends ImmutableLeafAstNode { + public get kind(): AstNodeKind.UnexpectedClosingBracket { + return AstNodeKind.UnexpectedClosingBracket; + } + + public readonly missingOpeningBracketIds: SmallImmutableSet; + + public constructor(closingBrackets: SmallImmutableSet, length: Length) { + super(length); + this.missingOpeningBracketIds = closingBrackets; + } + + public canBeReused(openedBracketIds: SmallImmutableSet) { + return !openedBracketIds.intersects(this.missingOpeningBracketIds); + } + + public computeMinIndentation(offset: Length, textModel: ITextModel): number { + return Number.MAX_SAFE_INTEGER; + } +} diff --git a/src/vs/editor/common/model/bracketPairColorizer/beforeEditPositionMapper.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/beforeEditPositionMapper.ts similarity index 100% rename from src/vs/editor/common/model/bracketPairColorizer/beforeEditPositionMapper.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/beforeEditPositionMapper.ts diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree.ts new file mode 100644 index 0000000000..2cd6d6ee06 --- /dev/null +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/bracketPairsTree.ts @@ -0,0 +1,231 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { BracketInfo, BracketPairWithMinIndentationInfo } from 'vs/editor/common/model/bracketPairs/bracketPairs'; +import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { AstNode, AstNodeKind } from './ast'; +import { TextEditInfo } from './beforeEditPositionMapper'; +import { LanguageAgnosticBracketTokens } from './brackets'; +import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './length'; +import { parseDocument } from './parser'; +import { DenseKeyProvider } from './smallImmutableSet'; +import { FastTokenizer, TextBufferTokenizer } from './tokenizer'; + +export class BracketPairsTree extends Disposable { + private readonly didChangeEmitter = new Emitter(); + + /* + There are two trees: + * The initial tree that has no token information and is used for performant initial bracket colorization. + * The tree that used token information to detect bracket pairs. + + To prevent flickering, we only switch from the initial tree to tree with token information + when tokenization completes. + Since the text can be edited while background tokenization is in progress, we need to update both trees. + */ + private initialAstWithoutTokens: AstNode | undefined; + private astWithTokens: AstNode | undefined; + + private readonly denseKeyProvider = new DenseKeyProvider(); + private readonly brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider, this.getLanguageConfiguration); + + public didLanguageChange(languageId: string): boolean { + return this.brackets.didLanguageChange(languageId); + } + + public readonly onDidChange = this.didChangeEmitter.event; + + public constructor( + private readonly textModel: TextModel, + private readonly getLanguageConfiguration: (languageId: string) => ResolvedLanguageConfiguration + ) { + super(); + + this._register(textModel.onBackgroundTokenizationStateChanged(() => { + if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { + const wasUndefined = this.initialAstWithoutTokens === undefined; + // Clear the initial tree as we can use the tree with token information now. + this.initialAstWithoutTokens = undefined; + if (!wasUndefined) { + this.didChangeEmitter.fire(); + } + } + })); + + this._register(textModel.onDidChangeTokens(({ ranges }) => { + const edits = ranges.map(r => + new TextEditInfo( + toLength(r.fromLineNumber - 1, 0), + toLength(r.toLineNumber, 0), + toLength(r.toLineNumber - r.fromLineNumber + 1, 0) + ) + ); + this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false); + if (!this.initialAstWithoutTokens) { + this.didChangeEmitter.fire(); + } + })); + + if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) { + // There are no token information yet + const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageId()); + const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets); + this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, true); + this.astWithTokens = this.initialAstWithoutTokens; + } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { + // Skip the initial ast, as there is no flickering. + // Directly create the tree with token information. + this.initialAstWithoutTokens = undefined; + this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined, false); + } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.InProgress) { + this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined, true); + this.astWithTokens = this.initialAstWithoutTokens; + } + } + + public handleContentChanged(change: IModelContentChangedEvent) { + const edits = change.changes.map(c => { + const range = Range.lift(c.range); + return new TextEditInfo( + positionToLength(range.getStartPosition()), + positionToLength(range.getEndPosition()), + lengthOfString(c.text) + ); + }).reverse(); + + this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false); + if (this.initialAstWithoutTokens) { + this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(edits, this.initialAstWithoutTokens, false); + } + } + + /** + * @pure (only if isPure = true) + */ + private parseDocumentFromTextBuffer(edits: TextEditInfo[], previousAst: AstNode | undefined, immutable: boolean): AstNode { + // Is much faster if `isPure = false`. + const isPure = false; + const previousAstClone = isPure ? previousAst?.deepClone() : previousAst; + const tokenizer = new TextBufferTokenizer(this.textModel, this.brackets); + const result = parseDocument(tokenizer, edits, previousAstClone, immutable); + return result; + } + + public getBracketsInRange(range: Range): BracketInfo[] { + const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1); + const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1); + const result = new Array(); + const node = this.initialAstWithoutTokens || this.astWithTokens!; + collectBrackets(node, lengthZero, node.length, startOffset, endOffset, result); + return result; + } + + public getBracketPairsInRange(range: Range, includeMinIndentation: boolean): BracketPairWithMinIndentationInfo[] { + const result = new Array(); + + const startLength = positionToLength(range.getStartPosition()); + const endLength = positionToLength(range.getEndPosition()); + + const node = this.initialAstWithoutTokens || this.astWithTokens!; + const context = new CollectBracketPairsContext(result, includeMinIndentation, this.textModel); + collectBracketPairs(node, lengthZero, node.length, startLength, endLength, context); + + return result; + } +} + +function collectBrackets(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, result: BracketInfo[], level: number = 0): void { + if (node.kind === AstNodeKind.Bracket) { + const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); + result.push(new BracketInfo(range, level - 1, false)); + } else if (node.kind === AstNodeKind.UnexpectedClosingBracket) { + const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); + result.push(new BracketInfo(range, level - 1, true)); + } else if (node.kind === AstNodeKind.List) { + for (const child of node.children) { + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { + collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); + } + nodeOffsetStart = nodeOffsetEnd; + } + } else if (node.kind === AstNodeKind.Pair) { + // Don't use node.children here to improve performance + level++; + + { + const child = node.openingBracket; + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { + collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); + } + nodeOffsetStart = nodeOffsetEnd; + } + + if (node.child) { + const child = node.child; + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { + collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); + } + nodeOffsetStart = nodeOffsetEnd; + } + if (node.closingBracket) { + const child = node.closingBracket; + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) { + collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level); + } + nodeOffsetStart = nodeOffsetEnd; + } + } +} + +class CollectBracketPairsContext { + constructor( + public readonly result: BracketPairWithMinIndentationInfo[], + public readonly includeMinIndentation: boolean, + public readonly textModel: ITextModel, + ) { + } +} + +function collectBracketPairs(node: AstNode, nodeOffset: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, context: CollectBracketPairsContext, level: number = 0) { + if (node.kind === AstNodeKind.Pair) { + const openingBracketEnd = lengthAdd(nodeOffset, node.openingBracket.length); + let minIndentation = -1; + if (context.includeMinIndentation) { + minIndentation = node.computeMinIndentation(nodeOffset, context.textModel); + } + + context.result.push(new BracketPairWithMinIndentationInfo( + lengthsToRange(nodeOffset, nodeOffsetEnd), + lengthsToRange(nodeOffset, openingBracketEnd), + node.closingBracket + ? lengthsToRange(lengthAdd(openingBracketEnd, node.child?.length || lengthZero), nodeOffsetEnd) + : undefined, + level, + minIndentation + )); + level++; + } + + let curOffset = nodeOffset; + for (const child of node.children) { + const childOffset = curOffset; + curOffset = lengthAdd(curOffset, child.length); + + if (lengthLessThanEqual(childOffset, endOffset) && lengthLessThanEqual(startOffset, curOffset)) { + collectBracketPairs(child, childOffset, curOffset, startOffset, endOffset, context, level); + } + } +} + diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairsTree/brackets.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/brackets.ts new file mode 100644 index 0000000000..5c50035129 --- /dev/null +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/brackets.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 { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { BracketAstNode } from './ast'; +import { toLength } from './length'; +import { DenseKeyProvider, identityKeyProvider, SmallImmutableSet } from './smallImmutableSet'; +import { OpeningBracketId, Token, TokenKind } from './tokenizer'; + +export class BracketTokens { + static createFromLanguage(configuration: ResolvedLanguageConfiguration, denseKeyProvider: DenseKeyProvider): BracketTokens { + function getId(languageId: string, openingText: string): OpeningBracketId { + return denseKeyProvider.getKey(`${languageId}:::${openingText}`); + } + + const brackets = configuration.characterPair.getColorizedBrackets(); + + const closingBrackets = new Map, first: OpeningBracketId }>(); + const openingBrackets = new Set(); + + for (const [openingText, closingText] of brackets) { + openingBrackets.add(openingText); + + let info = closingBrackets.get(closingText); + const openingTextId = getId(configuration.languageId, openingText); + if (!info) { + info = { openingBrackets: SmallImmutableSet.getEmpty(), first: openingTextId }; + closingBrackets.set(closingText, info); + } + info.openingBrackets = info.openingBrackets.add(openingTextId, identityKeyProvider); + } + + const map = new Map(); + + for (const [closingText, info] of closingBrackets) { + const length = toLength(0, closingText.length); + map.set(closingText, new Token( + length, + TokenKind.ClosingBracket, + info.first, + info.openingBrackets, + BracketAstNode.create(length) + )); + } + + for (const openingText of openingBrackets) { + const length = toLength(0, openingText.length); + const openingTextId = getId(configuration.languageId, openingText); + map.set(openingText, new Token( + length, + TokenKind.OpeningBracket, + openingTextId, + SmallImmutableSet.getEmpty().add(openingTextId, identityKeyProvider), + BracketAstNode.create(length) + )); + } + + return new BracketTokens(map); + } + + private hasRegExp = false; + private _regExpGlobal: RegExp | null = null; + + constructor( + private readonly map: Map + ) { } + + getRegExpStr(): string | null { + if (this.isEmpty) { + return null; + } else { + const keys = [...this.map.keys()]; + keys.sort(); + keys.reverse(); + return keys.map(k => prepareBracketForRegExp(k)).join('|'); + } + } + + /** + * Returns null if there is no such regexp (because there are no brackets). + */ + get regExpGlobal(): RegExp | null { + if (!this.hasRegExp) { + const regExpStr = this.getRegExpStr(); + this._regExpGlobal = regExpStr ? new RegExp(regExpStr, 'g') : null; + this.hasRegExp = true; + } + return this._regExpGlobal; + } + + getToken(value: string): Token | undefined { + return this.map.get(value); + } + + get isEmpty(): boolean { + return this.map.size === 0; + } +} + +function prepareBracketForRegExp(str: string): string { + const escaped = escapeRegExpCharacters(str); + // This bracket pair uses letters like e.g. "begin" - "end" (see https://github.com/microsoft/vscode/issues/132162) + const needsWordBoundaries = (/^[\w ]+$/.test(str)); + return (needsWordBoundaries ? `\\b${escaped}\\b` : escaped); +} + +export class LanguageAgnosticBracketTokens { + private readonly languageIdToBracketTokens = new Map(); + + constructor( + private readonly denseKeyProvider: DenseKeyProvider, + private readonly getLanguageConfiguration: (languageId: string) => ResolvedLanguageConfiguration, + ) { + } + + public didLanguageChange(languageId: string): boolean { + const existing = this.languageIdToBracketTokens.get(languageId); + if (!existing) { + return false; + } + const newRegExpStr = BracketTokens.createFromLanguage(this.getLanguageConfiguration(languageId), this.denseKeyProvider).getRegExpStr(); + return existing.getRegExpStr() !== newRegExpStr; + } + + getSingleLanguageBracketTokens(languageId: string): BracketTokens { + let singleLanguageBracketTokens = this.languageIdToBracketTokens.get(languageId); + if (!singleLanguageBracketTokens) { + singleLanguageBracketTokens = BracketTokens.createFromLanguage(this.getLanguageConfiguration(languageId), this.denseKeyProvider); + this.languageIdToBracketTokens.set(languageId, singleLanguageBracketTokens); + } + return singleLanguageBracketTokens; + } + + getToken(value: string, languageId: string): Token | undefined { + const singleLanguageBracketTokens = this.getSingleLanguageBracketTokens(languageId); + return singleLanguageBracketTokens.getToken(value); + } +} diff --git a/src/vs/editor/common/model/bracketPairs/bracketPairsTree/concat23Trees.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/concat23Trees.ts new file mode 100644 index 0000000000..d57dfba07b --- /dev/null +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/concat23Trees.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AstNode, AstNodeKind, ListAstNode } from './ast'; + +/** + * Concatenates a list of (2,3) AstNode's into a single (2,3) AstNode. + * This mutates the items of the input array! + * If all items have the same height, this method has runtime O(items.length). + * Otherwise, it has runtime O(items.length * max(log(items.length), items.max(i => i.height))). +*/ +export function concat23Trees(items: AstNode[]): AstNode | null { + if (items.length === 0) { + return null; + } + if (items.length === 1) { + return items[0]; + } + + let i = 0; + /** + * Reads nodes of same height and concatenates them to a single node. + */ + function readNode(): AstNode | null { + if (i >= items.length) { + return null; + } + const start = i; + const height = items[start].listHeight; + + i++; + while (i < items.length && items[i].listHeight === height) { + i++; + } + + if (i - start >= 2) { + return concat23TreesOfSameHeight(start === 0 && i === items.length ? items : items.slice(start, i), false); + } else { + return items[start]; + } + } + + // The items might not have the same height. + // We merge all items by using a binary concat operator. + let first = readNode()!; // There must be a first item + let second = readNode(); + if (!second) { + return first; + } + + for (let item = readNode(); item; item = readNode()) { + // Prefer concatenating smaller trees, as the runtime of concat depends on the tree height. + if (heightDiff(first, second) <= heightDiff(second, item)) { + first = concat(first, second); + second = item; + } else { + second = concat(second, item); + } + } + + const result = concat(first, second); + return result; +} + +export function concat23TreesOfSameHeight(items: AstNode[], createImmutableLists: boolean = false): AstNode | null { + if (items.length === 0) { + return null; + } + if (items.length === 1) { + return items[0]; + } + + let length = items.length; + // All trees have same height, just create parent nodes. + while (length > 3) { + const newLength = length >> 1; + for (let i = 0; i < newLength; i++) { + const j = i << 1; + items[i] = ListAstNode.create23(items[j], items[j + 1], j + 3 === length ? items[j + 2] : null, createImmutableLists); + } + length = newLength; + } + return ListAstNode.create23(items[0], items[1], length >= 3 ? items[2] : null, createImmutableLists); +} + +function heightDiff(node1: AstNode, node2: AstNode): number { + return Math.abs(node1.listHeight - node2.listHeight); +} + +function concat(node1: AstNode, node2: AstNode): AstNode { + if (node1.listHeight === node2.listHeight) { + return ListAstNode.create23(node1, node2, null, false); + } + else if (node1.listHeight > node2.listHeight) { + // node1 is the tree we want to insert into + return append(node1 as ListAstNode, node2); + } else { + return prepend(node2 as ListAstNode, node1); + } +} + +/** + * Appends the given node to the end of this (2,3) tree. + * Returns the new root. +*/ +function append(list: ListAstNode, nodeToAppend: AstNode): AstNode { + list = list.toMutable() as ListAstNode; + let curNode: AstNode = list; + const parents = new Array(); + let nodeToAppendOfCorrectHeight: AstNode | undefined; + while (true) { + // assert nodeToInsert.listHeight <= curNode.listHeight + if (nodeToAppend.listHeight === curNode.listHeight) { + nodeToAppendOfCorrectHeight = nodeToAppend; + break; + } + // assert 0 <= nodeToInsert.listHeight < curNode.listHeight + if (curNode.kind !== AstNodeKind.List) { + throw new Error('unexpected'); + } + parents.push(curNode); + // assert 2 <= curNode.childrenLength <= 3 + curNode = curNode.makeLastElementMutable()!; + } + // assert nodeToAppendOfCorrectHeight!.listHeight === curNode.listHeight + for (let i = parents.length - 1; i >= 0; i--) { + const parent = parents[i]; + if (nodeToAppendOfCorrectHeight) { + // Can we take the element? + if (parent.childrenLength >= 3) { + // assert parent.childrenLength === 3 && parent.listHeight === nodeToAppendOfCorrectHeight.listHeight + 1 + + // we need to split to maintain (2,3)-tree property. + // Send the third element + the new element to the parent. + nodeToAppendOfCorrectHeight = ListAstNode.create23(parent.unappendChild()!, nodeToAppendOfCorrectHeight, null, false); + } else { + parent.appendChildOfSameHeight(nodeToAppendOfCorrectHeight); + nodeToAppendOfCorrectHeight = undefined; + } + } else { + parent.handleChildrenChanged(); + } + } + if (nodeToAppendOfCorrectHeight) { + return ListAstNode.create23(list, nodeToAppendOfCorrectHeight, null, false); + } else { + return list; + } +} + +/** + * Prepends the given node to the end of this (2,3) tree. + * Returns the new root. +*/ +function prepend(list: ListAstNode, nodeToAppend: AstNode): AstNode { + list = list.toMutable() as ListAstNode; + let curNode: AstNode = list; + const parents = new Array(); + // assert nodeToInsert.listHeight <= curNode.listHeight + while (nodeToAppend.listHeight !== curNode.listHeight) { + // assert 0 <= nodeToInsert.listHeight < curNode.listHeight + if (curNode.kind !== AstNodeKind.List) { + throw new Error('unexpected'); + } + parents.push(curNode); + // assert 2 <= curNode.childrenFast.length <= 3 + curNode = curNode.makeFirstElementMutable()!; + } + let nodeToPrependOfCorrectHeight: AstNode | undefined = nodeToAppend; + // assert nodeToAppendOfCorrectHeight!.listHeight === curNode.listHeight + for (let i = parents.length - 1; i >= 0; i--) { + const parent = parents[i]; + if (nodeToPrependOfCorrectHeight) { + // Can we take the element? + if (parent.childrenLength >= 3) { + // assert parent.childrenLength === 3 && parent.listHeight === nodeToAppendOfCorrectHeight.listHeight + 1 + + // we need to split to maintain (2,3)-tree property. + // Send the third element + the new element to the parent. + nodeToPrependOfCorrectHeight = ListAstNode.create23(nodeToPrependOfCorrectHeight, parent.unprependChild()!, null, false); + } else { + parent.prependChildOfSameHeight(nodeToPrependOfCorrectHeight); + nodeToPrependOfCorrectHeight = undefined; + } + } else { + parent.handleChildrenChanged(); + } + } + if (nodeToPrependOfCorrectHeight) { + return ListAstNode.create23(nodeToPrependOfCorrectHeight, list, null, false); + } else { + return list; + } +} diff --git a/src/vs/editor/common/model/bracketPairColorizer/length.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/length.ts similarity index 99% rename from src/vs/editor/common/model/bracketPairColorizer/length.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/length.ts index 95e26fb4eb..e3e9800ec5 100644 --- a/src/vs/editor/common/model/bracketPairColorizer/length.ts +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/length.ts @@ -9,7 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; /** * Represents a non-negative length in terms of line and column count. - * Prefer using {@link Length}. + * Prefer using {@link Length} for performance reasons. */ export class LengthObj { public static zero = new LengthObj(0, 0); diff --git a/src/vs/editor/common/model/bracketPairColorizer/nodeReader.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/nodeReader.ts similarity index 81% rename from src/vs/editor/common/model/bracketPairColorizer/nodeReader.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/nodeReader.ts index 9a9a212aa6..ac7b7034b3 100644 --- a/src/vs/editor/common/model/bracketPairColorizer/nodeReader.ts +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/nodeReader.ts @@ -54,11 +54,12 @@ export class NodeReader { this.nextNodeAfterCurrent(); } else { // The reader is somewhere in the current node. - if (curNode.children.length > 0) { + const nextChildIdx = getNextChildIdx(curNode); + if (nextChildIdx !== -1) { // Go to the first child and repeat. - this.nextNodes.push(curNode.children[0]); + this.nextNodes.push(curNode.getChild(nextChildIdx)!); this.offsets.push(curNodeOffset); - this.idxs.push(0); + this.idxs.push(nextChildIdx); } else { // We don't have children this.nextNodeAfterCurrent(); @@ -70,16 +71,17 @@ export class NodeReader { this.nextNodeAfterCurrent(); return curNode; } else { + const nextChildIdx = getNextChildIdx(curNode); // look for shorter node - if (curNode.children.length === 0) { + if (nextChildIdx === -1) { // There is no shorter node. this.nextNodeAfterCurrent(); return undefined; } else { // Descend into first child & repeat. - this.nextNodes.push(curNode.children[0]); + this.nextNodes.push(curNode.getChild(nextChildIdx)!); this.offsets.push(curNodeOffset); - this.idxs.push(0); + this.idxs.push(nextChildIdx); } } } @@ -101,13 +103,12 @@ export class NodeReader { // Parent is not undefined, because idxs is not empty const parent = lastOrUndefined(this.nextNodes)!; + const nextChildIdx = getNextChildIdx(parent, this.idxs[this.idxs.length - 1]); - this.idxs[this.idxs.length - 1]++; - const parentIdx = this.idxs[this.idxs.length - 1]; - - if (parentIdx < parent.children.length) { - this.nextNodes.push(parent.children[parentIdx]); + if (nextChildIdx !== -1) { + this.nextNodes.push(parent.getChild(nextChildIdx)!); this.offsets.push(lengthAdd(currentOffset!, currentNode!.length)); + this.idxs[this.idxs.length - 1] = nextChildIdx; break; } else { this.idxs.pop(); @@ -118,6 +119,18 @@ export class NodeReader { } } +function getNextChildIdx(node: AstNode, curIdx: number = -1): number | -1 { + while (true) { + curIdx++; + if (curIdx >= node.childrenLength) { + return -1; + } + if (node.getChild(curIdx)) { + return curIdx; + } + } +} + function lastOrUndefined(arr: readonly T[]): T | undefined { return arr.length > 0 ? arr[arr.length - 1] : undefined; } diff --git a/src/vs/editor/common/model/bracketPairColorizer/parser.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/parser.ts similarity index 64% rename from src/vs/editor/common/model/bracketPairColorizer/parser.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/parser.ts index 1f529065ce..5f40715764 100644 --- a/src/vs/editor/common/model/bracketPairColorizer/parser.ts +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/parser.ts @@ -5,17 +5,23 @@ import { AstNode, AstNodeKind, BracketAstNode, InvalidBracketAstNode, ListAstNode, PairAstNode, TextAstNode } from './ast'; import { BeforeEditPositionMapper, TextEditInfo } from './beforeEditPositionMapper'; -import { DenseKeyProvider, SmallImmutableSet } from './smallImmutableSet'; -import { lengthGetLineCount, lengthIsZero, lengthLessThanEqual } from './length'; -import { concat23Trees } from './concat23Trees'; +import { SmallImmutableSet } from './smallImmutableSet'; +import { lengthIsZero, lengthLessThan } from './length'; +import { concat23Trees, concat23TreesOfSameHeight } from './concat23Trees'; import { NodeReader } from './nodeReader'; -import { Tokenizer, TokenKind } from './tokenizer'; +import { OpeningBracketId, Tokenizer, TokenKind } from './tokenizer'; -export function parseDocument(tokenizer: Tokenizer, edits: TextEditInfo[], oldNode: AstNode | undefined, denseKeyProvider: DenseKeyProvider): AstNode { - const parser = new Parser(tokenizer, edits, oldNode, denseKeyProvider); +/** + * Non incrementally built ASTs are immutable. +*/ +export function parseDocument(tokenizer: Tokenizer, edits: TextEditInfo[], oldNode: AstNode | undefined, createImmutableLists: boolean): AstNode { + const parser = new Parser(tokenizer, edits, oldNode, createImmutableLists); return parser.parseDocument(); } +/** + * Non incrementally built ASTs are immutable. +*/ class Parser { private readonly oldNodeReader?: NodeReader; private readonly positionMapper: BeforeEditPositionMapper; @@ -40,8 +46,12 @@ class Parser { private readonly tokenizer: Tokenizer, edits: TextEditInfo[], oldNode: AstNode | undefined, - private readonly denseKeyProvider: DenseKeyProvider, + private readonly createImmutableLists: boolean, ) { + if (oldNode && createImmutableLists) { + throw new Error('Not supported'); + } + this.oldNodeReader = oldNode ? new NodeReader(oldNode) : undefined; this.positionMapper = new BeforeEditPositionMapper(edits, tokenizer.length); } @@ -52,14 +62,14 @@ class Parser { let result = this.parseList(SmallImmutableSet.getEmpty()); if (!result) { - result = ListAstNode.create([]); + result = ListAstNode.getEmpty(); } return result; } private parseList( - expectedClosingCategories: SmallImmutableSet, + openedBracketIds: SmallImmutableSet, ): AstNode | null { const items = new Array(); @@ -68,36 +78,37 @@ class Parser { if ( !token || (token.kind === TokenKind.ClosingBracket && - expectedClosingCategories.has(token.category, this.denseKeyProvider)) + token.bracketIds.intersects(openedBracketIds)) ) { break; } - const child = this.parseChild(expectedClosingCategories); - if (child.kind === AstNodeKind.List && child.children.length === 0) { + const child = this.parseChild(openedBracketIds); + if (child.kind === AstNodeKind.List && child.childrenLength === 0) { continue; } items.push(child); } - const result = concat23Trees(items); + // When there is no oldNodeReader, all items are created from scratch and must have the same height. + const result = this.oldNodeReader ? concat23Trees(items) : concat23TreesOfSameHeight(items, this.createImmutableLists); return result; } private parseChild( - expectingClosingCategories: SmallImmutableSet, + openedBracketIds: SmallImmutableSet, ): AstNode { if (this.oldNodeReader) { const maxCacheableLength = this.positionMapper.getDistanceToNextChange(this.tokenizer.offset); if (!lengthIsZero(maxCacheableLength)) { const cachedNode = this.oldNodeReader.readLongestNodeAt(this.positionMapper.getOffsetBeforeChange(this.tokenizer.offset), curNode => { - if (!lengthLessThanEqual(curNode.length, maxCacheableLength)) { + if (!lengthLessThan(curNode.length, maxCacheableLength)) { + // Either the node contains edited text or touches edited text. + // In the latter case, brackets might have been extended (`end` -> `ending`), so even touching nodes cannot be reused. return false; } - - const endLineDidChange = lengthGetLineCount(curNode.length) === lengthGetLineCount(maxCacheableLength); - const canBeReused = curNode.canBeReused(expectingClosingCategories, endLineDidChange); + const canBeReused = curNode.canBeReused(openedBracketIds); return canBeReused; }); @@ -115,31 +126,29 @@ class Parser { switch (token.kind) { case TokenKind.ClosingBracket: - return new InvalidBracketAstNode(token.category, token.length, this.denseKeyProvider); + return new InvalidBracketAstNode(token.bracketIds, token.length); case TokenKind.Text: return token.astNode as TextAstNode; case TokenKind.OpeningBracket: - const set = expectingClosingCategories.add(token.category, this.denseKeyProvider); + const set = openedBracketIds.merge(token.bracketIds); const child = this.parseList(set); const nextToken = this.tokenizer.peek(); if ( nextToken && nextToken.kind === TokenKind.ClosingBracket && - nextToken.category === token.category + (nextToken.bracketId === token.bracketId || nextToken.bracketIds.intersects(token.bracketIds)) ) { this.tokenizer.read(); return PairAstNode.create( - token.category, token.astNode as BracketAstNode, child, nextToken.astNode as BracketAstNode ); } else { return PairAstNode.create( - token.category, token.astNode as BracketAstNode, child, null diff --git a/src/vs/editor/common/model/bracketPairColorizer/smallImmutableSet.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet.ts similarity index 84% rename from src/vs/editor/common/model/bracketPairColorizer/smallImmutableSet.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet.ts index cb4bbec9b7..17c2183d64 100644 --- a/src/vs/editor/common/model/bracketPairColorizer/smallImmutableSet.ts +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet.ts @@ -37,7 +37,7 @@ export class SmallImmutableSet { ) { } - public add(value: T, keyProvider: DenseKeyProvider): SmallImmutableSet { + public add(value: T, keyProvider: IDenseKeyProvider): SmallImmutableSet { const key = keyProvider.getKey(value); let idx = key >> 5; // divided by 32 if (idx === 0) { @@ -59,7 +59,7 @@ export class SmallImmutableSet { return SmallImmutableSet.create(this.items, newItems); } - public has(value: T, keyProvider: DenseKeyProvider): boolean { + public has(value: T, keyProvider: IDenseKeyProvider): boolean { const key = keyProvider.getKey(value); let idx = key >> 5; // divided by 32 if (idx === 0) { @@ -129,6 +129,16 @@ export class SmallImmutableSet { } } +export interface IDenseKeyProvider { + getKey(value: T): number; +} + +export const identityKeyProvider: IDenseKeyProvider = { + getKey(value: number) { + return value; + } +}; + /** * Assigns values a unique incrementing key. */ @@ -143,4 +153,22 @@ export class DenseKeyProvider { } return existing; } + + reverseLookup(value: number): T | undefined { + return [...this.items].find(([_key, v]) => v === value)?.[0]; + } + + reverseLookupSet(set: SmallImmutableSet): T[] { + const result: T[] = []; + for (const [key] of this.items) { + if (set.has(key, this)) { + result.push(key); + } + } + return result; + } + + keys(): IterableIterator { + return this.items.keys(); + } } diff --git a/src/vs/editor/common/model/bracketPairColorizer/tokenizer.ts b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer.ts similarity index 81% rename from src/vs/editor/common/model/bracketPairColorizer/tokenizer.ts rename to src/vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer.ts index 3723e03d7f..3391de5ae0 100644 --- a/src/vs/editor/common/model/bracketPairColorizer/tokenizer.ts +++ b/src/vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer.ts @@ -6,7 +6,8 @@ import { NotSupportedError } from 'vs/base/common/errors'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { ITextModel } from 'vs/editor/common/model'; -import { LanguageId, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; +import { SmallImmutableSet } from './smallImmutableSet'; +import { StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; import { BracketAstNode, TextAstNode } from './ast'; import { BracketTokens, LanguageAgnosticBracketTokens } from './brackets'; import { lengthGetColumnCountIfZeroLineCount, Length, lengthAdd, lengthDiff, lengthToObj, lengthZero, toLength } from './length'; @@ -28,12 +29,24 @@ export const enum TokenKind { ClosingBracket = 2, } +export type OpeningBracketId = number; + export class Token { constructor( readonly length: Length, readonly kind: TokenKind, - readonly category: number, - readonly languageId: LanguageId, + /** + * If this token is an opening bracket, this is the id of the opening bracket. + * If this token is a closing bracket, this is the id of the first opening bracket that is closed by this bracket. + * Otherwise, it is -1. + */ + readonly bracketId: OpeningBracketId, + /** + * If this token is an opening bracket, this just contains `bracketId`. + * If this token is a closing bracket, this lists all opening bracket ids, that it closes. + * Otherwise, it is empty. + */ + readonly bracketIds: SmallImmutableSet, readonly astNode: BracketAstNode | TextAstNode | undefined, ) { } } @@ -158,14 +171,14 @@ class NonPeekableTextBufferTokenizer { // limits the length of text tokens. // If text tokens get too long, incremental updates will be slow let lengthHeuristic = 0; - while (lengthHeuristic < 1000) { + while (true) { const lineTokens = this.lineTokens!; const tokenCount = lineTokens.getCount(); let peekedBracketToken: Token | null = null; if (this.lineTokenOffset < tokenCount) { - let tokenMetadata = lineTokens.getMetadata(this.lineTokenOffset); + const tokenMetadata = lineTokens.getMetadata(this.lineTokenOffset); while (this.lineTokenOffset + 1 < tokenCount && tokenMetadata === lineTokens.getMetadata(this.lineTokenOffset + 1)) { // Skip tokens that are identical. // Sometimes, (bracket) identifiers are split up into multiple tokens. @@ -224,12 +237,29 @@ class NonPeekableTextBufferTokenizer { this.line = this.lineTokens.getLineContent(); this.lineCharOffset = 0; - lengthHeuristic++; + lengthHeuristic += 33; // max 1000/33 = 30 lines + // This limits the amount of work to recompute min-indentation + + if (lengthHeuristic > 1000) { + // only break (automatically) at the end of line. + break; + } + } + + if (lengthHeuristic > 1500) { + // Eventually break regardless of the line length so that + // very long lines do not cause bad performance. + // This effective limits max indentation to 500, as + // indentation is not computed across multiple text nodes. + break; } } + // If a token contains some proper indentation, it also contains \n{INDENTATION+}(?!{INDENTATION}), + // unless the line is too long. + // Thus, the min indentation of the document is the minimum min indentation of every text node. const length = lengthDiff(startLineIdx, startLineCharOffset, this.lineIdx, this.lineCharOffset); - return new Token(length, TokenKind.Text, -1, -1, new TextAstNode(length)); + return new Token(length, TokenKind.Text, -1, SmallImmutableSet.getEmpty(), new TextAstNode(length)); } } @@ -255,7 +285,7 @@ export class FastTokenizer implements Tokenizer { for (let i = 0; i < 60; i++) { smallTextTokens0Line.push( new Token( - toLength(0, i), TokenKind.Text, -1, -1, + toLength(0, i), TokenKind.Text, -1, SmallImmutableSet.getEmpty(), new TextAstNode(toLength(0, i)) ) ); @@ -265,7 +295,7 @@ export class FastTokenizer implements Tokenizer { for (let i = 0; i < 60; i++) { smallTextTokens1Line.push( new Token( - toLength(1, i), TokenKind.Text, -1, -1, + toLength(1, i), TokenKind.Text, -1, SmallImmutableSet.getEmpty(), new TextAstNode(toLength(1, i)) ) ); @@ -273,6 +303,7 @@ export class FastTokenizer implements Tokenizer { if (regexp) { regexp.lastIndex = 0; + // If a token contains indentation, it also contains \n{INDENTATION+}(?!{INDENTATION}) while ((match = regexp.exec(text)) !== null) { const curOffset = match.index; const value = match[0]; @@ -288,7 +319,7 @@ export class FastTokenizer implements Tokenizer { token = smallTextTokens0Line[colCount]; } else { const length = toLength(0, colCount); - token = new Token(length, TokenKind.Text, -1, -1, new TextAstNode(length)); + token = new Token(length, TokenKind.Text, -1, SmallImmutableSet.getEmpty(), new TextAstNode(length)); } } else { const lineCount = curLineCount - lastTokenEndLine; @@ -297,7 +328,7 @@ export class FastTokenizer implements Tokenizer { token = smallTextTokens1Line[colCount]; } else { const length = toLength(lineCount, colCount); - token = new Token(length, TokenKind.Text, -1, -1, new TextAstNode(length)); + token = new Token(length, TokenKind.Text, -1, SmallImmutableSet.getEmpty(), new TextAstNode(length)); } } tokens.push(token); @@ -318,7 +349,7 @@ export class FastTokenizer implements Tokenizer { const length = (lastTokenEndLine === curLineCount) ? toLength(0, offset - lastTokenEndOffset) : toLength(curLineCount - lastTokenEndLine, offset - lastLineBreakOffset); - tokens.push(new Token(length, TokenKind.Text, -1, -1, new TextAstNode(length))); + tokens.push(new Token(length, TokenKind.Text, -1, SmallImmutableSet.getEmpty(), new TextAstNode(length))); } this.length = toLength(curLineCount, offset - lastLineBreakOffset); diff --git a/src/vs/editor/common/model/bracketPairs/colorizedBracketPairsDecorationProvider.ts b/src/vs/editor/common/model/bracketPairs/colorizedBracketPairsDecorationProvider.ts new file mode 100644 index 0000000000..ed3f947d79 --- /dev/null +++ b/src/vs/editor/common/model/bracketPairs/colorizedBracketPairsDecorationProvider.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Color } from 'vs/base/common/color'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/editor/common/core/range'; +import { BracketPairColorizationOptions, IModelDecoration } from 'vs/editor/common/model'; +import { BracketInfo } from 'vs/editor/common/model/bracketPairs/bracketPairs'; +import { DecorationProvider } from 'vs/editor/common/model/decorationProvider'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { + editorBracketHighlightingForeground1, editorBracketHighlightingForeground2, editorBracketHighlightingForeground3, editorBracketHighlightingForeground4, editorBracketHighlightingForeground5, editorBracketHighlightingForeground6, editorBracketHighlightingUnexpectedBracketForeground +} from 'vs/editor/common/view/editorColorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; + +export class ColorizedBracketPairsDecorationProvider extends Disposable implements DecorationProvider { + private colorizationOptions: BracketPairColorizationOptions; + private readonly colorProvider = new ColorProvider(); + + private readonly onDidChangeEmitter = new Emitter(); + public readonly onDidChange = this.onDidChangeEmitter.event; + + constructor(private readonly textModel: TextModel) { + super(); + + this.colorizationOptions = textModel.getOptions().bracketPairColorizationOptions; + + this._register(textModel.onDidChangeOptions(e => { + this.colorizationOptions = textModel.getOptions().bracketPairColorizationOptions; + })); + + this._register(textModel.bracketPairs.onDidChange(e => { + this.onDidChangeEmitter.fire(); + })); + } + + getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { + if (ownerId === undefined) { + return []; + } + if (!this.colorizationOptions.enabled) { + return []; + } + + const result = new Array(); + const bracketsInRange = this.textModel.bracketPairs.getBracketsInRange(range); + for (const bracket of bracketsInRange) { + result.push({ + id: `bracket${bracket.range.toString()}-${bracket.nestingLevel}`, + options: { description: 'BracketPairColorization', inlineClassName: this.colorProvider.getInlineClassName(bracket) }, + ownerId: 0, + range: bracket.range + }); + } + return result; + } + + getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { + if (ownerId === undefined) { + return []; + } + if (!this.colorizationOptions.enabled) { + return []; + } + return this.getDecorationsInRange( + new Range(1, 1, this.textModel.getLineCount(), 1), + ownerId, + filterOutValidation + ); + } +} + +class ColorProvider { + public readonly unexpectedClosingBracketClassName = 'unexpected-closing-bracket'; + + getInlineClassName(bracket: BracketInfo): string { + if (bracket.isInvalid) { + return this.unexpectedClosingBracketClassName; + } + return this.getInlineClassNameOfLevel(bracket.nestingLevel); + } + + getInlineClassNameOfLevel(level: number): string { + // To support a dynamic amount of colors up to 6 colors, + // we use a number that is a lcm of all numbers from 1 to 6. + return `bracket-highlighting-${level % 30}`; + } +} + +registerThemingParticipant((theme, collector) => { + const colors = [ + editorBracketHighlightingForeground1, + editorBracketHighlightingForeground2, + editorBracketHighlightingForeground3, + editorBracketHighlightingForeground4, + editorBracketHighlightingForeground5, + editorBracketHighlightingForeground6 + ]; + const colorProvider = new ColorProvider(); + + collector.addRule(`.monaco-editor .${colorProvider.unexpectedClosingBracketClassName} { color: ${theme.getColor(editorBracketHighlightingUnexpectedBracketForeground)}; }`); + + let colorValues = colors + .map(c => theme.getColor(c)) + .filter((c): c is Color => !!c) + .filter(c => !c.isTransparent()); + + for (let level = 0; level < 30; level++) { + const color = colorValues[level % colorValues.length]; + collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level)} { color: ${color}; }`); + } +}); diff --git a/src/vs/editor/common/model/decorationProvider.ts b/src/vs/editor/common/model/decorationProvider.ts index 3ea4d66692..4ea198052e 100644 --- a/src/vs/editor/common/model/decorationProvider.ts +++ b/src/vs/editor/common/model/decorationProvider.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecoration } from 'vs/editor/common/model'; @@ -25,5 +25,5 @@ export interface DecorationProvider { */ getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; - onDidChangeDecorations(listener: () => void): IDisposable; + onDidChange: Event; } diff --git a/src/vs/editor/common/model/mirrorTextModel.ts b/src/vs/editor/common/model/mirrorTextModel.ts index ea61d99f0a..487fed9035 100644 --- a/src/vs/editor/common/model/mirrorTextModel.ts +++ b/src/vs/editor/common/model/mirrorTextModel.ts @@ -106,7 +106,7 @@ export class MirrorTextModel implements IMirrorTextModel { this._lines[lineIndex] = newValue; if (this._lineStarts) { // update prefix sum - this._lineStarts.changeValue(lineIndex, this._lines[lineIndex].length + this._eol.length); + this._lineStarts.setValue(lineIndex, this._lines[lineIndex].length + this._eol.length); } } diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts index e3417c3190..0bd0680430 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase.ts @@ -394,26 +394,25 @@ export function recomputeTreeMetadata(tree: PieceTreeBase, x: TreeNode) { return; } - if (delta === 0) { - // go upwards till the node whose left subtree is changed. - while (x !== tree.root && x === x.parent.right) { - x = x.parent; - } - - if (x === tree.root) { - // well, it means we add a node to the end (inorder) - return; - } - - // x is the node whose right subtree is changed. + // go upwards till the node whose left subtree is changed. + while (x !== tree.root && x === x.parent.right) { x = x.parent; - - delta = calculateSize(x.left) - x.size_left; - lf_delta = calculateLF(x.left) - x.lf_left; - x.size_left += delta; - x.lf_left += lf_delta; } + if (x === tree.root) { + // well, it means we add a node to the end (inorder) + return; + } + + // x is the node whose right subtree is changed. + x = x.parent; + + delta = calculateSize(x.left) - x.size_left; + lf_delta = calculateLF(x.left) - x.lf_left; + x.size_left += delta; + x.lf_left += lf_delta; + + // go upwards till root. O(logN) while (x !== tree.root && (delta !== 0 || lf_delta !== 0)) { if (x.parent.left === x) { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 90a5725142..b97226adb2 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -24,11 +24,9 @@ import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguag import { SearchData, SearchParams, TextModelSearch } from 'vs/editor/common/model/textModelSearch'; import { TextModelTokenization } from 'vs/editor/common/model/textModelTokens'; import { getWordAtText } from 'vs/editor/common/model/wordHelper'; -import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { NULL_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/nullMode'; -import { ignoreBracketsInToken } from 'vs/editor/common/modes/supports'; -import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets'; +import { FormattingOptions } from 'vs/editor/common/modes'; +import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { NULL_MODE_ID } from 'vs/editor/common/modes/nullMode'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore'; @@ -39,9 +37,13 @@ import { TextChange } from 'vs/editor/common/model/textChange'; import { Constants } from 'vs/base/common/uint'; import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; import { listenStream } from 'vs/base/common/stream'; -import { ArrayQueue } from 'vs/base/common/arrays'; -import { BracketPairColorizer } from 'vs/editor/common/model/bracketPairColorizer/bracketPairColorizer'; +import { ArrayQueue, findLast } from 'vs/base/common/arrays'; +import { BracketPairInfo, IBracketPairs } from 'vs/editor/common/model/bracketPairs/bracketPairs'; +import { BracketPairs } from 'vs/editor/common/model/bracketPairs/bracketPairsImpl'; +import { ColorizedBracketPairsDecorationProvider } from 'vs/editor/common/model/bracketPairs/colorizedBracketPairsDecorationProvider'; import { DecorationProvider } from 'vs/editor/common/model/decorationProvider'; +import { CursorColumns } from 'vs/editor/common/controller/cursorColumns'; +import { IModeService } from 'vs/editor/common/services/modeService'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -170,21 +172,6 @@ export const enum BackgroundTokenizationState { Completed = 2, } -type ContinueBracketSearchPredicate = null | (() => boolean); - -class BracketSearchCanceled { - public static INSTANCE = new BracketSearchCanceled(); - _searchCanceledBrand = undefined; - private constructor() { } -} - -function stripBracketSearchCanceled(result: T | null | BracketSearchCanceled): T | null { - if (result instanceof BracketSearchCanceled) { - return null; - } - return result; -} - export class TextModel extends Disposable implements model.ITextModel, IDecorationsTreesHost { private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB @@ -267,7 +254,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public readonly id: string; public readonly isForSimpleWidget: boolean; private readonly _associatedResource: URI; - private readonly _undoRedoService: IUndoRedoService; private _attachedEditorCount: number; private _buffer: model.ITextBuffer; private _bufferDisposable: IDisposable; @@ -304,20 +290,26 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati //#endregion //#region Tokenization - private _languageIdentifier: LanguageIdentifier; + private _languageId: string; private readonly _languageRegistryListener: IDisposable; private readonly _tokens: TokensStore; private readonly _tokens2: TokensStore2; private readonly _tokenization: TextModelTokenization; //#endregion - private readonly _bracketPairColorizer; + private readonly _bracketPairColorizer: BracketPairs; + public get bracketPairs(): IBracketPairs { return this._bracketPairColorizer; } private _backgroundTokenizationState = BackgroundTokenizationState.Uninitialized; public get backgroundTokenizationState(): BackgroundTokenizationState { return this._backgroundTokenizationState; } - private setBackgroundTokenizationState(newState: BackgroundTokenizationState) { + private handleTokenizationProgress(completed: boolean) { + if (this._backgroundTokenizationState === BackgroundTokenizationState.Completed) { + // We already did a full tokenization and don't go back to progressing. + return; + } + const newState = completed ? BackgroundTokenizationState.Completed : BackgroundTokenizationState.InProgress; if (this._backgroundTokenizationState !== newState) { this._backgroundTokenizationState = newState; this._onBackgroundTokenizationStateChanged.fire(); @@ -330,9 +322,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati constructor( source: string | model.ITextBufferFactory, creationOptions: model.ITextModelCreationOptions, - languageIdentifier: LanguageIdentifier | null, + languageId: string | null, associatedResource: URI | null = null, - undoRedoService: IUndoRedoService + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + @IModeService private readonly _modeService: IModeService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(); @@ -349,7 +343,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } else { this._associatedResource = associatedResource; } - this._undoRedoService = undoRedoService; this._attachedEditorCount = 0; const { textBuffer, disposable } = createTextBuffer(source, creationOptions.defaultEOL); @@ -382,32 +375,34 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._isDisposed = false; this._isDisposing = false; - this._languageIdentifier = languageIdentifier || NULL_LANGUAGE_IDENTIFIER; + this._languageId = languageId || NULL_MODE_ID; - this._languageRegistryListener = LanguageConfigurationRegistry.onDidChange((e) => { - if (e.languageIdentifier.id === this._languageIdentifier.id) { - this._onDidChangeLanguageConfiguration.fire({}); + this._languageRegistryListener = this._languageConfigurationService.onDidChange( + e => { + if (e.affects(this._languageId)) { + this._onDidChangeLanguageConfiguration.fire({}); + } } - }); + ); this._instanceId = strings.singleLetterHash(MODEL_ID); this._lastDecorationId = 0; this._decorations = Object.create(null); this._decorationsTree = new DecorationsTrees(); - this._commandManager = new EditStack(this, undoRedoService); + this._commandManager = new EditStack(this, this._undoRedoService); this._isUndoing = false; this._isRedoing = false; this._trimAutoWhitespaceLines = null; - this._tokens = new TokensStore(); - this._tokens2 = new TokensStore2(); - this._tokenization = new TextModelTokenization(this); + this._tokens = new TokensStore(this._modeService.languageIdCodec); + this._tokens2 = new TokensStore2(this._modeService.languageIdCodec); + this._tokenization = new TextModelTokenization(this, this._modeService.languageIdCodec); - this._bracketPairColorizer = this._register(new BracketPairColorizer(this)); - this._decorationProvider = this._bracketPairColorizer; + this._bracketPairColorizer = this._register(new BracketPairs(this, this._languageConfigurationService)); + this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this)); - this._register(this._decorationProvider.onDidChangeDecorations(() => { + this._register(this._decorationProvider.onDidChange(() => { this._onDidChangeDecorations.beginDeferredEmit(); this._onDidChangeDecorations.fire(); this._onDidChangeDecorations.endDeferredEmit(); @@ -1793,8 +1788,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] { - const result = this._decorationsTree.getAll(this, ownerId, filterOutValidation, false); - result.push(...this._decorationProvider.getAllDecorations(ownerId, filterOutValidation)); + let result = this._decorationsTree.getAll(this, ownerId, filterOutValidation, false); + result = result.concat(this._decorationProvider.getAllDecorations(ownerId, filterOutValidation)); return result; } @@ -1961,7 +1956,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati throw new Error('Illegal value for lineNumber'); } - this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens, false); + this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens, false); } public setTokens(tokens: MultilineTokens[], backgroundTokenizationCompleted: boolean = false): void { @@ -1976,10 +1971,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati for (let j = 0, lenJ = element.tokens.length; j < lenJ; j++) { const lineNumber = element.startLineNumber + j; if (hasChange) { - this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], false); + this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], false); maxChangedLineNumber = lineNumber; } else { - const lineHasChange = this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], true); + const lineHasChange = this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], true); if (lineHasChange) { hasChange = true; minChangedLineNumber = lineNumber; @@ -2000,7 +1995,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati }); } } - this.setBackgroundTokenizationState(backgroundTokenizationCompleted ? BackgroundTokenizationState.Completed : BackgroundTokenizationState.InProgress); + this.handleTokenizationProgress(backgroundTokenizationCompleted); } public setSemanticTokens(tokens: MultilineTokens2[] | null, isComplete: boolean): void { @@ -2100,41 +2095,41 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _getLineTokens(lineNumber: number): LineTokens { const lineText = this.getLineContent(lineNumber); - const syntacticTokens = this._tokens.getTokens(this._languageIdentifier.id, lineNumber - 1, lineText); + const syntacticTokens = this._tokens.getTokens(this._languageId, lineNumber - 1, lineText); return this._tokens2.addSemanticTokens(lineNumber, syntacticTokens); } - public getLanguageIdentifier(): LanguageIdentifier { - return this._languageIdentifier; + public getLanguageId(): string { + return this._languageId; } - public getModeId(): string { - return this._languageIdentifier.language; - } - - public setMode(languageIdentifier: LanguageIdentifier): void { - if (this._languageIdentifier.id === languageIdentifier.id) { + public setMode(languageId: string): void { + if (this._languageId === languageId) { // There's nothing to do return; } let e: IModelLanguageChangedEvent = { - oldLanguage: this._languageIdentifier.language, - newLanguage: languageIdentifier.language + oldLanguage: this._languageId, + newLanguage: languageId }; - this._languageIdentifier = languageIdentifier; + this._languageId = languageId; this._onDidChangeLanguage.fire(e); this._onDidChangeLanguageConfiguration.fire({}); } - public getLanguageIdAtPosition(lineNumber: number, column: number): LanguageId { + public getLanguageIdAtPosition(lineNumber: number, column: number): string { const position = this.validatePosition(new Position(lineNumber, column)); const lineTokens = this.getLineTokens(position.lineNumber); return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); } + private getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration { + return this._languageConfigurationService.getLanguageConfiguration(languageId); + } + // Having tokens allows implementing additional helper methods public getWordAtPosition(_position: IPosition): model.IWordAtPosition | null { @@ -2148,7 +2143,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const [rbStartOffset, rbEndOffset] = TextModel._findLanguageBoundaries(lineTokens, tokenIndex); const rightBiasedWord = getWordAtText( position.column, - LanguageConfigurationRegistry.getWordDefinition(lineTokens.getLanguageId(tokenIndex)), + this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).getWordDefinition(), lineContent.substring(rbStartOffset, rbEndOffset), rbStartOffset ); @@ -2163,7 +2158,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const [lbStartOffset, lbEndOffset] = TextModel._findLanguageBoundaries(lineTokens, tokenIndex - 1); const leftBiasedWord = getWordAtText( position.column, - LanguageConfigurationRegistry.getWordDefinition(lineTokens.getLanguageId(tokenIndex - 1)), + this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex - 1)).getWordDefinition(), lineContent.substring(lbStartOffset, lbEndOffset), lbStartOffset ); @@ -2210,642 +2205,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati }; } - public findMatchingBracketUp(_bracket: string, _position: IPosition): Range | null { - let bracket = _bracket.toLowerCase(); - let position = this.validatePosition(_position); - - let lineTokens = this._getLineTokens(position.lineNumber); - let languageId = lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); - let bracketsSupport = LanguageConfigurationRegistry.getBracketsSupport(languageId); - - if (!bracketsSupport) { - return null; - } - - let data = bracketsSupport.textIsBracket[bracket]; - - if (!data) { - return null; - } - - return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, null)); - } - - public matchBracket(position: IPosition): [Range, Range] | null { - return this._matchBracket(this.validatePosition(position)); - } - - private _establishBracketSearchOffsets(position: Position, lineTokens: LineTokens, modeBrackets: RichEditBrackets, tokenIndex: number) { - const tokenCount = lineTokens.getCount(); - const currentLanguageId = lineTokens.getLanguageId(tokenIndex); - - // limit search to not go before `maxBracketLength` - let searchStartOffset = Math.max(0, position.column - 1 - modeBrackets.maxBracketLength); - for (let i = tokenIndex - 1; i >= 0; i--) { - const tokenEndOffset = lineTokens.getEndOffset(i); - if (tokenEndOffset <= searchStartOffset) { - break; - } - if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) { - searchStartOffset = tokenEndOffset; - break; - } - } - - // limit search to not go after `maxBracketLength` - let searchEndOffset = Math.min(lineTokens.getLineContent().length, position.column - 1 + modeBrackets.maxBracketLength); - for (let i = tokenIndex + 1; i < tokenCount; i++) { - const tokenStartOffset = lineTokens.getStartOffset(i); - if (tokenStartOffset >= searchEndOffset) { - break; - } - if (ignoreBracketsInToken(lineTokens.getStandardTokenType(i)) || lineTokens.getLanguageId(i) !== currentLanguageId) { - searchEndOffset = tokenStartOffset; - break; - } - } - - return { searchStartOffset, searchEndOffset }; - } - - private _matchBracket(position: Position): [Range, Range] | null { - const lineNumber = position.lineNumber; - const lineTokens = this._getLineTokens(lineNumber); - const lineText = this._buffer.getLineContent(lineNumber); - - const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - if (tokenIndex < 0) { - return null; - } - const currentModeBrackets = LanguageConfigurationRegistry.getBracketsSupport(lineTokens.getLanguageId(tokenIndex)); - - // check that the token is not to be ignored - if (currentModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))) { - - let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, currentModeBrackets, tokenIndex); - - // it might be the case that [currentTokenStart -> currentTokenEnd] contains multiple brackets - // `bestResult` will contain the most right-side result - let bestResult: [Range, Range] | null = null; - while (true) { - const foundBracket = BracketsUtils.findNextBracketInRange(currentModeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (!foundBracket) { - // there are no more brackets in this text - break; - } - - // check that we didn't hit a bracket too far away from position - if (foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { - const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); - const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText], null); - if (r) { - if (r instanceof BracketSearchCanceled) { - return null; - } - bestResult = r; - } - } - - searchStartOffset = foundBracket.endColumn - 1; - } - - if (bestResult) { - return bestResult; - } - } - - // If position is in between two tokens, try also looking in the previous token - if (tokenIndex > 0 && lineTokens.getStartOffset(tokenIndex) === position.column - 1) { - const prevTokenIndex = tokenIndex - 1; - const prevModeBrackets = LanguageConfigurationRegistry.getBracketsSupport(lineTokens.getLanguageId(prevTokenIndex)); - - // check that previous token is not to be ignored - if (prevModeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(prevTokenIndex))) { - - let { searchStartOffset, searchEndOffset } = this._establishBracketSearchOffsets(position, lineTokens, prevModeBrackets, prevTokenIndex); - - const foundBracket = BracketsUtils.findPrevBracketInRange(prevModeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - - // check that we didn't hit a bracket too far away from position - if (foundBracket && foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { - const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); - const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText], null); - if (r) { - if (r instanceof BracketSearchCanceled) { - return null; - } - return r; - } - } - } - } - - return null; - } - - private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null | BracketSearchCanceled { - if (!data) { - return null; - } - - const matched = ( - isOpen - ? this._findMatchingBracketDown(data, foundBracket.getEndPosition(), continueSearchPredicate) - : this._findMatchingBracketUp(data, foundBracket.getStartPosition(), continueSearchPredicate) - ); - - if (!matched) { - return null; - } - - if (matched instanceof BracketSearchCanceled) { - return matched; - } - - return [foundBracket, matched]; - } - - private _findMatchingBracketUp(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { - // console.log('_findMatchingBracketUp: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); - - const languageId = bracket.languageIdentifier.id; - const reversedBracketRegex = bracket.reversedRegex; - let count = -1; - - let totalCallCount = 0; - const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { - while (true) { - if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { - return BracketSearchCanceled.INSTANCE; - } - const r = BracketsUtils.findPrevBracketInRange(reversedBracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (!r) { - break; - } - - const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); - if (bracket.isOpen(hitText)) { - count++; - } else if (bracket.isClose(hitText)) { - count--; - } - - if (count === 0) { - return r; - } - - searchEndOffset = r.startColumn - 1; - } - - return null; - }; - - for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = tokenCount - 1; - let searchStartOffset = lineText.length; - let searchEndOffset = lineText.length; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - } - - let prevSearchInToken = true; - for (; tokenIndex >= 0; tokenIndex--) { - const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchStartOffset - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return r; - } - } - } - - prevSearchInToken = searchInToken; - } - - if (prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchPrevMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return r; - } - } - } - - return null; - } - - private _findMatchingBracketDown(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { - // console.log('_findMatchingBracketDown: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); - - const languageId = bracket.languageIdentifier.id; - const bracketRegex = bracket.forwardRegex; - let count = 1; - - let totalCallCount = 0; - const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { - while (true) { - if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { - return BracketSearchCanceled.INSTANCE; - } - const r = BracketsUtils.findNextBracketInRange(bracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (!r) { - break; - } - - const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); - if (bracket.isOpen(hitText)) { - count++; - } else if (bracket.isClose(hitText)) { - count--; - } - - if (count === 0) { - return r; - } - - searchStartOffset = r.endColumn - 1; - } - - return null; - }; - - const lineCount = this.getLineCount(); - for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = 0; - let searchStartOffset = 0; - let searchEndOffset = 0; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - } - - let prevSearchInToken = true; - for (; tokenIndex < tokenCount; tokenIndex++) { - const searchInToken = (lineTokens.getLanguageId(tokenIndex) === languageId && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchEndOffset - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return r; - } - } - } - - prevSearchInToken = searchInToken; - } - - if (prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchNextMatchingBracketInRange(lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return r; - } - } - } - - return null; - } - - public findPrevBracket(_position: IPosition): model.IFoundBracket | null { - const position = this.validatePosition(_position); - - let languageId: LanguageId = -1; - let modeBrackets: RichEditBrackets | null = null; - for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = tokenCount - 1; - let searchStartOffset = lineText.length; - let searchEndOffset = lineText.length; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - if (languageId !== tokenLanguageId) { - languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); - } - } - - let prevSearchInToken = true; - for (; tokenIndex >= 0; tokenIndex--) { - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - - if (languageId !== tokenLanguageId) { - // language id change! - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - prevSearchInToken = false; - } - languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); - } - - const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchStartOffset - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - } - } - - prevSearchInToken = searchInToken; - } - - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - } - } - - return null; - } - - public findNextBracket(_position: IPosition): model.IFoundBracket | null { - const position = this.validatePosition(_position); - const lineCount = this.getLineCount(); - - let languageId: LanguageId = -1; - let modeBrackets: RichEditBrackets | null = null; - for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = 0; - let searchStartOffset = 0; - let searchEndOffset = 0; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - if (languageId !== tokenLanguageId) { - languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); - } - } - - let prevSearchInToken = true; - for (; tokenIndex < tokenCount; tokenIndex++) { - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - - if (languageId !== tokenLanguageId) { - // language id change! - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - prevSearchInToken = false; - } - languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); - } - - const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchEndOffset - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - } - } - - prevSearchInToken = searchInToken; - } - - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return this._toFoundBracket(modeBrackets, r); - } - } - } - - return null; - } - - public findEnclosingBrackets(_position: IPosition, maxDuration?: number): [Range, Range] | null { - let continueSearchPredicate: ContinueBracketSearchPredicate; - if (typeof maxDuration === 'undefined') { - continueSearchPredicate = null; - } else { - const startTime = Date.now(); - continueSearchPredicate = () => { - return (Date.now() - startTime <= maxDuration); - }; - } - const position = this.validatePosition(_position); - const lineCount = this.getLineCount(); - const savedCounts = new Map(); - - let counts: number[] = []; - const resetCounts = (languageId: number, modeBrackets: RichEditBrackets | null) => { - if (!savedCounts.has(languageId)) { - let tmp = []; - for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) { - tmp[i] = 0; - } - savedCounts.set(languageId, tmp); - } - counts = savedCounts.get(languageId)!; - }; - - let totalCallCount = 0; - const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null | BracketSearchCanceled => { - while (true) { - if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { - return BracketSearchCanceled.INSTANCE; - } - const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (!r) { - break; - } - - const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); - const bracket = modeBrackets.textIsBracket[hitText]; - if (bracket) { - if (bracket.isOpen(hitText)) { - counts[bracket.index]++; - } else if (bracket.isClose(hitText)) { - counts[bracket.index]--; - } - - if (counts[bracket.index] === -1) { - return this._matchFoundBracket(r, bracket, false, continueSearchPredicate); - } - } - - searchStartOffset = r.endColumn - 1; - } - return null; - }; - - let languageId: LanguageId = -1; - let modeBrackets: RichEditBrackets | null = null; - for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this._getLineTokens(lineNumber); - const tokenCount = lineTokens.getCount(); - const lineText = this._buffer.getLineContent(lineNumber); - - let tokenIndex = 0; - let searchStartOffset = 0; - let searchEndOffset = 0; - if (lineNumber === position.lineNumber) { - tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - searchStartOffset = position.column - 1; - searchEndOffset = position.column - 1; - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - if (languageId !== tokenLanguageId) { - languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); - resetCounts(languageId, modeBrackets); - } - } - - let prevSearchInToken = true; - for (; tokenIndex < tokenCount; tokenIndex++) { - const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); - - if (languageId !== tokenLanguageId) { - // language id change! - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return stripBracketSearchCanceled(r); - } - prevSearchInToken = false; - } - languageId = tokenLanguageId; - modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); - resetCounts(languageId, modeBrackets); - } - - const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); - if (searchInToken) { - // this token should be searched - if (prevSearchInToken) { - // the previous token should be searched, simply extend searchEndOffset - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } else { - // the previous token should not be searched - searchStartOffset = lineTokens.getStartOffset(tokenIndex); - searchEndOffset = lineTokens.getEndOffset(tokenIndex); - } - } else { - // this token should not be searched - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return stripBracketSearchCanceled(r); - } - } - } - - prevSearchInToken = searchInToken; - } - - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { - const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); - if (r) { - return stripBracketSearchCanceled(r); - } - } - } - - return null; - } - - private _toFoundBracket(modeBrackets: RichEditBrackets, r: Range): model.IFoundBracket | null { - if (!r) { - return null; - } - - let text = this.getValueInRange(r); - text = text.toLowerCase(); - - let data = modeBrackets.textIsBracket[text]; - if (!data) { - return null; - } - - return { - range: r, - open: data.open, - close: data.close, - isOpen: modeBrackets.textIsOpenBracket[text] - }; - } - /** * Returns: * - -1 => the line consists of whitespace @@ -2887,7 +2246,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati throw new Error('Illegal value for lineNumber'); } - const foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id); + const foldingRules = this.getLanguageConfiguration(this._languageId).foldingRules; const offSide = Boolean(foldingRules && foldingRules.offSide); let up_aboveContentLineIndex = -2; /* -2 is a marker for not having computed it */ @@ -3071,6 +2430,188 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return { startLineNumber, endLineNumber, indent }; } + public getLinesBracketGuides( + startLineNumber: number, + endLineNumber: number, + activePosition: IPosition | null, + options: model.BracketGuideOptions + ): model.IndentGuide[][] { + const result: model.IndentGuide[][] = []; + const bracketPairs = + this._bracketPairColorizer.getBracketPairsInRangeWithMinIndentation( + new Range( + startLineNumber, + 1, + endLineNumber, + this.getLineMaxColumn(endLineNumber) + ) + ); + + let activeBracketPairRange: Range | undefined = undefined; + if (activePosition && bracketPairs.length > 0) { + const bracketsContainingActivePosition = + (startLineNumber <= activePosition.lineNumber && activePosition.lineNumber <= endLineNumber) + // Does active position intersect with the view port? -> Intersect bracket pairs with activePosition + ? bracketPairs.filter(bp => bp.range.containsPosition(activePosition)) + : this._bracketPairColorizer.getBracketPairsInRange( + Range.fromPositions(activePosition) + ); + + activeBracketPairRange = findLast( + bracketsContainingActivePosition, + /* Exclude single line bracket pairs for cases such as + * ``` + * function test() { + * if (true) { | } + * } + * ``` + */ + (i) => i.range.startLineNumber !== i.range.endLineNumber + )?.range; + } + + const queue = new ArrayQueue(bracketPairs); + /** Indexed by nesting level */ + const activeGuides = new Array<{ + nestingLevel: number, + guideVisibleColumn: number, + start: Position, + visibleStartColumn: number, + end: Position, + visibleEndColumn: number, + bracketPair: BracketPairInfo, + renderHorizontalEndLineAtTheBottom: boolean + } | null>(); + const nextGuides = new Array(); + const colorProvider = new BracketPairGuidesClassNames(); + + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + let guides = new Array(); + if (nextGuides.length > 0) { + guides = guides.concat(nextGuides); + nextGuides.length = 0; + } + result.push(guides); + + // Update activeGuides + for (const pair of queue.takeWhile(b => b.openingBracketRange.startLineNumber <= lineNumber) || []) { + if (pair.range.startLineNumber === pair.range.endLineNumber) { + // ignore single line brackets + continue; + } + const guideVisibleColumn = Math.min( + this.getVisibleColumnFromPosition(pair.openingBracketRange.getStartPosition()), + this.getVisibleColumnFromPosition(pair.closingBracketRange?.getStartPosition() ?? pair.range.getEndPosition()), + pair.minVisibleColumnIndentation + 1 + ); + let renderHorizontalEndLineAtTheBottom = false; + if (pair.closingBracketRange) { + const firstNonWsIndex = strings.firstNonWhitespaceIndex(this.getLineContent(pair.closingBracketRange.startLineNumber)); + if (firstNonWsIndex < pair.closingBracketRange.startColumn - 1) { + renderHorizontalEndLineAtTheBottom = true; + } + } + // TODO: Consider indentation when computing guideVisibleColumn + const start = pair.openingBracketRange.getStartPosition(); + const end = (pair.closingBracketRange?.getStartPosition() ?? pair.range.getEndPosition()); + + + if (pair.closingBracketRange === undefined) { + // Don't show guides for bracket pairs that are not balanced. + // See #135125. + activeGuides[pair.nestingLevel] = null; + } else { + activeGuides[pair.nestingLevel] = { + nestingLevel: pair.nestingLevel, + guideVisibleColumn, + start, + visibleStartColumn: this.getVisibleColumnFromPosition(start), + end, + visibleEndColumn: this.getVisibleColumnFromPosition(end), + bracketPair: pair, + renderHorizontalEndLineAtTheBottom + }; + } + } + + for (const line of activeGuides) { + if (!line) { + continue; + } + const isActive = activeBracketPairRange && line.bracketPair.range.equalsRange(activeBracketPairRange); + + const className = + colorProvider.getInlineClassNameOfLevel(line.nestingLevel) + + (options.highlightActive && isActive ? ' ' + colorProvider.activeClassName : ''); + + if ( + (isActive && options.horizontalGuides !== model.HorizontalGuidesState.Disabled) + || (options.includeInactive && options.horizontalGuides === model.HorizontalGuidesState.Enabled) + ) { + if (line.start.lineNumber === lineNumber) { + if (line.guideVisibleColumn < line.visibleStartColumn) { + guides.push(new model.IndentGuide(line.guideVisibleColumn, className, + new model.IndentGuideHorizontalLine(false, line.start.column))); + } + } + if (line.end.lineNumber === lineNumber + 1) { + // The next line might have horizontal guides. + // However, the next line might also have a new bracket pair with the same indentation, + // so the current bracket pair might get replaced. That's why we push the guide to nextGuides one line ahead. + if (line.guideVisibleColumn < line.visibleEndColumn) { + nextGuides.push(new model.IndentGuide(line.guideVisibleColumn, className, + new model.IndentGuideHorizontalLine(!line.renderHorizontalEndLineAtTheBottom, line.end.column))); + } + } + } + } + + let lastVisibleColumnCount = Number.MAX_SAFE_INTEGER; + // Going backwards, so the last guide potentially replaces others + for (let i = activeGuides.length - 1; i >= 0; i--) { + const line = activeGuides[i]; + if (!line) { + continue; + } + const isActive = options.highlightActive && activeBracketPairRange && + line.bracketPair.range.equalsRange(activeBracketPairRange); + + const className = + colorProvider.getInlineClassNameOfLevel(line.nestingLevel) + + (isActive ? ' ' + colorProvider.activeClassName : ''); + + if (isActive || options.includeInactive) { + if (line.renderHorizontalEndLineAtTheBottom && line.end.lineNumber === lineNumber + 1) { + nextGuides.push(new model.IndentGuide(line.guideVisibleColumn, className, null)); + } + } + + if (line.end.lineNumber <= lineNumber + || line.start.lineNumber >= lineNumber) { + continue; + } + + if (line.guideVisibleColumn >= lastVisibleColumnCount && !isActive) { + // Don't render a guide on top of an existing guide, unless it is active. + continue; + } + lastVisibleColumnCount = line.guideVisibleColumn; + + + if (isActive || options.includeInactive) { + guides.push(new model.IndentGuide(line.guideVisibleColumn, className, null)); + } + } + + guides.sort((a, b) => a.visibleColumn - b.visibleColumn); + } + return result; + } + + private getVisibleColumnFromPosition(position: Position): number { + return CursorColumns.visibleColumnFromColumn(this.getLineContent(position.lineNumber), position.column, this._options.tabSize) + 1; + } + public getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[] { this._assertNotDisposed(); const lineCount = this.getLineCount(); @@ -3082,7 +2623,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati throw new Error('Illegal value for endLineNumber'); } - const foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id); + const foldingRules = this.getLanguageConfiguration(this._languageId).foldingRules; const offSide = Boolean(foldingRules && foldingRules.offSide); let result: number[] = new Array(endLineNumber - startLineNumber + 1); @@ -3195,6 +2736,16 @@ function indentOfLine(line: string): number { return indent; } +export class BracketPairGuidesClassNames { + public readonly activeClassName = 'indent-active'; + + getInlineClassNameOfLevel(level: number): string { + // To support a dynamic amount of colors up to 6 colors, + // we use a number that is a lcm of all numbers from 1 to 6. + return `bracket-indent-guide lvl-${level % 30}`; + } +} + //#region Decorations function isNodeInOverviewRuler(node: IntervalNode): boolean { @@ -3262,13 +2813,13 @@ class DecorationsTrees { public getInjectedTextInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number): model.IModelDecoration[] { const versionId = host.getVersionId(); const result = this._injectedTextDecorationsTree.intervalSearch(start, end, filterOwnerId, false, versionId); - return this._ensureNodesHaveRanges(host, result); + return this._ensureNodesHaveRanges(host, result).filter((i) => i.options.showIfCollapsed || !i.range.isEmpty()); } public getAllInjectedText(host: IDecorationsTreesHost, filterOwnerId: number): model.IModelDecoration[] { const versionId = host.getVersionId(); const result = this._injectedTextDecorationsTree.search(filterOwnerId, false, versionId); - return this._ensureNodesHaveRanges(host, result); + return this._ensureNodesHaveRanges(host, result).filter((i) => i.options.showIfCollapsed || !i.range.isEmpty()); } public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean): model.IModelDecoration[] { diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index b1d051b5d6..1039ed37e6 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -8,7 +8,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { IModelDecoration, InjectedTextOptions } from 'vs/editor/common/model'; /** - * An event describing that the current mode associated with a model has changed. + * An event describing that the current language associated with a model has changed. */ export interface IModelLanguageChangedEvent { /** diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index e92c8fcf27..5a85ccd43b 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -85,7 +85,7 @@ export function isMultilineRegexSource(searchString: string): boolean { } const nextChCode = searchString.charCodeAt(i); - if (nextChCode === CharCode.n || nextChCode === CharCode.r || nextChCode === CharCode.W || nextChCode === CharCode.w) { + if (nextChCode === CharCode.n || nextChCode === CharCode.r || nextChCode === CharCode.W) { return true; } } diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index 7a9e2d301a..370599cc3e 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -9,7 +9,7 @@ import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { IState, ITokenizationSupport, LanguageIdentifier, TokenizationRegistry } from 'vs/editor/common/modes'; +import { ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/modes'; import { nullTokenize2 } from 'vs/editor/common/modes/nullMode'; import { TextModel } from 'vs/editor/common/model/textModel'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -101,8 +101,8 @@ export class TokenizationStateStore { if (insertCount === 0) { return; } - let beginState: (IState | null)[] = []; - let valid: boolean[] = []; + const beginState: (IState | null)[] = []; + const valid: boolean[] = []; for (let i = 0; i < insertCount; i++) { beginState[i] = null; valid[i] = false; @@ -194,21 +194,22 @@ export class TokenizationStateStore { export class TextModelTokenization extends Disposable { - private readonly _textModel: TextModel; private readonly _tokenizationStateStore: TokenizationStateStore; private _isDisposed: boolean; private _tokenizationSupport: ITokenizationSupport | null; - constructor(textModel: TextModel) { + constructor( + private readonly _textModel: TextModel, + private readonly _languageIdCodec: ILanguageIdCodec + ) { super(); this._isDisposed = false; - this._textModel = textModel; this._tokenizationStateStore = new TokenizationStateStore(); this._tokenizationSupport = null; this._register(TokenizationRegistry.onDidChange((e) => { - const languageIdentifier = this._textModel.getLanguageIdentifier(); - if (e.changedLanguages.indexOf(languageIdentifier.language) === -1) { + const languageId = this._textModel.getLanguageId(); + if (e.changedLanguages.indexOf(languageId) === -1) { return; } @@ -288,13 +289,13 @@ export class TextModelTokenization extends Disposable { } this._beginBackgroundTokenization(); - this._textModel.setTokens(builder.tokens, tokenizedLineNumber >= textModelLastLineNumber); + this._textModel.setTokens(builder.tokens, !this._hasLinesToTokenize()); } public tokenizeViewport(startLineNumber: number, endLineNumber: number): void { const builder = new MultilineTokensBuilder(); this._tokenizeViewport(builder, startLineNumber, endLineNumber); - this._textModel.setTokens(builder.tokens); + this._textModel.setTokens(builder.tokens, !this._hasLinesToTokenize()); } public reset(): void { @@ -305,7 +306,7 @@ export class TextModelTokenization extends Disposable { public forceTokenization(lineNumber: number): void { const builder = new MultilineTokensBuilder(); this._updateTokensUntilLine(builder, lineNumber); - this._textModel.setTokens(builder.tokens); + this._textModel.setTokens(builder.tokens, !this._hasLinesToTokenize()); } public isCheapToTokenize(lineNumber: number): boolean { @@ -349,7 +350,7 @@ export class TextModelTokenization extends Disposable { if (!this._tokenizationSupport) { return; } - const languageIdentifier = this._textModel.getLanguageIdentifier(); + const languageId = this._textModel.getLanguageId(); const linesLength = this._textModel.getLineCount(); const endLineIndex = lineNumber - 1; @@ -358,7 +359,7 @@ export class TextModelTokenization extends Disposable { const text = this._textModel.getLineContent(lineIndex + 1); const lineStartState = this._tokenizationStateStore.getBeginState(lineIndex); - const r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, true, lineStartState!); + const r = safeTokenize(this._languageIdCodec, languageId, this._tokenizationSupport, text, true, lineStartState!); builder.add(lineIndex + 1, r.tokens); this._tokenizationStateStore.setEndState(linesLength, lineIndex, r.endState); lineIndex = this._tokenizationStateStore.invalidLineStartIndex - 1; // -1 because the outer loop increments it @@ -383,10 +384,10 @@ export class TextModelTokenization extends Disposable { } let nonWhitespaceColumn = this._textModel.getLineFirstNonWhitespaceColumn(startLineNumber); - let fakeLines: string[] = []; + const fakeLines: string[] = []; let initialState: IState | null = null; - for (let i = startLineNumber - 1; nonWhitespaceColumn > 0 && i >= 1; i--) { - let newNonWhitespaceIndex = this._textModel.getLineFirstNonWhitespaceColumn(i); + for (let i = startLineNumber - 1; nonWhitespaceColumn > 1 && i >= 1; i--) { + const newNonWhitespaceIndex = this._textModel.getLineFirstNonWhitespaceColumn(i); if (newNonWhitespaceIndex === 0) { continue; @@ -406,16 +407,16 @@ export class TextModelTokenization extends Disposable { initialState = this._tokenizationSupport.getInitialState(); } - const languageIdentifier = this._textModel.getLanguageIdentifier(); + const languageId = this._textModel.getLanguageId(); let state = initialState; for (let i = fakeLines.length - 1; i >= 0; i--) { - let r = safeTokenize(languageIdentifier, this._tokenizationSupport, fakeLines[i], false, state); + const r = safeTokenize(this._languageIdCodec, languageId, this._tokenizationSupport, fakeLines[i], false, state); state = r.endState; } for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - let text = this._textModel.getLineContent(lineNumber); - let r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, true, state); + const text = this._textModel.getLineContent(lineNumber); + const r = safeTokenize(this._languageIdCodec, languageId, this._tokenizationSupport, text, true, state); builder.add(lineNumber, r.tokens); this._tokenizationStateStore.setFakeTokens(lineNumber - 1); state = r.endState; @@ -424,11 +425,11 @@ export class TextModelTokenization extends Disposable { } function initializeTokenization(textModel: TextModel): [ITokenizationSupport | null, IState | null] { - const languageIdentifier = textModel.getLanguageIdentifier(); + const languageId = textModel.getLanguageId(); let tokenizationSupport = ( textModel.isTooLargeForTokenization() ? null - : TokenizationRegistry.get(languageIdentifier.language) + : TokenizationRegistry.get(languageId) ); let initialState: IState | null = null; if (tokenizationSupport) { @@ -442,7 +443,7 @@ function initializeTokenization(textModel: TextModel): [ITokenizationSupport | n return [tokenizationSupport, initialState]; } -function safeTokenize(languageIdentifier: LanguageIdentifier, tokenizationSupport: ITokenizationSupport | null, text: string, hasEOL: boolean, state: IState): TokenizationResult2 { +function safeTokenize(languageIdCodec: ILanguageIdCodec, languageId: string, tokenizationSupport: ITokenizationSupport | null, text: string, hasEOL: boolean, state: IState): TokenizationResult2 { let r: TokenizationResult2 | null = null; if (tokenizationSupport) { @@ -454,7 +455,7 @@ function safeTokenize(languageIdentifier: LanguageIdentifier, tokenizationSuppor } if (!r) { - r = nullTokenize2(languageIdentifier.id, text, state, 0); + r = nullTokenize2(languageIdCodec.encodeLanguageId(languageId), text, state, 0); } LineTokens.convertToEndOffset(r.tokens, text.length); diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index 193ca7ada4..5ec6baafb8 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -7,7 +7,7 @@ import * as arrays from 'vs/base/common/arrays'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; +import { ColorId, FontStyle, ILanguageIdCodec, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer'; import { CharCode } from 'vs/base/common/charCode'; @@ -873,10 +873,12 @@ export class TokensStore2 { private _pieces: MultilineTokens2[]; private _isComplete: boolean; + private readonly _languageIdCodec: ILanguageIdCodec; - constructor() { + constructor(languageIdCodec: ILanguageIdCodec) { this._pieces = []; this._isComplete = false; + this._languageIdCodec = languageIdCodec; } public flush(): void { @@ -1058,7 +1060,7 @@ export class TokensStore2 { aIndex++; } - return new LineTokens(new Uint32Array(result), aTokens.getLineContent()); + return new LineTokens(new Uint32Array(result), aTokens.getLineContent(), this._languageIdCodec); } private static _findFirstPieceWithLine(pieces: MultilineTokens2[], lineNumber: number): number { @@ -1097,10 +1099,12 @@ export class TokensStore2 { export class TokensStore { private _lineTokens: (Uint32Array | ArrayBuffer | null)[]; private _len: number; + private readonly _languageIdCodec: ILanguageIdCodec; - constructor() { + constructor(languageIdCodec: ILanguageIdCodec) { this._lineTokens = []; this._len = 0; + this._languageIdCodec = languageIdCodec; } public flush(): void { @@ -1108,20 +1112,20 @@ export class TokensStore { this._len = 0; } - public getTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineText: string): LineTokens { + public getTokens(topLevelLanguageId: string, lineIndex: number, lineText: string): LineTokens { let rawLineTokens: Uint32Array | ArrayBuffer | null = null; if (lineIndex < this._len) { rawLineTokens = this._lineTokens[lineIndex]; } if (rawLineTokens !== null && rawLineTokens !== EMPTY_LINE_TOKENS) { - return new LineTokens(toUint32Array(rawLineTokens), lineText); + return new LineTokens(toUint32Array(rawLineTokens), lineText, this._languageIdCodec); } - let lineTokens = new Uint32Array(2); + const lineTokens = new Uint32Array(2); lineTokens[0] = lineText.length; - lineTokens[1] = getDefaultMetadata(topLevelLanguageId); - return new LineTokens(lineTokens, lineText); + lineTokens[1] = getDefaultMetadata(this._languageIdCodec.encodeLanguageId(topLevelLanguageId)); + return new LineTokens(lineTokens, lineText, this._languageIdCodec); } private static _massageTokens(topLevelLanguageId: LanguageId, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null): Uint32Array | ArrayBuffer { @@ -1186,8 +1190,8 @@ export class TokensStore { this._len += insertCount; } - public setTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null, checkEquality: boolean): boolean { - const tokens = TokensStore._massageTokens(topLevelLanguageId, lineTextLength, _tokens); + public setTokens(topLevelLanguageId: string, lineIndex: number, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null, checkEquality: boolean): boolean { + const tokens = TokensStore._massageTokens(this._languageIdCodec.encodeLanguageId(topLevelLanguageId), lineTextLength, _tokens); this._ensureLine(lineIndex); const oldTokens = this._lineTokens[lineIndex]; this._lineTokens[lineIndex] = tokens; diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 56fe65c6c9..09ef360da6 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -29,40 +29,6 @@ export const enum LanguageId { PlainText = 1 } -/** - * @internal - */ -export class LanguageIdentifier { - - /** - * A string identifier. Unique across languages. e.g. 'javascript'. - */ - public readonly language: string; - - /** - * A numeric identifier. Unique across languages. e.g. 5 - * Will vary at runtime based on registration order, etc. - */ - public readonly id: LanguageId; - - constructor(language: string, id: LanguageId) { - this.language = language; - this.id = id; - } -} - -/** - * A mode. Will soon be obsolete. - * @internal - */ -export interface IMode { - - getId(): string; - - getLanguageIdentifier(): LanguageIdentifier; - -} - /** * A font style. Values are 2^x such that a bit mask can be used. * @internal @@ -203,6 +169,14 @@ export class TokenMetadata { } } +/** + * @internal + */ +export interface ILanguageIdCodec { + encodeLanguageId(languageId: string): LanguageId; + decodeLanguageId(languageId: LanguageId): string; +} + /** * @internal */ @@ -509,6 +483,11 @@ export const enum CompletionItemInsertTextRule { InsertAsSnippet = 0b100, } +export interface CompletionItemRanges { + insert: IRange; + replace: IRange; +} + /** * A completion item represents a text snippet that is * proposed to complete text that is being typed. @@ -577,7 +556,7 @@ export interface CompletionItem { * *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 }; + range: IRange | CompletionItemRanges; /** * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous @@ -691,6 +670,13 @@ export interface InlineCompletionContext { * How the completion was triggered. */ readonly triggerKind: InlineCompletionTriggerKind; + + readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined; +} + +export interface SelectedSuggestionInfo { + range: IRange; + text: string; } export interface InlineCompletion { @@ -1760,7 +1746,7 @@ export interface InlayHint { } export interface InlayHintsProvider { - onDidChangeInlayHints?: Event | undefined; + onDidChangeInlayHints?: Event; provideInlayHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; } diff --git a/src/vs/editor/common/modes/abstractMode.ts b/src/vs/editor/common/modes/abstractMode.ts deleted file mode 100644 index cd01761b1d..0000000000 --- a/src/vs/editor/common/modes/abstractMode.ts +++ /dev/null @@ -1,23 +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 { IMode, LanguageIdentifier } from 'vs/editor/common/modes'; - -export class FrankensteinMode implements IMode { - - private readonly _languageIdentifier: LanguageIdentifier; - - constructor(languageIdentifier: LanguageIdentifier) { - this._languageIdentifier = languageIdentifier; - } - - public getId(): string { - return this._languageIdentifier.language; - } - - public getLanguageIdentifier(): LanguageIdentifier { - return this._languageIdentifier; - } -} diff --git a/src/vs/editor/common/modes/languageConfiguration.ts b/src/vs/editor/common/modes/languageConfiguration.ts index 791ae70082..aad4c2abd2 100644 --- a/src/vs/editor/common/modes/languageConfiguration.ts +++ b/src/vs/editor/common/modes/languageConfiguration.ts @@ -60,7 +60,11 @@ export interface LanguageConfiguration { * settings will be used. */ surroundingPairs?: IAutoClosingPair[]; - + /** + * Defines a list of bracket pairs that are colorized depending on their nesting level. + * If not set, the configured brackets will be used. + */ + colorizedBracketPairs?: CharacterPair[]; /** * Defines what characters must be after the cursor for bracket or quote autoclosing to occur when using the \'languageDefined\' autoclosing setting. * @@ -83,6 +87,16 @@ export interface LanguageConfiguration { }; } +/** + * @internal + */ +type OrUndefined = { [P in keyof T]: T[P] | undefined }; + +/** + * @internal + */ +export type ExplicitLanguageConfiguration = OrUndefined>; + /** * Describes indentation rules for a language. */ diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index 22ab9e5675..0598762d7d 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; -import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; -import { EnterAction, FoldingRules, IAutoClosingPair, IndentAction, IndentationRule, LanguageConfiguration, StandardAutoClosingPairConditional, CompleteEnterAction, AutoClosingPairs } from 'vs/editor/common/modes/languageConfiguration'; +import { EnterAction, FoldingRules, IAutoClosingPair, IndentAction, IndentationRule, LanguageConfiguration, StandardAutoClosingPairConditional, CompleteEnterAction, AutoClosingPairs, CharacterPair, ExplicitLanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; import { createScopedLineTokens, ScopedLineTokens } from 'vs/editor/common/modes/supports'; import { CharacterPairSupport } from 'vs/editor/common/modes/supports/characterPair'; import { BracketElectricCharacterSupport, IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter'; @@ -19,6 +18,10 @@ import { IndentConsts, IndentRulesSupport } from 'vs/editor/common/modes/support import { OnEnterSupport } from 'vs/editor/common/modes/supports/onEnter'; import { RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; /** * Interface used to support insertion of mode specific comments. @@ -31,8 +34,8 @@ export interface ICommentsConfiguration { export interface IVirtualModel { getLineTokens(lineNumber: number): LineTokens; - getLanguageIdentifier(): LanguageIdentifier; - getLanguageIdAtPosition(lineNumber: number, column: number): LanguageId; + getLanguageId(): string; + getLanguageIdAtPosition(lineNumber: number, column: number): string; getLineContent(lineNumber: number): string; } @@ -42,171 +45,137 @@ export interface IIndentConverter { normalizeIndentation?(indentation: string): string; } -export class RichEditSupport { +export interface ILanguageConfigurationService { + readonly _serviceBrand: undefined; - private readonly _conf: LanguageConfiguration; - private readonly _languageIdentifier: LanguageIdentifier; - private _brackets: RichEditBrackets | null; - private _electricCharacter: BracketElectricCharacterSupport | null; - private readonly _onEnterSupport: OnEnterSupport | null; + onDidChange: Event; + getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration; +} - public readonly comments: ICommentsConfiguration | null; - public readonly characterPair: CharacterPairSupport; - public readonly wordDefinition: RegExp; - public readonly indentRulesSupport: IndentRulesSupport | null; - public readonly indentationRules: IndentationRule | undefined; - public readonly foldingRules: FoldingRules; +export class LanguageConfigurationServiceChangeEvent { + constructor(public readonly languageId: string | undefined) { } - constructor(languageIdentifier: LanguageIdentifier, rawConf: LanguageConfiguration) { - this._languageIdentifier = languageIdentifier; - this._brackets = null; - this._electricCharacter = null; - this._conf = rawConf; - this._onEnterSupport = (this._conf.brackets || this._conf.indentationRules || this._conf.onEnterRules ? new OnEnterSupport(this._conf) : null); - this.comments = RichEditSupport._handleComments(this._conf); - this.characterPair = new CharacterPairSupport(this._conf); - this.wordDefinition = this._conf.wordPattern || DEFAULT_WORD_REGEXP; - this.indentationRules = this._conf.indentationRules; - if (this._conf.indentationRules) { - this.indentRulesSupport = new IndentRulesSupport(this._conf.indentationRules); - } else { - this.indentRulesSupport = null; - } - this.foldingRules = this._conf.folding || {}; - } - - public get brackets(): RichEditBrackets | null { - if (!this._brackets && this._conf.brackets) { - this._brackets = new RichEditBrackets(this._languageIdentifier, this._conf.brackets); - } - return this._brackets; - } - - public get electricCharacter(): BracketElectricCharacterSupport | null { - if (!this._electricCharacter) { - this._electricCharacter = new BracketElectricCharacterSupport(this.brackets); - } - return this._electricCharacter; - } - - public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { - if (!this._onEnterSupport) { - return null; - } - return this._onEnterSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); - } - - private static _handleComments(conf: LanguageConfiguration): ICommentsConfiguration | null { - let commentRule = conf.comments; - if (!commentRule) { - return null; - } - - // comment configuration - let comments: ICommentsConfiguration = {}; - - if (commentRule.lineComment) { - comments.lineCommentToken = commentRule.lineComment; - } - if (commentRule.blockComment) { - let [blockStart, blockEnd] = commentRule.blockComment; - comments.blockCommentStartToken = blockStart; - comments.blockCommentEndToken = blockEnd; - } - - return comments; + public affects(languageId: string): boolean { + return !this.languageId ? true : this.languageId === languageId; } } -export class LanguageConfigurationChangeEvent { - constructor( - public readonly languageIdentifier: LanguageIdentifier - ) { } -} +export const ILanguageConfigurationService = createDecorator('languageConfigurationService'); -class LanguageConfigurationEntry { +export class LanguageConfigurationService extends Disposable implements ILanguageConfigurationService { + _serviceBrand: undefined; + + private readonly onDidChangeEmitter = this._register(new Emitter()); + public readonly onDidChange = this.onDidChangeEmitter.event; + + private readonly configurations = new Map(); constructor( - public readonly configuration: LanguageConfiguration, - public readonly priority: number, - public readonly order: number - ) { } - - public static cmp(a: LanguageConfigurationEntry, b: LanguageConfigurationEntry) { - if (a.priority === b.priority) { - // higher order last - return a.order - b.order; - } - // higher priority last - return a.priority - b.priority; - } -} - -class LanguageConfigurationEntries { - - private readonly _entries: LanguageConfigurationEntry[]; - private _order: number; - private _resolved: RichEditSupport | null = null; - - constructor( - public readonly languageIdentifier: LanguageIdentifier + @IConfigurationService private readonly configurationService: IConfigurationService, + @IModeService private readonly modeService: IModeService ) { - this._entries = []; - this._order = 0; - this._resolved = null; - } + super(); - public register(configuration: LanguageConfiguration, priority: number): IDisposable { - const entry = new LanguageConfigurationEntry(configuration, priority, ++this._order); - this._entries.push(entry); - this._resolved = null; - return toDisposable(() => { - for (let i = 0; i < this._entries.length; i++) { - if (this._entries[i] === entry) { - this._entries.splice(i, 1); - this._resolved = null; - break; + const languageConfigKeys = new Set(Object.values(customizedLanguageConfigKeys)); + + this._register(this.configurationService.onDidChangeConfiguration((e) => { + const globalConfigChanged = e.change.keys.some((k) => + languageConfigKeys.has(k) + ); + const localConfigChanged = e.change.overrides + .filter(([overrideLangName, keys]) => + keys.some((k) => languageConfigKeys.has(k)) + ) + .map(([overrideLangName]) => this.modeService.validateLanguageId(overrideLangName)); + + if (globalConfigChanged) { + this.configurations.clear(); + this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(undefined)); + } else { + for (const languageId of localConfigChanged) { + if (languageId) { + this.configurations.delete(languageId); + this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(languageId)); + } } } - }); + })); + + this._register(LanguageConfigurationRegistry.onDidChange((e) => { + this.configurations.delete(e.languageId); + this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(e.languageId)); + })); } - public getRichEditSupport(): RichEditSupport | null { - if (!this._resolved) { - const config = this._resolve(); - if (config) { - this._resolved = new RichEditSupport(this.languageIdentifier, config); - } - } - return this._resolved; - } - - private _resolve(): LanguageConfiguration | null { - if (this._entries.length === 0) { - return null; - } - this._entries.sort(LanguageConfigurationEntry.cmp); - const result: LanguageConfiguration = {}; - for (const entry of this._entries) { - const conf = entry.configuration; - result.comments = conf.comments || result.comments; - result.brackets = conf.brackets || result.brackets; - result.wordPattern = conf.wordPattern || result.wordPattern; - result.indentationRules = conf.indentationRules || result.indentationRules; - result.onEnterRules = conf.onEnterRules || result.onEnterRules; - result.autoClosingPairs = conf.autoClosingPairs || result.autoClosingPairs; - result.surroundingPairs = conf.surroundingPairs || result.surroundingPairs; - result.autoCloseBefore = conf.autoCloseBefore || result.autoCloseBefore; - result.folding = conf.folding || result.folding; - result.__electricCharacterSupport = conf.__electricCharacterSupport || result.__electricCharacterSupport; + public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration { + let result = this.configurations.get(languageId); + if (!result) { + result = computeConfig(languageId, this.configurationService, this.modeService); + this.configurations.set(languageId, result); } return result; } } -export class LanguageConfigurationRegistryImpl { +function computeConfig( + languageId: string, + configurationService: IConfigurationService, + modeService: IModeService, +): ResolvedLanguageConfiguration { + let languageConfig = LanguageConfigurationRegistry.getLanguageConfiguration(languageId); - private readonly _entries2 = new Map(); + if (!languageConfig) { + const validLanguageId = modeService.validateLanguageId(languageId); + if (!validLanguageId) { + throw new Error('Unexpected languageId'); + } + languageConfig = new ResolvedLanguageConfiguration(validLanguageId, {}); + } + + const customizedConfig = getCustomizedLanguageConfig(languageConfig.languageId, configurationService); + const data = combineLanguageConfigurations([languageConfig.underlyingConfig, customizedConfig]); + const config = new ResolvedLanguageConfiguration(languageConfig.languageId, data); + return config; +} + +const customizedLanguageConfigKeys = { + brackets: 'editor.language.brackets', + colorizedBracketPairs: 'editor.language.colorizedBracketPairs' +}; + +function getCustomizedLanguageConfig(languageId: string, configurationService: IConfigurationService): LanguageConfiguration { + const brackets = configurationService.getValue(customizedLanguageConfigKeys.brackets, { + overrideIdentifier: languageId, + }); + + const colorizedBracketPairs = configurationService.getValue(customizedLanguageConfigKeys.colorizedBracketPairs, { + overrideIdentifier: languageId, + }); + + return { + brackets: validateBracketPairs(brackets), + colorizedBracketPairs: validateBracketPairs(colorizedBracketPairs), + }; +} + +function validateBracketPairs(data: unknown): CharacterPair[] | undefined { + if (!Array.isArray(data)) { + return undefined; + } + return data.map(pair => { + if (!Array.isArray(pair) || pair.length !== 2) { + return undefined; + } + return [pair[0], pair[1]] as CharacterPair; + }).filter((p): p is CharacterPair => !!p); +} + +export class LanguageConfigurationChangeEvent { + constructor(public readonly languageId: string) { } +} + +export class LanguageConfigurationRegistryImpl { + private readonly _entries = new Map(); private readonly _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; @@ -214,43 +183,43 @@ export class LanguageConfigurationRegistryImpl { /** * @param priority Use a higher number for higher priority */ - public register(languageIdentifier: LanguageIdentifier, configuration: LanguageConfiguration, priority: number = 0): IDisposable { - let entries = this._entries2.get(languageIdentifier.id); + public register(languageId: string, configuration: LanguageConfiguration, priority: number = 0): IDisposable { + let entries = this._entries.get(languageId); if (!entries) { - entries = new LanguageConfigurationEntries(languageIdentifier); - this._entries2.set(languageIdentifier.id, entries); + entries = new ComposedLanguageConfiguration(languageId); + this._entries.set(languageId, entries); } const disposable = entries.register(configuration, priority); - this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageIdentifier)); + this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); return toDisposable(() => { disposable.dispose(); - this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageIdentifier)); + this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); }); } - private _getRichEditSupport(languageId: LanguageId): RichEditSupport | null { - const entries = this._entries2.get(languageId); - return entries ? entries.getRichEditSupport() : null; + public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration | null { + let entries = this._entries.get(languageId); + return entries?.getResolvedConfiguration() || null; } - public getIndentationRules(languageId: LanguageId): IndentationRule | null { - const value = this._getRichEditSupport(languageId); + public getIndentationRules(languageId: string): IndentationRule | null { + const value = this.getLanguageConfiguration(languageId); return value ? value.indentationRules || null : null; } // begin electricCharacter - private _getElectricCharacterSupport(languageId: LanguageId): BracketElectricCharacterSupport | null { - let value = this._getRichEditSupport(languageId); + private _getElectricCharacterSupport(languageId: string): BracketElectricCharacterSupport | null { + let value = this.getLanguageConfiguration(languageId); if (!value) { return null; } return value.electricCharacter || null; } - public getElectricCharacters(languageId: LanguageId): string[] { + public getElectricCharacters(languageId: string): string[] { let electricCharacterSupport = this._getElectricCharacterSupport(languageId); if (!electricCharacterSupport) { return []; @@ -272,8 +241,8 @@ export class LanguageConfigurationRegistryImpl { // end electricCharacter - public getComments(languageId: LanguageId): ICommentsConfiguration | null { - let value = this._getRichEditSupport(languageId); + public getComments(languageId: string): ICommentsConfiguration | null { + let value = this.getLanguageConfiguration(languageId); if (!value) { return null; } @@ -282,20 +251,20 @@ export class LanguageConfigurationRegistryImpl { // begin characterPair - private _getCharacterPairSupport(languageId: LanguageId): CharacterPairSupport | null { - let value = this._getRichEditSupport(languageId); + private _getCharacterPairSupport(languageId: string): CharacterPairSupport | null { + let value = this.getLanguageConfiguration(languageId); if (!value) { return null; } return value.characterPair || null; } - public getAutoClosingPairs(languageId: LanguageId): AutoClosingPairs { + public getAutoClosingPairs(languageId: string): AutoClosingPairs { const characterPairSupport = this._getCharacterPairSupport(languageId); return new AutoClosingPairs(characterPairSupport ? characterPairSupport.getAutoClosingPairs() : []); } - public getAutoCloseBeforeSet(languageId: LanguageId): string { + public getAutoCloseBeforeSet(languageId: string): string { let characterPairSupport = this._getCharacterPairSupport(languageId); if (!characterPairSupport) { return CharacterPairSupport.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED; @@ -303,7 +272,7 @@ export class LanguageConfigurationRegistryImpl { return characterPairSupport.getAutoCloseBeforeSet(); } - public getSurroundingPairs(languageId: LanguageId): IAutoClosingPair[] { + public getSurroundingPairs(languageId: string): IAutoClosingPair[] { let characterPairSupport = this._getCharacterPairSupport(languageId); if (!characterPairSupport) { return []; @@ -318,18 +287,18 @@ export class LanguageConfigurationRegistryImpl { // end characterPair - public getWordDefinition(languageId: LanguageId): RegExp { - let value = this._getRichEditSupport(languageId); + public getWordDefinition(languageId: string): RegExp { + let value = this.getLanguageConfiguration(languageId); if (!value) { return ensureValidWordDefinition(null); } return ensureValidWordDefinition(value.wordDefinition || null); } - public getWordDefinitions(): [LanguageId, RegExp][] { - let result: [LanguageId, RegExp][] = []; - for (const [language, entries] of this._entries2) { - const value = entries.getRichEditSupport(); + public getWordDefinitions(): [string, RegExp][] { + let result: [string, RegExp][] = []; + for (const [language, entries] of this._entries) { + const value = entries.getResolvedConfiguration(); if (value) { result.push([language, value.wordDefinition]); } @@ -337,8 +306,8 @@ export class LanguageConfigurationRegistryImpl { return result; } - public getFoldingRules(languageId: LanguageId): FoldingRules { - let value = this._getRichEditSupport(languageId); + public getFoldingRules(languageId: string): FoldingRules { + let value = this.getLanguageConfiguration(languageId); if (!value) { return {}; } @@ -347,8 +316,8 @@ export class LanguageConfigurationRegistryImpl { // begin Indent Rules - public getIndentRulesSupport(languageId: LanguageId): IndentRulesSupport | null { - let value = this._getRichEditSupport(languageId); + public getIndentRulesSupport(languageId: string): IndentRulesSupport | null { + let value = this.getLanguageConfiguration(languageId); if (!value) { return null; } @@ -356,7 +325,7 @@ export class LanguageConfigurationRegistryImpl { } /** - * Get nearest preceiding line which doesn't match unIndentPattern or contains all whitespace. + * Get nearest preceding line which doesn't match unIndentPattern or contains all whitespace. * Result: * -1: run into the boundary of embedded languages * 0: every line above are invalid @@ -402,7 +371,7 @@ export class LanguageConfigurationRegistryImpl { return null; } - const indentRulesSupport = this.getIndentRulesSupport(model.getLanguageIdentifier().id); + const indentRulesSupport = this.getIndentRulesSupport(model.getLanguageId()); if (!indentRulesSupport) { return null; } @@ -521,12 +490,12 @@ export class LanguageConfigurationRegistryImpl { } } - public getGoodIndentForLine(autoIndent: EditorAutoIndentStrategy, virtualModel: IVirtualModel, languageId: LanguageId, lineNumber: number, indentConverter: IIndentConverter): string | null { + public getGoodIndentForLine(autoIndent: EditorAutoIndentStrategy, virtualModel: IVirtualModel, languageId: string, lineNumber: number, indentConverter: IIndentConverter): string | null { if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } - const richEditSupport = this._getRichEditSupport(languageId); + const richEditSupport = this.getLanguageConfiguration(languageId); if (!richEditSupport) { return null; } @@ -628,8 +597,8 @@ export class LanguageConfigurationRegistryImpl { getLineTokens: (lineNumber: number) => { return model.getLineTokens(lineNumber); }, - getLanguageIdentifier: () => { - return model.getLanguageIdentifier(); + getLanguageId: () => { + return model.getLanguageId(); }, getLanguageIdAtPosition: (lineNumber: number, column: number) => { return model.getLanguageIdAtPosition(lineNumber, column); @@ -723,7 +692,7 @@ export class LanguageConfigurationRegistryImpl { } public getIndentMetadata(model: ITextModel, lineNumber: number): number | null { - const indentRulesSupport = this.getIndentRulesSupport(model.getLanguageIdentifier().id); + const indentRulesSupport = this.getIndentRulesSupport(model.getLanguageId()); if (!indentRulesSupport) { return null; } @@ -739,7 +708,7 @@ export class LanguageConfigurationRegistryImpl { public getEnterAction(autoIndent: EditorAutoIndentStrategy, model: ITextModel, range: Range): CompleteEnterAction | null { const scopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber, range.startColumn); - const richEditSupport = this._getRichEditSupport(scopedLineTokens.languageId); + const richEditSupport = this.getLanguageConfiguration(scopedLineTokens.languageId); if (!richEditSupport) { return null; } @@ -820,13 +789,230 @@ export class LanguageConfigurationRegistryImpl { // end onEnter - public getBracketsSupport(languageId: LanguageId): RichEditBrackets | null { - const value = this._getRichEditSupport(languageId); + public getBracketsSupport(languageId: string): RichEditBrackets | null { + const value = this.getLanguageConfiguration(languageId); if (!value) { return null; } return value.brackets || null; } + + public getColorizedBracketPairs(languageId: string): readonly CharacterPair[] { + return this.getLanguageConfiguration(languageId)?.characterPair.getColorizedBrackets() || []; + } } export const LanguageConfigurationRegistry = new LanguageConfigurationRegistryImpl(); + +class ComposedLanguageConfiguration { + private readonly _entries: LanguageConfigurationContribution[]; + private _order: number; + private _resolved: ResolvedLanguageConfiguration | null = null; + + constructor(public readonly languageId: string) { + this._entries = []; + this._order = 0; + this._resolved = null; + } + + public register( + configuration: LanguageConfiguration, + priority: number + ): IDisposable { + const entry = new LanguageConfigurationContribution( + configuration, + priority, + ++this._order + ); + this._entries.push(entry); + this._resolved = null; + return toDisposable(() => { + for (let i = 0; i < this._entries.length; i++) { + if (this._entries[i] === entry) { + this._entries.splice(i, 1); + this._resolved = null; + break; + } + } + }); + } + + public getResolvedConfiguration(): ResolvedLanguageConfiguration | null { + if (!this._resolved) { + const config = this._resolve(); + if (config) { + this._resolved = new ResolvedLanguageConfiguration( + this.languageId, + config + ); + } + } + return this._resolved; + } + + private _resolve(): LanguageConfiguration | null { + if (this._entries.length === 0) { + return null; + } + this._entries.sort(LanguageConfigurationContribution.cmp); + return combineLanguageConfigurations(this._entries.map(e => e.configuration)); + } +} + +function combineLanguageConfigurations(configs: LanguageConfiguration[]): LanguageConfiguration { + let result: ExplicitLanguageConfiguration = { + comments: undefined, + brackets: undefined, + wordPattern: undefined, + indentationRules: undefined, + onEnterRules: undefined, + autoClosingPairs: undefined, + surroundingPairs: undefined, + autoCloseBefore: undefined, + folding: undefined, + colorizedBracketPairs: undefined, + __electricCharacterSupport: undefined, + }; + for (const entry of configs) { + result = { + comments: entry.comments || result.comments, + brackets: entry.brackets || result.brackets, + wordPattern: entry.wordPattern || result.wordPattern, + indentationRules: entry.indentationRules || result.indentationRules, + onEnterRules: entry.onEnterRules || result.onEnterRules, + autoClosingPairs: entry.autoClosingPairs || result.autoClosingPairs, + surroundingPairs: entry.surroundingPairs || result.surroundingPairs, + autoCloseBefore: entry.autoCloseBefore || result.autoCloseBefore, + folding: entry.folding || result.folding, + colorizedBracketPairs: entry.colorizedBracketPairs || result.colorizedBracketPairs, + __electricCharacterSupport: entry.__electricCharacterSupport || result.__electricCharacterSupport, + }; + } + + return result; +} + +class LanguageConfigurationContribution { + constructor( + public readonly configuration: LanguageConfiguration, + public readonly priority: number, + public readonly order: number + ) { } + + public static cmp(a: LanguageConfigurationContribution, b: LanguageConfigurationContribution) { + if (a.priority === b.priority) { + // higher order last + return a.order - b.order; + } + // higher priority last + return a.priority - b.priority; + } +} + +/** + * Immutable. +*/ +export class ResolvedLanguageConfiguration { + private _brackets: RichEditBrackets | null; + private _electricCharacter: BracketElectricCharacterSupport | null; + private readonly _onEnterSupport: OnEnterSupport | null; + + public readonly comments: ICommentsConfiguration | null; + public readonly characterPair: CharacterPairSupport; + public readonly wordDefinition: RegExp; + public readonly indentRulesSupport: IndentRulesSupport | null; + public readonly indentationRules: IndentationRule | undefined; + public readonly foldingRules: FoldingRules; + + constructor( + public readonly languageId: string, + public readonly underlyingConfig: LanguageConfiguration + ) { + this._brackets = null; + this._electricCharacter = null; + this._onEnterSupport = + this.underlyingConfig.brackets || + this.underlyingConfig.indentationRules || + this.underlyingConfig.onEnterRules + ? new OnEnterSupport(this.underlyingConfig) + : null; + this.comments = ResolvedLanguageConfiguration._handleComments(this.underlyingConfig); + this.characterPair = new CharacterPairSupport(this.underlyingConfig); + + this.wordDefinition = this.underlyingConfig.wordPattern || DEFAULT_WORD_REGEXP; + this.indentationRules = this.underlyingConfig.indentationRules; + if (this.underlyingConfig.indentationRules) { + this.indentRulesSupport = new IndentRulesSupport( + this.underlyingConfig.indentationRules + ); + } else { + this.indentRulesSupport = null; + } + this.foldingRules = this.underlyingConfig.folding || {}; + } + + public getWordDefinition(): RegExp { + return ensureValidWordDefinition(this.wordDefinition); + } + + public get brackets(): RichEditBrackets | null { + if (!this._brackets && this.underlyingConfig.brackets) { + this._brackets = new RichEditBrackets( + this.languageId, + this.underlyingConfig.brackets + ); + } + return this._brackets; + } + + public get electricCharacter(): BracketElectricCharacterSupport | null { + if (!this._electricCharacter) { + this._electricCharacter = new BracketElectricCharacterSupport( + this.brackets + ); + } + return this._electricCharacter; + } + + public onEnter( + autoIndent: EditorAutoIndentStrategy, + previousLineText: string, + beforeEnterText: string, + afterEnterText: string + ): EnterAction | null { + if (!this._onEnterSupport) { + return null; + } + return this._onEnterSupport.onEnter( + autoIndent, + previousLineText, + beforeEnterText, + afterEnterText + ); + } + + private static _handleComments( + conf: LanguageConfiguration + ): ICommentsConfiguration | null { + let commentRule = conf.comments; + if (!commentRule) { + return null; + } + + // comment configuration + let comments: ICommentsConfiguration = {}; + + if (commentRule.lineComment) { + comments.lineCommentToken = commentRule.lineComment; + } + if (commentRule.blockComment) { + let [blockStart, blockEnd] = commentRule.blockComment; + comments.blockCommentStartToken = blockStart; + comments.blockCommentEndToken = blockEnd; + } + + return comments; + } +} + +registerSingleton(ILanguageConfigurationService, LanguageConfigurationService); diff --git a/src/vs/editor/common/modes/languageFeatureRegistry.ts b/src/vs/editor/common/modes/languageFeatureRegistry.ts index e27eb68192..07f103d318 100644 --- a/src/vs/editor/common/modes/languageFeatureRegistry.ts +++ b/src/vs/editor/common/modes/languageFeatureRegistry.ts @@ -132,7 +132,7 @@ export class LanguageFeatureRegistry { let candidate = { uri: model.uri.toString(), - language: model.getLanguageIdentifier().language + language: model.getLanguageId() }; if (this._lastCandidate @@ -146,7 +146,7 @@ export class LanguageFeatureRegistry { this._lastCandidate = candidate; for (let entry of this._entries) { - entry._score = score(entry.selector, model.uri, model.getLanguageIdentifier().language, shouldSynchronizeModel(model)); + entry._score = score(entry.selector, model.uri, model.getLanguageId(), shouldSynchronizeModel(model)); if (isExclusive(entry.selector) && entry._score > 0) { // support for one exclusive selector that overwrites diff --git a/src/vs/editor/common/modes/modesRegistry.ts b/src/vs/editor/common/modes/modesRegistry.ts index cc6e463084..96284f77d2 100644 --- a/src/vs/editor/common/modes/modesRegistry.ts +++ b/src/vs/editor/common/modes/modesRegistry.ts @@ -5,7 +5,6 @@ import * as nls from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; -import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { ILanguageExtensionPoint } from 'vs/editor/common/services/modeService'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -60,7 +59,6 @@ Registry.add(Extensions.ModesRegistry, ModesRegistry); export const PLAINTEXT_MODE_ID = 'plaintext'; export const PLAINTEXT_EXTENSION = '.txt'; -export const PLAINTEXT_LANGUAGE_IDENTIFIER = new LanguageIdentifier(PLAINTEXT_MODE_ID, LanguageId.PlainText); ModesRegistry.registerLanguage({ id: PLAINTEXT_MODE_ID, @@ -68,7 +66,7 @@ ModesRegistry.registerLanguage({ aliases: [nls.localize('plainText.alias', "Plain Text"), 'text'], mimetypes: [Mimes.text] }); -LanguageConfigurationRegistry.register(PLAINTEXT_LANGUAGE_IDENTIFIER, { +LanguageConfigurationRegistry.register(PLAINTEXT_MODE_ID, { brackets: [ ['(', ')'], ['[', ']'], @@ -83,6 +81,7 @@ LanguageConfigurationRegistry.register(PLAINTEXT_LANGUAGE_IDENTIFIER, { { open: '\'', close: '\'' }, { open: '`', close: '`' }, ], + colorizedBracketPairs: [], folding: { offSide: true } diff --git a/src/vs/editor/common/modes/nullMode.ts b/src/vs/editor/common/modes/nullMode.ts index da4c083783..b7590e0f63 100644 --- a/src/vs/editor/common/modes/nullMode.ts +++ b/src/vs/editor/common/modes/nullMode.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Token, TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/token'; -import { ColorId, FontStyle, IState, LanguageId, LanguageIdentifier, MetadataConsts, StandardTokenType } from 'vs/editor/common/modes'; +import { ColorId, FontStyle, IState, LanguageId, MetadataConsts, StandardTokenType } from 'vs/editor/common/modes'; class NullStateImpl implements IState { @@ -21,10 +21,8 @@ export const NULL_STATE: IState = new NullStateImpl(); export const NULL_MODE_ID = 'vs.editor.nullMode'; -export const NULL_LANGUAGE_IDENTIFIER = new LanguageIdentifier(NULL_MODE_ID, LanguageId.Null); - -export function nullTokenize(modeId: string, buffer: string, state: IState, deltaOffset: number): TokenizationResult { - return new TokenizationResult([new Token(deltaOffset, '', modeId)], state); +export function nullTokenize(languageId: string, buffer: string, state: IState, deltaOffset: number): TokenizationResult { + return new TokenizationResult([new Token(deltaOffset, '', languageId)], state); } export function nullTokenize2(languageId: LanguageId, buffer: string, state: IState | null, deltaOffset: number): TokenizationResult2 { diff --git a/src/vs/editor/common/modes/supports.ts b/src/vs/editor/common/modes/supports.ts index 2d2231ef7f..23c18eee2e 100644 --- a/src/vs/editor/common/modes/supports.ts +++ b/src/vs/editor/common/modes/supports.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LineTokens } from 'vs/editor/common/core/lineTokens'; -import * as modes from 'vs/editor/common/modes'; +import { StandardTokenType } from 'vs/editor/common/modes'; export function createScopedLineTokens(context: LineTokens, offset: number): ScopedLineTokens { let tokenCount = context.getCount(); @@ -34,7 +34,7 @@ export function createScopedLineTokens(context: LineTokens, offset: number): Sco export class ScopedLineTokens { _scopedLineTokensBrand: void = undefined; - public readonly languageId: modes.LanguageId; + public readonly languageId: string; private readonly _actual: LineTokens; private readonly _firstTokenIndex: number; private readonly _lastTokenIndex: number; @@ -43,7 +43,7 @@ export class ScopedLineTokens { constructor( actual: LineTokens, - languageId: modes.LanguageId, + languageId: string, firstTokenIndex: number, lastTokenIndex: number, firstCharOffset: number, @@ -75,15 +75,15 @@ export class ScopedLineTokens { return this._actual.findTokenIndexAtOffset(offset + this.firstCharOffset) - this._firstTokenIndex; } - public getStandardTokenType(tokenIndex: number): modes.StandardTokenType { + public getStandardTokenType(tokenIndex: number): StandardTokenType { return this._actual.getStandardTokenType(tokenIndex + this._firstTokenIndex); } } const enum IgnoreBracketsInTokens { - value = modes.StandardTokenType.Comment | modes.StandardTokenType.String | modes.StandardTokenType.RegEx + value = StandardTokenType.Comment | StandardTokenType.String | StandardTokenType.RegEx } -export function ignoreBracketsInToken(standardTokenType: modes.StandardTokenType): boolean { +export function ignoreBracketsInToken(standardTokenType: StandardTokenType): boolean { return (standardTokenType & IgnoreBracketsInTokens.value) !== 0; } diff --git a/src/vs/editor/common/modes/supports/characterPair.ts b/src/vs/editor/common/modes/supports/characterPair.ts index 37e3386e19..8121a1d030 100644 --- a/src/vs/editor/common/modes/supports/characterPair.ts +++ b/src/vs/editor/common/modes/supports/characterPair.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAutoClosingPair, StandardAutoClosingPairConditional, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; +import { IAutoClosingPair, StandardAutoClosingPairConditional, LanguageConfiguration, CharacterPair } from 'vs/editor/common/modes/languageConfiguration'; import { ScopedLineTokens } from 'vs/editor/common/modes/supports'; export class CharacterPairSupport { @@ -14,6 +14,7 @@ export class CharacterPairSupport { private readonly _autoClosingPairs: StandardAutoClosingPairConditional[]; private readonly _surroundingPairs: IAutoClosingPair[]; private readonly _autoCloseBefore: string; + private readonly _colorizedBracketPairs: CharacterPair[]; constructor(config: LanguageConfiguration) { if (config.autoClosingPairs) { @@ -24,6 +25,20 @@ export class CharacterPairSupport { this._autoClosingPairs = []; } + if (config.colorizedBracketPairs) { + this._colorizedBracketPairs = filterValidBrackets(config.colorizedBracketPairs.map(b => [b[0], b[1]])); + } else if (config.brackets) { + this._colorizedBracketPairs = filterValidBrackets(config.brackets + .map((b) => [b[0], b[1]] as [string, string]) + // Many languages set < ... > as bracket pair, even though they also use it as comparison operator. + // This leads to problems when colorizing this bracket, so we exclude it by default. + // Languages can still override this by configuring `colorizedBracketPairs` + // https://github.com/microsoft/vscode/issues/132476 + .filter((p) => !(p[0] === '<' && p[1] === '>'))); + } else { + this._colorizedBracketPairs = []; + } + if (config.__electricCharacterSupport && config.__electricCharacterSupport.docComment) { const docComment = config.__electricCharacterSupport.docComment; // IDocComment is legacy, only partially supported @@ -57,4 +72,12 @@ export class CharacterPairSupport { public getSurroundingPairs(): IAutoClosingPair[] { return this._surroundingPairs; } + + public getColorizedBrackets(): readonly CharacterPair[] { + return this._colorizedBracketPairs; + } +} + +function filterValidBrackets(bracketPairs: [string, string][]): [string, string][] { + return bracketPairs.filter(([open, close]) => open !== '' && close !== ''); } diff --git a/src/vs/editor/common/modes/supports/richEditBrackets.ts b/src/vs/editor/common/modes/supports/richEditBrackets.ts index 4fce7baa51..dc9fdb3662 100644 --- a/src/vs/editor/common/modes/supports/richEditBrackets.ts +++ b/src/vs/editor/common/modes/supports/richEditBrackets.ts @@ -6,7 +6,6 @@ import * as strings from 'vs/base/common/strings'; import * as stringBuilder from 'vs/editor/common/core/stringBuilder'; import { Range } from 'vs/editor/common/core/range'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; import { CharacterPair } from 'vs/editor/common/modes/languageConfiguration'; interface InternalBracket { @@ -14,20 +13,72 @@ interface InternalBracket { close: string[]; } +/** + * Represents a grouping of colliding bracket pairs. + * + * Most of the times this contains a single bracket pair, + * but sometimes this contains multiple bracket pairs in cases + * where the same string appears as a closing bracket for multiple + * bracket pairs, or the same string appears an opening bracket for + * multiple bracket pairs. + * + * e.g. of a group containing a single pair: + * open: ['{'], close: ['}'] + * + * e.g. of a group containing multiple pairs: + * open: ['if', 'for'], close: ['end', 'end'] + */ export class RichEditBracket { _richEditBracketBrand: void = undefined; - readonly languageIdentifier: LanguageIdentifier; + readonly languageId: string; + /** + * A 0-based consecutive unique identifier for this bracket pair. + * If a language has 5 bracket pairs, out of which 2 are grouped together, + * it is expected that the `index` goes from 0 to 4. + */ readonly index: number; + /** + * The open sequence for each bracket pair contained in this group. + * + * The open sequence at a specific index corresponds to the + * closing sequence at the same index. + * + * [ open[i], closed[i] ] represent a bracket pair. + */ readonly open: string[]; + /** + * The close sequence for each bracket pair contained in this group. + * + * The close sequence at a specific index corresponds to the + * opening sequence at the same index. + * + * [ open[i], closed[i] ] represent a bracket pair. + */ readonly close: string[]; + /** + * A regular expression that is useful to search for this bracket pair group in a string. + * + * This regular expression is built in a way that it is aware of the other bracket + * pairs defined for the language, so it might match brackets from other groups. + * + * See the fine details in `getRegexForBracketPair`. + */ readonly forwardRegex: RegExp; + /** + * A regular expression that is useful to search for this bracket pair group in a string backwards. + * + * This regular expression is built in a way that it is aware of the other bracket + * pairs defined for the language, so it might match brackets from other groups. + * + * See the fine defails in `getReversedRegexForBracketPair`. + */ readonly reversedRegex: RegExp; private readonly _openSet: Set; private readonly _closeSet: Set; - constructor(languageIdentifier: LanguageIdentifier, index: number, open: string[], close: string[], forwardRegex: RegExp, reversedRegex: RegExp) { - this.languageIdentifier = languageIdentifier; + constructor(languageId: string, index: number, open: string[], close: string[], forwardRegex: RegExp, reversedRegex: RegExp) { + this.languageId = languageId; this.index = index; this.open = open; this.close = close; @@ -37,10 +88,16 @@ export class RichEditBracket { this._closeSet = RichEditBracket._toSet(this.close); } + /** + * Check if the provided `text` is an open bracket in this group. + */ public isOpen(text: string) { return this._openSet.has(text); } + /** + * Check if the provided `text` is a close bracket in this group. + */ public isClose(text: string) { return this._closeSet.has(text); } @@ -54,7 +111,20 @@ export class RichEditBracket { } } -function groupFuzzyBrackets(brackets: CharacterPair[]): InternalBracket[] { +/** + * Groups together brackets that have equal open or close sequences. + * + * For example, if the following brackets are defined: + * ['IF','END'] + * ['for','end'] + * ['{','}'] + * + * Then the grouped brackets would be: + * { open: ['if', 'for'], close: ['end', 'end'] } + * { open: ['{'], close: ['}'] } + * + */ +function groupFuzzyBrackets(brackets: readonly CharacterPair[]): InternalBracket[] { const N = brackets.length; brackets = brackets.map(b => [b[0].toLowerCase(), b[1].toLowerCase()]); @@ -115,19 +185,41 @@ function groupFuzzyBrackets(brackets: CharacterPair[]): InternalBracket[] { export class RichEditBrackets { _richEditBracketsBrand: void = undefined; + /** + * All groups of brackets defined for this language. + */ public readonly brackets: RichEditBracket[]; + /** + * A regular expression that is useful to search for all bracket pairs in a string. + * + * See the fine details in `getRegexForBrackets`. + */ public readonly forwardRegex: RegExp; + /** + * A regular expression that is useful to search for all bracket pairs in a string backwards. + * + * See the fine details in `getReversedRegexForBrackets`. + */ public readonly reversedRegex: RegExp; + /** + * The length (i.e. str.length) for the longest bracket pair. + */ public readonly maxBracketLength: number; + /** + * A map useful for decoding a regex match and finding which bracket group was matched. + */ public readonly textIsBracket: { [text: string]: RichEditBracket; }; + /** + * A set useful for decoding if a regex match is the open bracket of a bracket pair. + */ public readonly textIsOpenBracket: { [text: string]: boolean; }; - constructor(languageIdentifier: LanguageIdentifier, _brackets: CharacterPair[]) { + constructor(languageId: string, _brackets: readonly CharacterPair[]) { const brackets = groupFuzzyBrackets(_brackets); this.brackets = brackets.map((b, index) => { return new RichEditBracket( - languageIdentifier, + languageId, index, b.open, b.close, @@ -197,6 +289,29 @@ function unique(arr: string[]): string[] { return result; } +/** + * Create a regular expression that can be used to search forward in a piece of text + * for a group of bracket pairs. But this regex must be built in a way in which + * it is aware of the other bracket pairs defined for the language. + * + * For example, if a language contains the following bracket pairs: + * ['begin', 'end'] + * ['if', 'end if'] + * The two bracket pairs do not collide because no open or close brackets are equal. + * So the function getRegexForBracketPair is called twice, once with + * the ['begin'], ['end'] group consisting of one bracket pair, and once with + * the ['if'], ['end if'] group consiting of the other bracket pair. + * + * But there could be a situation where an occurrence of 'end if' is mistaken + * for an occurrence of 'end'. + * + * Therefore, for the bracket pair ['begin', 'end'], the regex will also + * target 'end if'. The regex will be something like: + * /(\bend if\b)|(\bend\b)|(\bif\b)/ + * + * The regex also searches for "superstrings" (other brackets that might be mistaken with the current bracket). + * + */ function getRegexForBracketPair(open: string[], close: string[], brackets: InternalBracket[], currentIndex: number): RegExp { // search in all brackets for other brackets that are a superstring of these brackets let pieces: string[] = []; @@ -211,6 +326,16 @@ function getRegexForBracketPair(open: string[], close: string[], brackets: Inter return createBracketOrRegExp(pieces); } +/** + * Matching a regular expression in JS can only be done "forwards". So JS offers natively only + * methods to find the first match of a regex in a string. But sometimes, it is useful to + * find the last match of a regex in a string. For such a situation, a nice solution is to + * simply reverse the string and then search for a reversed regex. + * + * This function also has the fine details of `getRegexForBracketPair`. For the same example + * given above, the regex produced here would look like: + * /(\bfi dne\b)|(\bdne\b)|(\bfi\b)/ + */ function getReversedRegexForBracketPair(open: string[], close: string[], brackets: InternalBracket[], currentIndex: number): RegExp { // search in all brackets for other brackets that are a superstring of these brackets let pieces: string[] = []; @@ -225,6 +350,16 @@ function getReversedRegexForBracketPair(open: string[], close: string[], bracket return createBracketOrRegExp(pieces.map(toReversedString)); } +/** + * Creates a regular expression that targets all bracket pairs. + * + * e.g. for the bracket pairs: + * ['{','}'] + * ['begin,'end'] + * ['for','end'] + * the regex would look like: + * /(\{)|(\})|(\bbegin\b)|(\bend\b)|(\bfor\b)/ + */ function getRegexForBrackets(brackets: RichEditBracket[]): RegExp { let pieces: string[] = []; for (const bracket of brackets) { @@ -239,6 +374,19 @@ function getRegexForBrackets(brackets: RichEditBracket[]): RegExp { return createBracketOrRegExp(pieces); } +/** + * Matching a regular expression in JS can only be done "forwards". So JS offers natively only + * methods to find the first match of a regex in a string. But sometimes, it is useful to + * find the last match of a regex in a string. For such a situation, a nice solution is to + * simply reverse the string and then search for a reversed regex. + * + * e.g. for the bracket pairs: + * ['{','}'] + * ['begin,'end'] + * ['for','end'] + * the regex would look like: + * /(\{)|(\})|(\bnigeb\b)|(\bdne\b)|(\brof\b)/ + */ function getReversedRegexForBrackets(brackets: RichEditBracket[]): RegExp { let pieces: string[] = []; for (const bracket of brackets) { diff --git a/src/vs/editor/common/modes/textToHtmlTokenizer.ts b/src/vs/editor/common/modes/textToHtmlTokenizer.ts index 25e229590a..092ad59cf3 100644 --- a/src/vs/editor/common/modes/textToHtmlTokenizer.ts +++ b/src/vs/editor/common/modes/textToHtmlTokenizer.ts @@ -7,7 +7,7 @@ import { CharCode } from 'vs/base/common/charCode'; import * as strings from 'vs/base/common/strings'; import { IViewLineTokens, LineTokens } from 'vs/editor/common/core/lineTokens'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { IState, LanguageId } from 'vs/editor/common/modes'; +import { ILanguageIdCodec, IState, LanguageId } from 'vs/editor/common/modes'; import { NULL_STATE, nullTokenize2 } from 'vs/editor/common/modes/nullMode'; export interface IReducedTokenizationSupport { @@ -20,8 +20,8 @@ const fallback: IReducedTokenizationSupport = { tokenize2: (buffer: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(LanguageId.Null, buffer, state, deltaOffset) }; -export function tokenizeToString(text: string, tokenizationSupport: IReducedTokenizationSupport = fallback): string { - return _tokenizeToString(text, tokenizationSupport || fallback); +export function tokenizeToString(text: string, languageIdCodec: ILanguageIdCodec, tokenizationSupport: IReducedTokenizationSupport = fallback): string { + return _tokenizeToString(text, languageIdCodec, tokenizationSupport || fallback); } export function tokenizeLineToHTML(text: string, viewLineTokens: IViewLineTokens, colorMap: string[], startOffset: number, endOffset: number, tabSize: number, useNbsp: boolean): string { @@ -29,6 +29,8 @@ export function tokenizeLineToHTML(text: string, viewLineTokens: IViewLineTokens let charIndex = startOffset; let tabsCharDelta = 0; + let prevIsSpace = true; + for (let tokenIndex = 0, tokenCount = viewLineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) { const tokenEndIndex = viewLineTokens.getEndOffset(tokenIndex); @@ -46,25 +48,35 @@ export function tokenizeLineToHTML(text: string, viewLineTokens: IViewLineTokens let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize; tabsCharDelta += insertSpacesCount - 1; while (insertSpacesCount > 0) { - partContent += useNbsp ? ' ' : ' '; + if (useNbsp && prevIsSpace) { + partContent += ' '; + prevIsSpace = false; + } else { + partContent += ' '; + prevIsSpace = true; + } insertSpacesCount--; } break; case CharCode.LessThan: partContent += '<'; + prevIsSpace = false; break; case CharCode.GreaterThan: partContent += '>'; + prevIsSpace = false; break; case CharCode.Ampersand: partContent += '&'; + prevIsSpace = false; break; case CharCode.Null: partContent += '�'; + prevIsSpace = false; break; case CharCode.UTF8_BOM: @@ -72,19 +84,28 @@ export function tokenizeLineToHTML(text: string, viewLineTokens: IViewLineTokens case CharCode.PARAGRAPH_SEPARATOR: case CharCode.NEXT_LINE: partContent += '\ufffd'; + prevIsSpace = false; break; case CharCode.CarriageReturn: // zero width space, because carriage return would introduce a line break partContent += '​'; + prevIsSpace = false; break; case CharCode.Space: - partContent += useNbsp ? ' ' : ' '; + if (useNbsp && prevIsSpace) { + partContent += ' '; + prevIsSpace = false; + } else { + partContent += ' '; + prevIsSpace = true; + } break; default: partContent += String.fromCharCode(charCode); + prevIsSpace = false; } } @@ -99,21 +120,21 @@ export function tokenizeLineToHTML(text: string, viewLineTokens: IViewLineTokens return result; } -function _tokenizeToString(text: string, tokenizationSupport: IReducedTokenizationSupport): string { +function _tokenizeToString(text: string, languageIdCodec: ILanguageIdCodec, tokenizationSupport: IReducedTokenizationSupport): string { let result = `
`; - let lines = strings.splitLines(text); + const lines = strings.splitLines(text); let currentState = tokenizationSupport.getInitialState(); for (let i = 0, len = lines.length; i < len; i++) { - let line = lines[i]; + const line = lines[i]; if (i > 0) { result += `
`; } - let tokenizationResult = tokenizationSupport.tokenize2(line, true, currentState, 0); + const tokenizationResult = tokenizationSupport.tokenize2(line, true, currentState, 0); LineTokens.convertToEndOffset(tokenizationResult.tokens, line.length); - let lineTokens = new LineTokens(tokenizationResult.tokens, line); - let viewLineTokens = lineTokens.inflate(); + const lineTokens = new LineTokens(tokenizationResult.tokens, line, languageIdCodec); + const viewLineTokens = lineTokens.inflate(); let startOffset = 0; for (let j = 0, lenJ = viewLineTokens.getCount(); j < lenJ; j++) { diff --git a/src/vs/editor/common/modes/tokenization/typescript.ts b/src/vs/editor/common/modes/tokenization/typescript.ts deleted file mode 100644 index 99248381c5..0000000000 --- a/src/vs/editor/common/modes/tokenization/typescript.ts +++ /dev/null @@ -1,304 +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 { StandardTokenType } from 'vs/editor/common/modes'; -import { CharCode } from 'vs/base/common/charCode'; - -class ParserContext { - public readonly text: string; - public readonly len: number; - public readonly tokens: number[]; - public pos: number; - - private currentTokenStartOffset: number; - private currentTokenType: StandardTokenType; - - constructor(text: string) { - this.text = text; - this.len = this.text.length; - this.tokens = []; - this.pos = 0; - this.currentTokenStartOffset = 0; - this.currentTokenType = StandardTokenType.Other; - } - - private _safeCharCodeAt(index: number): number { - if (index >= this.len) { - return CharCode.Null; - } - return this.text.charCodeAt(index); - } - - peek(distance: number = 0): number { - return this._safeCharCodeAt(this.pos + distance); - } - - next(): number { - const result = this._safeCharCodeAt(this.pos); - this.pos++; - return result; - } - - advance(distance: number): void { - this.pos += distance; - } - - eof(): boolean { - return this.pos >= this.len; - } - - beginToken(tokenType: StandardTokenType, deltaPos: number = 0): void { - this.currentTokenStartOffset = this.pos + deltaPos; - this.currentTokenType = tokenType; - } - - endToken(deltaPos: number = 0): void { - const length = this.pos + deltaPos - this.currentTokenStartOffset; - // check if it is touching previous token - if (this.tokens.length > 0) { - const previousStartOffset = this.tokens[this.tokens.length - 3]; - const previousLength = this.tokens[this.tokens.length - 2]; - const previousTokenType = this.tokens[this.tokens.length - 1]; - const previousEndOffset = previousStartOffset + previousLength; - if (this.currentTokenStartOffset === previousEndOffset && previousTokenType === this.currentTokenType) { - // extend previous token - this.tokens[this.tokens.length - 2] += length; - return; - } - } - this.tokens.push(this.currentTokenStartOffset, length, this.currentTokenType); - } -} - -export function parse(text: string): number[] { - const ctx = new ParserContext(text); - while (!ctx.eof()) { - parseRoot(ctx); - } - return ctx.tokens; -} - -function parseRoot(ctx: ParserContext): void { - let curlyCount = 0; - while (!ctx.eof()) { - const ch = ctx.peek(); - - switch (ch) { - case CharCode.SingleQuote: - parseSimpleString(ctx, CharCode.SingleQuote); - break; - case CharCode.DoubleQuote: - parseSimpleString(ctx, CharCode.DoubleQuote); - break; - case CharCode.BackTick: - parseInterpolatedString(ctx); - break; - case CharCode.Slash: - parseSlash(ctx); - break; - case CharCode.OpenCurlyBrace: - ctx.advance(1); - curlyCount++; - break; - case CharCode.CloseCurlyBrace: - ctx.advance(1); - curlyCount--; - if (curlyCount < 0) { - return; - } - break; - default: - ctx.advance(1); - } - } - -} - -function parseSimpleString(ctx: ParserContext, closingQuote: number): void { - ctx.beginToken(StandardTokenType.String); - - // skip the opening quote - ctx.advance(1); - - while (!ctx.eof()) { - const ch = ctx.next(); - if (ch === CharCode.Backslash) { - // skip \r\n or any other character following a backslash - const advanceCount = (ctx.peek() === CharCode.CarriageReturn && ctx.peek(1) === CharCode.LineFeed ? 2 : 1); - ctx.advance(advanceCount); - } else if (ch === closingQuote) { - // hit end quote, so stop - break; - } - } - - ctx.endToken(); -} - -function parseInterpolatedString(ctx: ParserContext): void { - ctx.beginToken(StandardTokenType.String); - - // skip the opening quote - ctx.advance(1); - - while (!ctx.eof()) { - const ch = ctx.next(); - if (ch === CharCode.Backslash) { - // skip \r\n or any other character following a backslash - const advanceCount = (ctx.peek() === CharCode.CarriageReturn && ctx.peek(1) === CharCode.LineFeed ? 2 : 1); - ctx.advance(advanceCount); - } else if (ch === CharCode.BackTick) { - // hit end quote, so stop - break; - } else if (ch === CharCode.DollarSign) { - if (ctx.peek() === CharCode.OpenCurlyBrace) { - ctx.advance(1); - ctx.endToken(); - parseRoot(ctx); - ctx.beginToken(StandardTokenType.String, -1); - } - } - } - - ctx.endToken(); -} - -function parseSlash(ctx: ParserContext): void { - - const nextCh = ctx.peek(1); - if (nextCh === CharCode.Asterisk) { - parseMultiLineComment(ctx); - return; - } - - if (nextCh === CharCode.Slash) { - parseSingleLineComment(ctx); - return; - } - - if (tryParseRegex(ctx)) { - return; - } - - ctx.advance(1); -} - -function tryParseRegex(ctx: ParserContext): boolean { - // See https://www.ecma-international.org/ecma-262/10.0/index.html#prod-RegularExpressionLiteral - - // TODO: avoid regex... - let contentBefore = ctx.text.substr(ctx.pos - 100, 100); - if (/[a-zA-Z0-9](\s*)$/.test(contentBefore)) { - // Cannot start after an identifier - return false; - } - - let pos = 0; - let len = ctx.len - ctx.pos; - let inClass = false; - - // skip / - pos++; - - while (pos < len) { - const ch = ctx.peek(pos++); - - if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) { - return false; - } - - if (ch === CharCode.Backslash) { - const nextCh = ctx.peek(); - if (nextCh === CharCode.CarriageReturn || nextCh === CharCode.LineFeed) { - return false; - } - // skip next character - pos++; - continue; - } - - if (inClass) { - - if (ch === CharCode.CloseSquareBracket) { - inClass = false; - continue; - } - - } else { - - if (ch === CharCode.Slash) { - // cannot be directly followed by a / - if (ctx.peek(pos) === CharCode.Slash) { - return false; - } - - // consume flags - do { - let nextCh = ctx.peek(pos); - if (nextCh >= CharCode.a && nextCh <= CharCode.z) { - pos++; - continue; - } else { - break; - } - } while (true); - - // TODO: avoid regex... - if (/^(\s*)(\.|;|\/|,|\)|\]|\}|$)/.test(ctx.text.substr(ctx.pos + pos))) { - // Must be followed by an operator of kinds - ctx.beginToken(StandardTokenType.RegEx); - ctx.advance(pos); - ctx.endToken(); - return true; - } - - return false; - } - - if (ch === CharCode.OpenSquareBracket) { - inClass = true; - continue; - } - - } - } - - return false; -} - -function parseMultiLineComment(ctx: ParserContext): void { - ctx.beginToken(StandardTokenType.Comment); - - // skip the /* - ctx.advance(2); - - while (!ctx.eof()) { - const ch = ctx.next(); - if (ch === CharCode.Asterisk) { - if (ctx.peek() === CharCode.Slash) { - ctx.advance(1); - break; - } - } - } - - ctx.endToken(); -} - -function parseSingleLineComment(ctx: ParserContext): void { - ctx.beginToken(StandardTokenType.Comment); - - // skip the // - ctx.advance(2); - - while (!ctx.eof()) { - const ch = ctx.next(); - if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) { - break; - } - } - - ctx.endToken(); -} diff --git a/src/vs/editor/common/services/editorWorkerService.ts b/src/vs/editor/common/services/editorWorkerService.ts index 1ec8ca8649..9ec58dba61 100644 --- a/src/vs/editor/common/services/editorWorkerService.ts +++ b/src/vs/editor/common/services/editorWorkerService.ts @@ -21,7 +21,6 @@ export interface IDiffComputationResult { export interface IEditorWorkerService { readonly _serviceBrand: undefined; - canComputeDiff(original: URI, modified: URI): boolean; computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise; canComputeDirtyDiff(original: URI, modified: URI): boolean; diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index f9192d8a19..295270a043 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -64,7 +64,7 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker this._logService = logService; // register default link-provider and default completions-provider - this._register(modes.LinkProviderRegistry.register('*', { + this._register(modes.LinkProviderRegistry.register({ language: '*', hasAccessToAllModels: true }, { provideLinks: (model, token) => { if (!canSyncModel(this._modelService, model.uri)) { return Promise.resolve({ links: [] }); // File too large @@ -81,10 +81,6 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker super.dispose(); } - public canComputeDiff(original: URI, modified: URI): boolean { - return (canSyncModel(this._modelService, original) && canSyncModel(this._modelService, modified)); - } - public computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise { return this._workerManager.withWorker().then(client => client.computeDiff(original, modified, ignoreTrimWhitespace, maxComputationTime)); } @@ -172,7 +168,7 @@ class WordBasedCompletionItemProvider implements modes.CompletionItemProvider { if (candidate === model) { models.unshift(candidate.uri); - } else if (config.wordBasedSuggestionsMode === 'allDocuments' || candidate.getLanguageIdentifier().id === model.getLanguageIdentifier().id) { + } else if (config.wordBasedSuggestionsMode === 'allDocuments' || candidate.getLanguageId() === model.getLanguageId()) { models.push(candidate.uri); } } @@ -182,7 +178,7 @@ class WordBasedCompletionItemProvider implements modes.CompletionItemProvider { return undefined; // File too large, no other files } - const wordDefRegExp = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + const wordDefRegExp = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageId()); const word = model.getWordAtPosition(position); const replace = !word ? Range.fromPositions(position) : new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn); const insert = replace.setEndPosition(position.lineNumber, position.column); @@ -301,12 +297,12 @@ class EditorModelManager extends Disposable { super.dispose(); } - public ensureSyncedResources(resources: URI[]): void { + public ensureSyncedResources(resources: URI[], forceLargeModels: boolean): void { for (const resource of resources) { let resourceStr = resource.toString(); if (!this._syncedModels[resourceStr]) { - this._beginModelSync(resource); + this._beginModelSync(resource, forceLargeModels); } if (this._syncedModels[resourceStr]) { this._syncedModelsLastUsedTime[resourceStr] = (new Date()).getTime(); @@ -330,12 +326,12 @@ class EditorModelManager extends Disposable { } } - private _beginModelSync(resource: URI): void { + private _beginModelSync(resource: URI, forceLargeModels: boolean): void { let model = this._modelService.getModel(resource); if (!model) { return; } - if (model.isTooLargeForSyncing()) { + if (!forceLargeModels && model.isTooLargeForSyncing()) { return; } @@ -460,18 +456,18 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien return this._modelManager; } - protected _withSyncedResources(resources: URI[]): Promise { + protected async _withSyncedResources(resources: URI[], forceLargeModels: boolean = false): Promise { if (this._disposed) { return Promise.reject(canceled()); } return this._getProxy().then((proxy) => { - this._getOrCreateModelManager(proxy).ensureSyncedResources(resources); + this._getOrCreateModelManager(proxy).ensureSyncedResources(resources, forceLargeModels); return proxy; }); } public computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise { - return this._withSyncedResources([original, modified]).then(proxy => { + return this._withSyncedResources([original, modified], /* forceLargeModels */true).then(proxy => { return proxy.computeDiff(original.toString(), modified.toString(), ignoreTrimWhitespace, maxComputationTime); }); } @@ -507,7 +503,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien if (!model) { return Promise.resolve(null); } - let wordDefRegExp = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + let wordDefRegExp = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageId()); let wordDef = wordDefRegExp.source; let wordDefFlags = regExpFlags(wordDefRegExp); return proxy.computeWordRanges(resource.toString(), range, wordDef, wordDefFlags); @@ -520,7 +516,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien if (!model) { return null; } - let wordDefRegExp = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + let wordDefRegExp = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageId()); let wordDef = wordDefRegExp.source; let wordDefFlags = regExpFlags(wordDefRegExp); return proxy.navigateValueSet(resource.toString(), range, up, wordDef, wordDefFlags); diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index b4f248b404..64ebc0436c 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -64,7 +64,7 @@ export function getIconClassesForModeId(modeId: string): string[] { return ['file-icon', `${cssEscape(modeId)}-lang-file-icon`]; } -export function detectModeId(modelService: IModelService, modeService: IModeService, resource: uri): string | null { +function detectModeId(modelService: IModelService, modeService: IModeService, resource: uri): string | null { if (!resource) { return null; // we need a resource at least } @@ -85,7 +85,7 @@ export function detectModeId(modelService: IModelService, modeService: IModeServ else { const model = modelService.getModel(resource); if (model) { - modeId = model.getModeId(); + modeId = model.getLanguageId(); } } diff --git a/src/vs/editor/common/services/getSemanticTokens.ts b/src/vs/editor/common/services/getSemanticTokens.ts index 2e1d06c3d7..e8b2f22393 100644 --- a/src/vs/editor/common/services/getSemanticTokens.ts +++ b/src/vs/editor/common/services/getSemanticTokens.ts @@ -23,30 +23,111 @@ export function isSemanticTokensEdits(v: SemanticTokens | SemanticTokensEdits): return v && Array.isArray((v).edits); } -export interface IDocumentSemanticTokensResult { - provider: DocumentSemanticTokensProvider; - request: Promise; +export class DocumentSemanticTokensResult { + constructor( + public readonly provider: DocumentSemanticTokensProvider, + public readonly tokens: SemanticTokens | SemanticTokensEdits | null, + ) { } } -export function getDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): IDocumentSemanticTokensResult | null { - const provider = _getDocumentSemanticTokensProvider(model); - if (!provider) { - return null; +export function hasDocumentSemanticTokensProvider(model: ITextModel): boolean { + return DocumentSemanticTokensProviderRegistry.has(model); +} + +function getDocumentSemanticTokensProviders(model: ITextModel): DocumentSemanticTokensProvider[] { + const groups = DocumentSemanticTokensProviderRegistry.orderedGroups(model); + return (groups.length > 0 ? groups[0] : []); +} + +export async function getDocumentSemanticTokens(model: ITextModel, lastProvider: DocumentSemanticTokensProvider | null, lastResultId: string | null, token: CancellationToken): Promise { + const providers = getDocumentSemanticTokensProviders(model); + + // Get tokens from all providers at the same time. + const results = await Promise.all(providers.map(async (provider) => { + let result: SemanticTokens | SemanticTokensEdits | null | undefined; + try { + result = await provider.provideDocumentSemanticTokens(model, (provider === lastProvider ? lastResultId : null), token); + } catch (err) { + onUnexpectedExternalError(err); + result = null; + } + + if (!result || (!isSemanticTokens(result) && !isSemanticTokensEdits(result))) { + result = null; + } + + return new DocumentSemanticTokensResult(provider, result); + })); + + // Try to return the first result with actual tokens + for (const result of results) { + if (result.tokens) { + return result; + } } - return { - provider: provider, - request: Promise.resolve(provider.provideDocumentSemanticTokens(model, lastResultId, token)) - }; + + // Return the first result, even if it doesn't have tokens + if (results.length > 0) { + return results[0]; + } + + return null; } -function _getDocumentSemanticTokensProvider(model: ITextModel): DocumentSemanticTokensProvider | null { - const result = DocumentSemanticTokensProviderRegistry.ordered(model); +function _getDocumentSemanticTokensProviderHighestGroup(model: ITextModel): DocumentSemanticTokensProvider[] | null { + const result = DocumentSemanticTokensProviderRegistry.orderedGroups(model); return (result.length > 0 ? result[0] : null); } -export function getDocumentRangeSemanticTokensProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null { - const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model); - return (result.length > 0 ? result[0] : null); +class DocumentRangeSemanticTokensResult { + constructor( + public readonly provider: DocumentRangeSemanticTokensProvider, + public readonly tokens: SemanticTokens | null, + ) { } +} + +export function hasDocumentRangeSemanticTokensProvider(model: ITextModel): boolean { + return DocumentRangeSemanticTokensProviderRegistry.has(model); +} + +function getDocumentRangeSemanticTokensProviders(model: ITextModel): DocumentRangeSemanticTokensProvider[] { + const groups = DocumentRangeSemanticTokensProviderRegistry.orderedGroups(model); + return (groups.length > 0 ? groups[0] : []); +} + +export async function getDocumentRangeSemanticTokens(model: ITextModel, range: Range, token: CancellationToken): Promise { + const providers = getDocumentRangeSemanticTokensProviders(model); + + // Get tokens from all providers at the same time. + const results = await Promise.all(providers.map(async (provider) => { + let result: SemanticTokens | null | undefined; + try { + result = await provider.provideDocumentRangeSemanticTokens(model, range, token); + } catch (err) { + onUnexpectedExternalError(err); + result = null; + } + + if (!result || !isSemanticTokens(result)) { + result = null; + } + + return new DocumentRangeSemanticTokensResult(provider, result); + })); + + // Try to return the first result with actual tokens + for (const result of results) { + if (result.tokens) { + return result; + } + } + + // Return the first result, even if it doesn't have tokens + if (results.length > 0) { + return results[0]; + } + + return null; } CommandsRegistry.registerCommand('_provideDocumentSemanticTokensLegend', async (accessor, ...args): Promise => { @@ -58,13 +139,13 @@ CommandsRegistry.registerCommand('_provideDocumentSemanticTokensLegend', async ( return undefined; } - const provider = _getDocumentSemanticTokensProvider(model); - if (!provider) { + const providers = _getDocumentSemanticTokensProviderHighestGroup(model); + if (!providers) { // there is no provider => fall back to a document range semantic tokens provider return accessor.get(ICommandService).executeCommand('_provideDocumentRangeSemanticTokensLegend', uri); } - return provider.getLegend(); + return providers[0].getLegend(); }); CommandsRegistry.registerCommand('_provideDocumentSemanticTokens', async (accessor, ...args): Promise => { @@ -76,39 +157,35 @@ CommandsRegistry.registerCommand('_provideDocumentSemanticTokens', async (access return undefined; } - const r = getDocumentSemanticTokens(model, null, CancellationToken.None); - if (!r) { + if (!hasDocumentSemanticTokensProvider(model)) { // there is no provider => fall back to a document range semantic tokens provider return accessor.get(ICommandService).executeCommand('_provideDocumentRangeSemanticTokens', uri, model.getFullModelRange()); } - const { provider, request } = r; - - let result: SemanticTokens | SemanticTokensEdits | null | undefined; - try { - result = await request; - } catch (err) { - onUnexpectedExternalError(err); + const r = await getDocumentSemanticTokens(model, null, null, CancellationToken.None); + if (!r) { return undefined; } - if (!result || !isSemanticTokens(result)) { + const { provider, tokens } = r; + + if (!tokens || !isSemanticTokens(tokens)) { return undefined; } const buff = encodeSemanticTokensDto({ id: 0, type: 'full', - data: result.data + data: tokens.data }); - if (result.resultId) { - provider.releaseDocumentSemanticTokens(result.resultId); + if (tokens.resultId) { + provider.releaseDocumentSemanticTokens(tokens.resultId); } return buff; }); CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokensLegend', async (accessor, ...args): Promise => { - const [uri] = args; + const [uri, range] = args; assertType(uri instanceof URI); const model = accessor.get(IModelService).getModel(uri); @@ -116,12 +193,31 @@ CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokensLegend', as return undefined; } - const provider = getDocumentRangeSemanticTokensProvider(model); - if (!provider) { + const providers = getDocumentRangeSemanticTokensProviders(model); + if (providers.length === 0) { + // no providers return undefined; } - return provider.getLegend(); + if (providers.length === 1) { + // straight forward case, just a single provider + return providers[0].getLegend(); + } + + if (!range || !Range.isIRange(range)) { + // if no range is provided, we cannot support multiple providers + // as we cannot fall back to the one which would give results + // => return the first legend for backwards compatibility and print a warning + console.warn(`provideDocumentRangeSemanticTokensLegend might be out-of-sync with provideDocumentRangeSemanticTokens unless a range argument is passed in`); + return providers[0].getLegend(); + } + + const result = await getDocumentRangeSemanticTokens(model, Range.lift(range), CancellationToken.None); + if (!result) { + return undefined; + } + + return result.provider.getLegend(); }); CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokens', async (accessor, ...args): Promise => { @@ -134,27 +230,15 @@ CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokens', async (a return undefined; } - const provider = getDocumentRangeSemanticTokensProvider(model); - if (!provider) { - // there is no provider - return undefined; - } - - let result: SemanticTokens | null | undefined; - try { - result = await provider.provideDocumentRangeSemanticTokens(model, Range.lift(range), CancellationToken.None); - } catch (err) { - onUnexpectedExternalError(err); - return undefined; - } - - if (!result || !isSemanticTokens(result)) { + const result = await getDocumentRangeSemanticTokens(model, Range.lift(range), CancellationToken.None); + if (!result || !result.tokens) { + // there is no provider or it didn't return tokens return undefined; } return encodeSemanticTokensDto({ id: 0, type: 'full', - data: result.data + data: result.tokens.data }); }); diff --git a/src/vs/editor/common/services/languagesRegistry.ts b/src/vs/editor/common/services/languagesRegistry.ts index b40dad2267..e0e63172c7 100644 --- a/src/vs/editor/common/services/languagesRegistry.ts +++ b/src/vs/editor/common/services/languagesRegistry.ts @@ -9,9 +9,9 @@ import { Disposable } from 'vs/base/common/lifecycle'; import * as mime from 'vs/base/common/mime'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; -import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; -import { NULL_LANGUAGE_IDENTIFIER, NULL_MODE_ID } from 'vs/editor/common/modes/nullMode'; +import { ILanguageIdCodec, LanguageId } from 'vs/editor/common/modes'; +import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { NULL_MODE_ID } from 'vs/editor/common/modes/nullMode'; import { ILanguageExtensionPoint } from 'vs/editor/common/services/modeService'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -19,7 +19,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; const hasOwnProperty = Object.prototype.hasOwnProperty; export interface IResolvedLanguage { - identifier: LanguageIdentifier; + identifier: string; name: string | null; mimetypes: string[]; aliases: string[]; @@ -28,31 +28,62 @@ export interface IResolvedLanguage { configurationFiles: URI[]; } +export class LanguageIdCodec implements ILanguageIdCodec { + + private _nextLanguageId: number; + private readonly _languageIdToLanguage: string[] = []; + private readonly _languageToLanguageId = new Map(); + + constructor() { + this._register(NULL_MODE_ID, LanguageId.Null); + this._register(PLAINTEXT_MODE_ID, LanguageId.PlainText); + this._nextLanguageId = 2; + } + + private _register(language: string, languageId: LanguageId): void { + this._languageIdToLanguage[languageId] = language; + this._languageToLanguageId.set(language, languageId); + } + + public register(language: string): void { + if (this._languageToLanguageId.has(language)) { + return; + } + const languageId = this._nextLanguageId++; + this._register(language, languageId); + } + + public encodeLanguageId(languageId: string): LanguageId { + return this._languageToLanguageId.get(languageId) || LanguageId.Null; + } + + public decodeLanguageId(languageId: LanguageId): string { + return this._languageIdToLanguage[languageId] || NULL_MODE_ID; + } +} + export class LanguagesRegistry extends Disposable { + static instanceCount = 0; + private readonly _onDidChange: Emitter = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; private readonly _warnOnOverwrite: boolean; - private _nextLanguageId2: number; - private readonly _languageIdToLanguage: string[]; - private readonly _languageToLanguageId: { [id: string]: number; }; - + public readonly languageIdCodec: LanguageIdCodec; private _languages: { [id: string]: IResolvedLanguage; }; - private _mimeTypesMap: { [mimeType: string]: LanguageIdentifier; }; - private _nameMap: { [name: string]: LanguageIdentifier; }; - private _lowercaseNameMap: { [name: string]: LanguageIdentifier; }; + private _mimeTypesMap: { [mimeType: string]: string; }; + private _nameMap: { [name: string]: string; }; + private _lowercaseNameMap: { [name: string]: string; }; constructor(useModesRegistry = true, warnOnOverwrite = false) { super(); + LanguagesRegistry.instanceCount++; this._warnOnOverwrite = warnOnOverwrite; - this._nextLanguageId2 = 1; - this._languageIdToLanguage = []; - this._languageToLanguageId = Object.create(null); - + this.languageIdCodec = new LanguageIdCodec(); this._languages = {}; this._mimeTypesMap = {}; this._nameMap = {}; @@ -60,16 +91,25 @@ export class LanguagesRegistry extends Disposable { if (useModesRegistry) { this._initializeFromRegistry(); - this._register(ModesRegistry.onDidChangeLanguages((m) => this._initializeFromRegistry())); + this._register(ModesRegistry.onDidChangeLanguages((m) => { + // console.log(`onDidChangeLanguages - inst count: ${LanguagesRegistry.instanceCount}`); + this._initializeFromRegistry(); + })); } } + override dispose() { + LanguagesRegistry.instanceCount--; + super.dispose(); + } + private _initializeFromRegistry(): void { this._languages = {}; this._mimeTypesMap = {}; this._nameMap = {}; this._lowercaseNameMap = {}; + mime.clearTextMimes(); const desc = ModesRegistry.getLanguages(); this._registerLanguages(desc); } @@ -102,18 +142,6 @@ export class LanguagesRegistry extends Disposable { this._onDidChange.fire(); } - private _getLanguageId(language: string): number { - if (this._languageToLanguageId[language]) { - return this._languageToLanguageId[language]; - } - - const languageId = this._nextLanguageId2++; - this._languageIdToLanguage[languageId] = language; - this._languageToLanguageId[language] = languageId; - - return languageId; - } - private _registerLanguage(lang: ILanguageExtensionPoint): void { const langId = lang.id; @@ -121,9 +149,9 @@ export class LanguagesRegistry extends Disposable { if (hasOwnProperty.call(this._languages, langId)) { resolvedLanguage = this._languages[langId]; } else { - const languageId = this._getLanguageId(langId); + this.languageIdCodec.register(langId); resolvedLanguage = { - identifier: new LanguageIdentifier(langId, languageId), + identifier: langId, name: null, mimetypes: [], aliases: [], @@ -246,32 +274,32 @@ export class LanguagesRegistry extends Disposable { return Object.keys(this._nameMap); } - public getLanguageName(modeId: string): string | null { - if (!hasOwnProperty.call(this._languages, modeId)) { + public getLanguageName(languageId: string): string | null { + if (!hasOwnProperty.call(this._languages, languageId)) { return null; } - return this._languages[modeId].name; + return this._languages[languageId].name; } public getModeIdForLanguageNameLowercase(languageNameLower: string): string | null { if (!hasOwnProperty.call(this._lowercaseNameMap, languageNameLower)) { return null; } - return this._lowercaseNameMap[languageNameLower].language; + return this._lowercaseNameMap[languageNameLower]; } - public getConfigurationFiles(modeId: string): URI[] { - if (!hasOwnProperty.call(this._languages, modeId)) { + public getConfigurationFiles(languageId: string): URI[] { + if (!hasOwnProperty.call(this._languages, languageId)) { return []; } - return this._languages[modeId].configurationFiles || []; + return this._languages[languageId].configurationFiles || []; } - public getMimeForMode(modeId: string): string | null { - if (!hasOwnProperty.call(this._languages, modeId)) { + public getMimeForMode(languageId: string): string | null { + if (!hasOwnProperty.call(this._languages, languageId)) { return null; } - const language = this._languages[modeId]; + const language = this._languages[languageId]; return (language.mimetypes[0] || null); } @@ -286,45 +314,36 @@ export class LanguagesRegistry extends Disposable { map((mimeTypeOrId) => mimeTypeOrId.trim()). map((mimeTypeOrId) => { if (hasOwnProperty.call(this._mimeTypesMap, mimeTypeOrId)) { - return this._mimeTypesMap[mimeTypeOrId].language; + return this._mimeTypesMap[mimeTypeOrId]; } return mimeTypeOrId; }). - filter((modeId) => { - return hasOwnProperty.call(this._languages, modeId); + filter((languageId) => { + return hasOwnProperty.call(this._languages, languageId); }) ); } - public getLanguageIdentifier(_modeId: string | LanguageId): LanguageIdentifier | null { - if (_modeId === NULL_MODE_ID || _modeId === LanguageId.Null) { - return NULL_LANGUAGE_IDENTIFIER; + public validateLanguageId(languageId: string | null): string | null { + if (!languageId || languageId === NULL_MODE_ID) { + return NULL_MODE_ID; } - let modeId: string; - if (typeof _modeId === 'string') { - modeId = _modeId; - } else { - modeId = this._languageIdToLanguage[_modeId]; - if (!modeId) { - return null; - } - } - - if (!hasOwnProperty.call(this._languages, modeId)) { + if (!hasOwnProperty.call(this._languages, languageId)) { return null; } - return this._languages[modeId].identifier; + + return languageId; } - public getModeIdsFromLanguageName(languageName: string): string[] { + public getModeIdFromLanguageName(languageName: string): string | null { if (!languageName) { - return []; + return null; } if (hasOwnProperty.call(this._nameMap, languageName)) { - return [this._nameMap[languageName].language]; + return this._nameMap[languageName]; } - return []; + return null; } public getModeIdsFromFilepathOrFirstLine(resource: URI | null, firstLine?: string): string[] { @@ -340,7 +359,7 @@ export class LanguagesRegistry extends Disposable { return []; } const languageId = this._nameMap[languageName]; - return this._languages[languageId.language].extensions; + return this._languages[languageId].extensions; } public getFilenames(languageName: string): string[] { @@ -348,6 +367,6 @@ export class LanguagesRegistry extends Disposable { return []; } const languageId = this._nameMap[languageName]; - return this._languages[languageId.language].filenames; + return this._languages[languageId].filenames; } } diff --git a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index da79c11478..39b78982fc 100644 --- a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -151,35 +151,7 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor ret = ret.setEndPosition(ret.startLineNumber, ret.startColumn + 2); } - ret = model.validateRange(ret); - - if (ret.isEmpty()) { - let word = model.getWordAtPosition(ret.getStartPosition()); - if (word) { - ret = new Range(ret.startLineNumber, word.startColumn, ret.endLineNumber, word.endColumn); - } else { - let maxColumn = model.getLineLastNonWhitespaceColumn(ret.startLineNumber) || - model.getLineMaxColumn(ret.startLineNumber); - - if (maxColumn === 1) { - // empty line - // console.warn('marker on empty line:', marker); - } else if (ret.endColumn >= maxColumn) { - // behind eol - ret = new Range(ret.startLineNumber, maxColumn - 1, ret.endLineNumber, maxColumn); - } else { - // extend marker to width = 1 - ret = new Range(ret.startLineNumber, ret.startColumn, ret.endLineNumber, ret.endColumn + 1); - } - } - } else if (rawMarker.endColumn === Number.MAX_VALUE && rawMarker.startColumn === 1 && ret.startLineNumber === ret.endLineNumber) { - let minColumn = model.getLineFirstNonWhitespaceColumn(rawMarker.startLineNumber); - if (minColumn < ret.endColumn) { - ret = new Range(ret.startLineNumber, minColumn, ret.endLineNumber, ret.endColumn); - rawMarker.startColumn = minColumn; - } - } - return ret; + return model.validateRange(ret); } private _createDecorationOption(marker: IMarker): IModelDecorationOptions { diff --git a/src/vs/editor/common/services/modeService.ts b/src/vs/editor/common/services/modeService.ts index 0fec406ac6..eb9b8642f3 100644 --- a/src/vs/editor/common/services/modeService.ts +++ b/src/vs/editor/common/services/modeService.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { IMode, LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; +import { ILanguageIdCodec } from 'vs/editor/common/modes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const IModeService = createDecorator('modeService'); @@ -22,14 +22,16 @@ export interface ILanguageExtensionPoint { } export interface ILanguageSelection { - readonly languageIdentifier: LanguageIdentifier; - readonly onDidChange: Event; + readonly languageId: string; + readonly onDidChange: Event; } export interface IModeService { readonly _serviceBrand: undefined; - onDidCreateMode: Event; + readonly languageIdCodec: ILanguageIdCodec; + + onDidEncounterLanguage: Event; onLanguagesMaybeChanged: Event; // --- reading @@ -38,13 +40,13 @@ export interface IModeService { getRegisteredLanguageNames(): string[]; getExtensions(alias: string): string[]; getFilenames(alias: string): string[]; - getMimeForMode(modeId: string): string | null; - getLanguageName(modeId: string): string | null; + getMimeForMode(languageId: string): string | null; + getLanguageName(languageId: string): string | null; getModeIdForLanguageName(alias: string): string | null; getModeIdByFilepathOrFirstLine(resource: URI, firstLine?: string): string | null; getModeId(commaSeparatedMimetypesOrCommaSeparatedIds: string): string | null; - getLanguageIdentifier(modeId: string | LanguageId): LanguageIdentifier | null; - getConfigurationFiles(modeId: string): URI[]; + validateLanguageId(languageId: string): string | null; + getConfigurationFiles(languageId: string): URI[]; // --- instantiation create(commaSeparatedMimetypesOrCommaSeparatedIds: string | undefined): ILanguageSelection; diff --git a/src/vs/editor/common/services/modeServiceImpl.ts b/src/vs/editor/common/services/modeServiceImpl.ts index 49b8bab9ca..e566dbb924 100644 --- a/src/vs/editor/common/services/modeServiceImpl.ts +++ b/src/vs/editor/common/services/modeServiceImpl.ts @@ -6,27 +6,26 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IMode, LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; -import { FrankensteinMode } from 'vs/editor/common/modes/abstractMode'; -import { NULL_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/nullMode'; +import { NULL_MODE_ID } from 'vs/editor/common/modes/nullMode'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; import { ILanguageSelection, IModeService } from 'vs/editor/common/services/modeService'; import { firstOrDefault } from 'vs/base/common/arrays'; +import { ILanguageIdCodec } from 'vs/editor/common/modes'; class LanguageSelection implements ILanguageSelection { - public languageIdentifier: LanguageIdentifier; + public languageId: string; - private readonly _selector: () => LanguageIdentifier; - private readonly _onDidChange: Emitter; - public readonly onDidChange: Event; + private readonly _selector: () => string; + private readonly _onDidChange: Emitter; + public readonly onDidChange: Event; - constructor(onLanguagesMaybeChanged: Event, selector: () => LanguageIdentifier) { + constructor(onLanguagesMaybeChanged: Event, selector: () => string) { this._selector = selector; - this.languageIdentifier = this._selector(); + this.languageId = this._selector(); let listener: IDisposable; - this._onDidChange = new Emitter({ + this._onDidChange = new Emitter({ onFirstListenerAdd: () => { listener = onLanguagesMaybeChanged(() => this._evaluate()); }, @@ -38,38 +37,43 @@ class LanguageSelection implements ILanguageSelection { } private _evaluate(): void { - let languageIdentifier = this._selector(); - if (languageIdentifier.id === this.languageIdentifier.id) { + const languageId = this._selector(); + if (languageId === this.languageId) { // no change return; } - this.languageIdentifier = languageIdentifier; - this._onDidChange.fire(this.languageIdentifier); + this.languageId = languageId; + this._onDidChange.fire(this.languageId); } } export class ModeServiceImpl extends Disposable implements IModeService { public _serviceBrand: undefined; - private readonly _instantiatedModes: { [modeId: string]: IMode; }; - private readonly _registry: LanguagesRegistry; + static instanceCount = 0; - private readonly _onDidCreateMode = this._register(new Emitter()); - public readonly onDidCreateMode: Event = this._onDidCreateMode.event; + private readonly _encounteredLanguages: Set; + private readonly _registry: LanguagesRegistry; + public readonly languageIdCodec: ILanguageIdCodec; + + private readonly _onDidEncounterLanguage = this._register(new Emitter()); + public readonly onDidEncounterLanguage: Event = this._onDidEncounterLanguage.event; protected readonly _onLanguagesMaybeChanged = this._register(new Emitter({ leakWarningThreshold: 200 /* https://github.com/microsoft/vscode/issues/119968 */ })); public readonly onLanguagesMaybeChanged: Event = this._onLanguagesMaybeChanged.event; constructor(warnOnOverwrite = false) { super(); - this._instantiatedModes = {}; - + ModeServiceImpl.instanceCount++; + this._encounteredLanguages = new Set(); this._registry = this._register(new LanguagesRegistry(true, warnOnOverwrite)); + this.languageIdCodec = this._registry.languageIdCodec; this._register(this._registry.onDidChange(() => this._onLanguagesMaybeChanged.fire())); } - protected _onReady(): Promise { - return Promise.resolve(true); + public override dispose(): void { + ModeServiceImpl.instanceCount--; + super.dispose(); } public isRegisteredMode(mimetypeOrModeId: string): boolean { @@ -92,12 +96,12 @@ export class ModeServiceImpl extends Disposable implements IModeService { return this._registry.getFilenames(alias); } - public getMimeForMode(modeId: string): string | null { - return this._registry.getMimeForMode(modeId); + public getMimeForMode(languageId: string): string | null { + return this._registry.getMimeForMode(languageId); } - public getLanguageName(modeId: string): string | null { - return this._registry.getLanguageName(modeId); + public getLanguageName(languageId: string): string | null { + return this._registry.getLanguageName(languageId); } public getModeIdForLanguageName(alias: string): string | null { @@ -114,66 +118,59 @@ export class ModeServiceImpl extends Disposable implements IModeService { return firstOrDefault(modeIds, null); } - public getLanguageIdentifier(modeId: string | LanguageId): LanguageIdentifier | null { - return this._registry.getLanguageIdentifier(modeId); + public validateLanguageId(languageId: string | null): string | null { + return this._registry.validateLanguageId(languageId); } - public getConfigurationFiles(modeId: string): URI[] { - return this._registry.getConfigurationFiles(modeId); + public getConfigurationFiles(languageId: string): URI[] { + return this._registry.getConfigurationFiles(languageId); } // --- instantiation public create(commaSeparatedMimetypesOrCommaSeparatedIds: string | undefined): ILanguageSelection { return new LanguageSelection(this.onLanguagesMaybeChanged, () => { - const modeId = this.getModeId(commaSeparatedMimetypesOrCommaSeparatedIds); - return this._createModeAndGetLanguageIdentifier(modeId); + const languageId = this.getModeId(commaSeparatedMimetypesOrCommaSeparatedIds); + return this._createModeAndGetLanguageIdentifier(languageId); }); } public createByLanguageName(languageName: string): ILanguageSelection { return new LanguageSelection(this.onLanguagesMaybeChanged, () => { - const modeId = this._getModeIdByLanguageName(languageName); - return this._createModeAndGetLanguageIdentifier(modeId); + const languageId = this._getModeIdByLanguageName(languageName); + return this._createModeAndGetLanguageIdentifier(languageId); }); } public createByFilepathOrFirstLine(resource: URI | null, firstLine?: string): ILanguageSelection { return new LanguageSelection(this.onLanguagesMaybeChanged, () => { - const modeId = this.getModeIdByFilepathOrFirstLine(resource, firstLine); - return this._createModeAndGetLanguageIdentifier(modeId); + const languageId = this.getModeIdByFilepathOrFirstLine(resource, firstLine); + return this._createModeAndGetLanguageIdentifier(languageId); }); } - private _createModeAndGetLanguageIdentifier(modeId: string | null): LanguageIdentifier { + private _createModeAndGetLanguageIdentifier(languageId: string | null): string { // Fall back to plain text if no mode was found - const languageIdentifier = this.getLanguageIdentifier(modeId || 'plaintext') || NULL_LANGUAGE_IDENTIFIER; - this._getOrCreateMode(languageIdentifier.language); - return languageIdentifier; + const validLanguageId = this.validateLanguageId(languageId || 'plaintext') || NULL_MODE_ID; + this._getOrCreateMode(validLanguageId); + return validLanguageId; } public triggerMode(commaSeparatedMimetypesOrCommaSeparatedIds: string): void { - const modeId = this.getModeId(commaSeparatedMimetypesOrCommaSeparatedIds); + const languageId = this.getModeId(commaSeparatedMimetypesOrCommaSeparatedIds); // Fall back to plain text if no mode was found - this._getOrCreateMode(modeId || 'plaintext'); - } - - public waitForLanguageRegistration(): Promise { - return this._onReady().then(() => { }); + this._getOrCreateMode(languageId || 'plaintext'); } private _getModeIdByLanguageName(languageName: string): string | null { - const modeIds = this._registry.getModeIdsFromLanguageName(languageName); - return firstOrDefault(modeIds, null); + return this._registry.getModeIdFromLanguageName(languageName); } - private _getOrCreateMode(modeId: string): IMode { - if (!this._instantiatedModes.hasOwnProperty(modeId)) { - let languageIdentifier = this.getLanguageIdentifier(modeId) || NULL_LANGUAGE_IDENTIFIER; - this._instantiatedModes[modeId] = new FrankensteinMode(languageIdentifier); - - this._onDidCreateMode.fire(this._instantiatedModes[modeId]); + private _getOrCreateMode(languageId: string): void { + if (!this._encounteredLanguages.has(languageId)) { + this._encounteredLanguages.add(languageId); + const validLanguageId = this.validateLanguageId(languageId) || NULL_MODE_ID; + this._onDidEncounterLanguage.fire(validLanguageId); } - return this._instantiatedModes[modeId]; } } diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index c5038e7386..5320836968 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -14,9 +14,9 @@ import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { IModelLanguageChangedEvent, IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { LanguageIdentifier, DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits } from 'vs/editor/common/modes'; -import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry'; -import { ILanguageSelection } from 'vs/editor/common/services/modeService'; +import { DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits } from 'vs/editor/common/modes'; +import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { ILanguageSelection, IModeService } from 'vs/editor/common/services/modeService'; import { IModelService, DocumentTokensProvider } from 'vs/editor/common/services/modelService'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -29,8 +29,9 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { EditStackElement, isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; -import { getDocumentSemanticTokens, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/common/services/getSemanticTokens'; +import { getDocumentSemanticTokens, hasDocumentSemanticTokensProvider, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/common/services/getSemanticTokens'; import { equals } from 'vs/base/common/objects'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; export interface IEditorSemanticHighlightingOptions { enabled: true | false | 'configuredByTheme'; @@ -89,8 +90,8 @@ class ModelData implements IDisposable { public setLanguage(languageSelection: ILanguageSelection): void { this._disposeLanguageSelection(); this._languageSelection = languageSelection; - this._languageSelectionListener = this._languageSelection.onDidChange(() => this.model.setMode(languageSelection.languageIdentifier)); - this.model.setMode(languageSelection.languageIdentifier); + this._languageSelectionListener = this._languageSelection.onDidChange(() => this.model.setMode(languageSelection.languageId)); + this.model.setMode(languageSelection.languageId); } } @@ -130,16 +131,6 @@ class DisposedModelInfo { ) { } } -function schemaShouldMaintainUndoRedoElements(resource: URI) { - return ( - resource.scheme === Schemas.file - || resource.scheme === Schemas.vscodeRemote - || resource.scheme === Schemas.userData - || resource.scheme === Schemas.vscodeNotebookCell - || resource.scheme === 'fake-fs' // for tests - ); -} - export class ModelServiceImpl extends Disposable implements IModelService { public static MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK = 20 * 1024 * 1024; @@ -171,13 +162,15 @@ export class ModelServiceImpl extends Disposable implements IModelService { @IThemeService private readonly _themeService: IThemeService, @ILogService private readonly _logService: ILogService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + @IModeService private readonly _modeService: IModeService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService ) { super(); this._modelCreationOptionsByLanguageAndResource = Object.create(null); this._models = {}; this._disposedModels = new Map(); this._disposedModelsHeapSize = 0; - this._semanticStyling = this._register(new SemanticStyling(this._themeService, this._logService)); + this._semanticStyling = this._register(new SemanticStyling(this._themeService, this._modeService, this._logService)); this._register(this._configurationService.onDidChangeConfiguration(() => this._updateModelOptions())); this._updateModelOptions(); @@ -294,7 +287,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { for (let i = 0, len = keys.length; i < len; i++) { const modelId = keys[i]; const modelData = this._models[modelId]; - const language = modelData.model.getLanguageIdentifier().language; + const language = modelData.model.getLanguageId(); const uri = modelData.model.uri; const oldOptions = oldOptionsByLanguageAndResource[language + uri]; const newOptions = this.getCreationOptions(language, uri, modelData.model.isForSimpleWidget); @@ -372,10 +365,18 @@ export class ModelServiceImpl extends Disposable implements IModelService { } } - private _createModelData(value: string | ITextBufferFactory, languageIdentifier: LanguageIdentifier, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { + private _createModelData(value: string | ITextBufferFactory, languageId: string, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { // create & save the model - const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget); - const model: TextModel = new TextModel(value, options, languageIdentifier, resource, this._undoRedoService); + const options = this.getCreationOptions(languageId, resource, isForSimpleWidget); + const model: TextModel = new TextModel( + value, + options, + languageId, + resource, + this._undoRedoService, + this._modeService, + this._languageConfigurationService, + ); if (resource && this._disposedModels.has(MODEL_ID(resource))) { const disposedModelData = this._removeDisposedModel(resource)!; const elements = this._undoRedoService.getElements(resource); @@ -421,7 +422,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { } public updateModel(model: ITextModel, value: string | ITextBufferFactory): void { - const options = this.getCreationOptions(model.getLanguageIdentifier().language, model.uri, model.isForSimpleWidget); + const options = this.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget); const { textBuffer, disposable } = createTextBuffer(value, options.defaultEOL); // Return early if the text is already set in that form @@ -497,10 +498,10 @@ export class ModelServiceImpl extends Disposable implements IModelService { let modelData: ModelData; if (languageSelection) { - modelData = this._createModelData(value, languageSelection.languageIdentifier, resource, isForSimpleWidget); + modelData = this._createModelData(value, languageSelection.languageId, resource, isForSimpleWidget); this.setMode(modelData.model, languageSelection); } else { - modelData = this._createModelData(value, PLAINTEXT_LANGUAGE_IDENTIFIER, resource, isForSimpleWidget); + modelData = this._createModelData(value, PLAINTEXT_MODE_ID, resource, isForSimpleWidget); } this._onModelAdded.fire(modelData.model); @@ -555,6 +556,16 @@ export class ModelServiceImpl extends Disposable implements IModelService { // --- end IModelService + protected _schemaShouldMaintainUndoRedoElements(resource: URI) { + return ( + resource.scheme === Schemas.file + || resource.scheme === Schemas.vscodeRemote + || resource.scheme === Schemas.userData + || resource.scheme === Schemas.vscodeNotebookCell + || resource.scheme === 'fake-fs' // for tests + ); + } + private _onWillDispose(model: ITextModel): void { const modelId = MODEL_ID(model.uri); const modelData = this._models[modelId]; @@ -562,7 +573,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString()); let maintainUndoRedoStack = false; let heapSize = 0; - if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && schemaShouldMaintainUndoRedoElements(model.uri))) { + if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && this._schemaShouldMaintainUndoRedoElements(model.uri))) { const elements = this._undoRedoService.getElements(model.uri); if (elements.past.length > 0 || elements.future.length > 0) { for (const element of elements.past) { @@ -607,14 +618,14 @@ export class ModelServiceImpl extends Disposable implements IModelService { modelData.dispose(); // clean up cache - delete this._modelCreationOptionsByLanguageAndResource[model.getLanguageIdentifier().language + model.uri]; + delete this._modelCreationOptionsByLanguageAndResource[model.getLanguageId() + model.uri]; this._onModelRemoved.fire(model); } private _onDidChangeLanguage(model: ITextModel, e: IModelLanguageChangedEvent): void { const oldModeId = e.oldLanguage; - const newModeId = model.getLanguageIdentifier().language; + const newModeId = model.getLanguageId(); const oldOptions = this.getCreationOptions(oldModeId, model.uri, model.isForSimpleWidget); const newOptions = this.getCreationOptions(newModeId, model.uri, model.isForSimpleWidget); ModelServiceImpl._setModelOptionsForModel(model, newOptions, oldOptions); @@ -629,7 +640,7 @@ export interface ILineSequence { export const SEMANTIC_HIGHLIGHTING_SETTING_ID = 'editor.semanticHighlighting'; export function isSemanticColoringEnabled(model: ITextModel, themeService: IThemeService, configurationService: IConfigurationService): boolean { - const setting = configurationService.getValue(SEMANTIC_HIGHLIGHTING_SETTING_ID, { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri })?.enabled; + const setting = configurationService.getValue(SEMANTIC_HIGHLIGHTING_SETTING_ID, { overrideIdentifier: model.getLanguageId(), resource: model.uri })?.enabled; if (typeof setting === 'boolean') { return setting; } @@ -693,6 +704,7 @@ class SemanticStyling extends Disposable { constructor( private readonly _themeService: IThemeService, + private readonly _modeService: IModeService, private readonly _logService: ILogService ) { super(); @@ -704,7 +716,7 @@ class SemanticStyling extends Disposable { public get(provider: DocumentTokensProvider): SemanticTokensProviderStyling { if (!this._caches.has(provider)) { - this._caches.set(provider, new SemanticTokensProviderStyling(provider.getLegend(), this._themeService, this._logService)); + this._caches.set(provider, new SemanticTokensProviderStyling(provider.getLegend(), this._themeService, this._modeService, this._logService)); } return this._caches.get(provider)!; } @@ -712,13 +724,13 @@ class SemanticStyling extends Disposable { class SemanticTokensResponse { constructor( - private readonly _provider: DocumentSemanticTokensProvider, + public readonly provider: DocumentSemanticTokensProvider, public readonly resultId: string | undefined, public readonly data: Uint32Array ) { } public dispose(): void { - this._provider.releaseDocumentSemanticTokens(this.resultId); + this.provider.releaseDocumentSemanticTokens(this.resultId); } } @@ -808,10 +820,7 @@ export class ModelSemanticColoring extends Disposable { return; } - const cancellationTokenSource = new CancellationTokenSource(); - const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; - const r = getDocumentSemanticTokens(this._model, lastResultId, cancellationTokenSource.token); - if (!r) { + if (!hasDocumentSemanticTokensProvider(this._model)) { // there is no provider if (this._currentDocumentResponse) { // there are semantic tokens set @@ -820,7 +829,10 @@ export class ModelSemanticColoring extends Disposable { return; } - const { provider, request } = r; + const cancellationTokenSource = new CancellationTokenSource(); + const lastProvider = this._currentDocumentResponse ? this._currentDocumentResponse.provider : null; + const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; + const request = getDocumentSemanticTokens(this._model, lastProvider, lastResultId, cancellationTokenSource.token); this._currentDocumentRequestCancellationTokenSource = cancellationTokenSource; const pendingChanges: IModelContentChangedEvent[] = []; @@ -828,12 +840,17 @@ export class ModelSemanticColoring extends Disposable { pendingChanges.push(e); }); - const styling = this._semanticStyling.get(provider); - request.then((res) => { this._currentDocumentRequestCancellationTokenSource = null; contentChangeListener.dispose(); - this._setDocumentSemanticTokens(provider, res || null, styling, pendingChanges); + + if (!res) { + this._setDocumentSemanticTokens(null, null, null, pendingChanges); + } else { + const { provider, tokens } = res; + const styling = this._semanticStyling.get(provider); + this._setDocumentSemanticTokens(provider, tokens || null, styling, pendingChanges); + } }, (err) => { const isExpectedError = err && (errors.isPromiseCanceledError(err) || (typeof err.message === 'string' && err.message.indexOf('busy') !== -1)); if (!isExpectedError) { @@ -944,7 +961,7 @@ export class ModelSemanticColoring extends Disposable { this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); - const result = toMultilineTokens2(tokens, styling, this._model.getLanguageIdentifier()); + const result = toMultilineTokens2(tokens, styling, this._model.getLanguageId()); // Adjust incoming semantic tokens if (pendingChanges.length > 0) { diff --git a/src/vs/editor/common/services/semanticTokensProviderStyling.ts b/src/vs/editor/common/services/semanticTokensProviderStyling.ts index d9977a0993..cbb4618e59 100644 --- a/src/vs/editor/common/services/semanticTokensProviderStyling.ts +++ b/src/vs/editor/common/services/semanticTokensProviderStyling.ts @@ -3,10 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SemanticTokensLegend, TokenMetadata, FontStyle, MetadataConsts, SemanticTokens, LanguageIdentifier } from 'vs/editor/common/modes'; +import { SemanticTokensLegend, TokenMetadata, FontStyle, MetadataConsts, SemanticTokens } from 'vs/editor/common/modes'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { MultilineTokens2, SparseEncodedTokens } from 'vs/editor/common/model/tokensStore'; +import { IModeService } from 'vs/editor/common/services/modeService'; export const enum SemanticTokensProviderStylingConstants { NO_STYLING = 0b01111111111111111111111111111111 @@ -19,15 +20,17 @@ export class SemanticTokensProviderStyling { constructor( private readonly _legend: SemanticTokensLegend, - private readonly _themeService: IThemeService, - private readonly _logService: ILogService + @IThemeService private readonly _themeService: IThemeService, + @IModeService private readonly _modeService: IModeService, + @ILogService private readonly _logService: ILogService ) { this._hashTable = new HashTable(); this._hasWarnedOverlappingTokens = false; } - public getMetadata(tokenTypeIndex: number, tokenModifierSet: number, languageId: LanguageIdentifier): number { - const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet, languageId.id); + public getMetadata(tokenTypeIndex: number, tokenModifierSet: number, languageId: string): number { + const encodedLanguageId = this._modeService.languageIdCodec.encodeLanguageId(languageId); + const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet, encodedLanguageId); let metadata: number; if (entry) { metadata = entry.metadata; @@ -50,7 +53,7 @@ export class SemanticTokensProviderStyling { tokenModifiers.push('not-in-legend'); } - const tokenStyle = this._themeService.getColorTheme().getTokenStyleMetadata(tokenType, tokenModifiers, languageId.language); + const tokenStyle = this._themeService.getColorTheme().getTokenStyleMetadata(tokenType, tokenModifiers, languageId); if (typeof tokenStyle === 'undefined') { metadata = SemanticTokensProviderStylingConstants.NO_STYLING; } else { @@ -83,7 +86,7 @@ export class SemanticTokensProviderStyling { metadata = SemanticTokensProviderStylingConstants.NO_STYLING; tokenType = 'not-in-legend'; } - this._hashTable.add(tokenTypeIndex, tokenModifierSet, languageId.id, metadata); + this._hashTable.add(tokenTypeIndex, tokenModifierSet, encodedLanguageId, metadata); if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace(`SemanticTokensProviderStyling ${tokenTypeIndex} (${tokenType}) / ${tokenModifierSet} (${tokenModifiers.join(' ')}): foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); @@ -116,7 +119,7 @@ const enum SemanticColoringConstants { DesiredMaxAreas = 1024, } -export function toMultilineTokens2(tokens: SemanticTokens, styling: SemanticTokensProviderStyling, languageId: LanguageIdentifier): MultilineTokens2[] { +export function toMultilineTokens2(tokens: SemanticTokens, styling: SemanticTokensProviderStyling, languageId: string): MultilineTokens2[] { const srcData = tokens.data; const tokenCount = (tokens.data.length / 5) | 0; const tokensPerArea = Math.max(Math.ceil(tokenCount / SemanticColoringConstants.DesiredMaxAreas), SemanticColoringConstants.DesiredTokensPerArea); @@ -159,8 +162,10 @@ export function toMultilineTokens2(tokens: SemanticTokens, styling: SemanticToke const srcOffset = 5 * tokenIndex; const deltaLine = srcData[srcOffset]; const deltaCharacter = srcData[srcOffset + 1]; - const lineNumber = lastLineNumber + deltaLine; - const startCharacter = (deltaLine === 0 ? lastStartCharacter + deltaCharacter : deltaCharacter); + // Casting both `lineNumber` and `startCharacter` here to uint32 using `|0` + // to do checks below with the actual value that will be inserted in the Uint32Array result + const lineNumber = (lastLineNumber + deltaLine) | 0; + const startCharacter = (deltaLine === 0 ? (lastStartCharacter + deltaCharacter) | 0 : deltaCharacter); const length = srcData[srcOffset + 2]; const tokenTypeIndex = srcData[srcOffset + 3]; const tokenModifierSet = srcData[srcOffset + 4]; diff --git a/src/vs/editor/common/services/textResourceConfigurationServiceImpl.ts b/src/vs/editor/common/services/textResourceConfigurationServiceImpl.ts index 46e05e91d6..3427bba62e 100644 --- a/src/vs/editor/common/services/textResourceConfigurationServiceImpl.ts +++ b/src/vs/editor/common/services/textResourceConfigurationServiceImpl.ts @@ -109,7 +109,7 @@ export class TextResourceConfigurationService extends Disposable implements ITex private getLanguage(resource: URI, position: IPosition | null): string | null { const model = this.modelService.getModel(resource); if (model) { - return position ? this.modeService.getLanguageIdentifier(model.getLanguageIdAtPosition(position.lineNumber, position.column))!.language : model.getLanguageIdentifier().language; + return position ? model.getLanguageIdAtPosition(position.lineNumber, position.column) : model.getLanguageId(); } return this.modeService.getModeIdByFilepathOrFirstLine(resource); } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index a7d1347aa8..2a49462586 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -180,45 +180,45 @@ export enum EditorOption { automaticLayout = 10, autoSurround = 11, bracketPairColorization = 12, - codeLens = 13, - codeLensFontFamily = 14, - codeLensFontSize = 15, - colorDecorators = 16, - columnSelection = 17, - comments = 18, - contextmenu = 19, - copyWithSyntaxHighlighting = 20, - cursorBlinking = 21, - cursorSmoothCaretAnimation = 22, - cursorStyle = 23, - cursorSurroundingLines = 24, - cursorSurroundingLinesStyle = 25, - cursorWidth = 26, - disableLayerHinting = 27, - disableMonospaceOptimizations = 28, - domReadOnly = 29, - dragAndDrop = 30, - emptySelectionClipboard = 31, - extraEditorClassName = 32, - fastScrollSensitivity = 33, - find = 34, - fixedOverflowWidgets = 35, - folding = 36, - foldingStrategy = 37, - foldingHighlight = 38, - foldingImportsByDefault = 39, - unfoldOnClickAfterEndOfLine = 40, - fontFamily = 41, - fontInfo = 42, - fontLigatures = 43, - fontSize = 44, - fontWeight = 45, - formatOnPaste = 46, - formatOnType = 47, - glyphMargin = 48, - gotoLocation = 49, - hideCursorInOverviewRuler = 50, - highlightActiveIndentGuide = 51, + guides = 13, + codeLens = 14, + codeLensFontFamily = 15, + codeLensFontSize = 16, + colorDecorators = 17, + columnSelection = 18, + comments = 19, + contextmenu = 20, + copyWithSyntaxHighlighting = 21, + cursorBlinking = 22, + cursorSmoothCaretAnimation = 23, + cursorStyle = 24, + cursorSurroundingLines = 25, + cursorSurroundingLinesStyle = 26, + cursorWidth = 27, + disableLayerHinting = 28, + disableMonospaceOptimizations = 29, + domReadOnly = 30, + dragAndDrop = 31, + emptySelectionClipboard = 32, + extraEditorClassName = 33, + fastScrollSensitivity = 34, + find = 35, + fixedOverflowWidgets = 36, + folding = 37, + foldingStrategy = 38, + foldingHighlight = 39, + foldingImportsByDefault = 40, + unfoldOnClickAfterEndOfLine = 41, + fontFamily = 42, + fontInfo = 43, + fontLigatures = 44, + fontSize = 45, + fontWeight = 46, + formatOnPaste = 47, + formatOnType = 48, + glyphMargin = 49, + gotoLocation = 50, + hideCursorInOverviewRuler = 51, hover = 52, inDiffEditor = 53, inlineSuggest = 54, @@ -250,55 +250,54 @@ export enum EditorOption { readOnly = 80, renameOnType = 81, renderControlCharacters = 82, - renderIndentGuides = 83, - renderFinalNewline = 84, - renderLineHighlight = 85, - renderLineHighlightOnlyWhenFocus = 86, - renderValidationDecorations = 87, - renderWhitespace = 88, - revealHorizontalRightPadding = 89, - roundedSelection = 90, - rulers = 91, - scrollbar = 92, - scrollBeyondLastColumn = 93, - scrollBeyondLastLine = 94, - scrollPredominantAxis = 95, - selectionClipboard = 96, - selectionHighlight = 97, - selectOnLineNumbers = 98, - showFoldingControls = 99, - showUnused = 100, - snippetSuggestions = 101, - smartSelect = 102, - smoothScrolling = 103, - stickyTabStops = 104, - stopRenderingLineAfter = 105, - suggest = 106, - suggestFontSize = 107, - suggestLineHeight = 108, - suggestOnTriggerCharacters = 109, - suggestSelection = 110, - tabCompletion = 111, - tabIndex = 112, - unusualLineTerminators = 113, - useShadowDOM = 114, - useTabStops = 115, - wordSeparators = 116, - wordWrap = 117, - wordWrapBreakAfterCharacters = 118, - wordWrapBreakBeforeCharacters = 119, - wordWrapColumn = 120, - wordWrapOverride1 = 121, - wordWrapOverride2 = 122, - wrappingIndent = 123, - wrappingStrategy = 124, - showDeprecated = 125, - inlayHints = 126, - editorClassName = 127, - pixelRatio = 128, - tabFocusMode = 129, - layoutInfo = 130, - wrappingInfo = 131 + renderFinalNewline = 83, + renderLineHighlight = 84, + renderLineHighlightOnlyWhenFocus = 85, + renderValidationDecorations = 86, + renderWhitespace = 87, + revealHorizontalRightPadding = 88, + roundedSelection = 89, + rulers = 90, + scrollbar = 91, + scrollBeyondLastColumn = 92, + scrollBeyondLastLine = 93, + scrollPredominantAxis = 94, + selectionClipboard = 95, + selectionHighlight = 96, + selectOnLineNumbers = 97, + showFoldingControls = 98, + showUnused = 99, + snippetSuggestions = 100, + smartSelect = 101, + smoothScrolling = 102, + stickyTabStops = 103, + stopRenderingLineAfter = 104, + suggest = 105, + suggestFontSize = 106, + suggestLineHeight = 107, + suggestOnTriggerCharacters = 108, + suggestSelection = 109, + tabCompletion = 110, + tabIndex = 111, + unusualLineTerminators = 112, + useShadowDOM = 113, + useTabStops = 114, + wordSeparators = 115, + wordWrap = 116, + wordWrapBreakAfterCharacters = 117, + wordWrapBreakBeforeCharacters = 118, + wordWrapColumn = 119, + wordWrapOverride1 = 120, + wordWrapOverride2 = 121, + wrappingIndent = 122, + wrappingStrategy = 123, + showDeprecated = 124, + inlayHints = 125, + editorClassName = 126, + pixelRatio = 127, + tabFocusMode = 128, + layoutInfo = 129, + wrappingInfo = 130 } /** @@ -378,7 +377,6 @@ export enum InlineCompletionTriggerKind { */ 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 @@ -410,42 +408,42 @@ export enum KeyCode { DownArrow = 18, Insert = 19, Delete = 20, - KEY_0 = 21, - KEY_1 = 22, - KEY_2 = 23, - KEY_3 = 24, - KEY_4 = 25, - KEY_5 = 26, - KEY_6 = 27, - KEY_7 = 28, - KEY_8 = 29, - KEY_9 = 30, - KEY_A = 31, - KEY_B = 32, - KEY_C = 33, - KEY_D = 34, - KEY_E = 35, - KEY_F = 36, - KEY_G = 37, - KEY_H = 38, - KEY_I = 39, - KEY_J = 40, - KEY_K = 41, - KEY_L = 42, - KEY_M = 43, - KEY_N = 44, - KEY_O = 45, - KEY_P = 46, - KEY_Q = 47, - KEY_R = 48, - KEY_S = 49, - KEY_T = 50, - KEY_U = 51, - KEY_V = 52, - KEY_W = 53, - KEY_X = 54, - KEY_Y = 55, - KEY_Z = 56, + Digit0 = 21, + Digit1 = 22, + Digit2 = 23, + Digit3 = 24, + Digit4 = 25, + Digit5 = 26, + Digit6 = 27, + Digit7 = 28, + Digit8 = 29, + Digit9 = 30, + KeyA = 31, + KeyB = 32, + KeyC = 33, + KeyD = 34, + KeyE = 35, + KeyF = 36, + KeyG = 37, + KeyH = 38, + KeyI = 39, + KeyJ = 40, + KeyK = 41, + KeyL = 42, + KeyM = 43, + KeyN = 44, + KeyO = 45, + KeyP = 46, + KeyQ = 47, + KeyR = 48, + KeyS = 49, + KeyT = 50, + KeyU = 51, + KeyV = 52, + KeyW = 53, + KeyX = 54, + KeyY = 55, + KeyZ = 56, Meta = 57, ContextMenu = 58, F1 = 59, @@ -473,57 +471,57 @@ export enum KeyCode { * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the ';:' key */ - US_SEMICOLON = 80, + Semicolon = 80, /** * For any country/region, the '+' key * For the US standard keyboard, the '=+' key */ - US_EQUAL = 81, + Equal = 81, /** * For any country/region, the ',' key * For the US standard keyboard, the ',<' key */ - US_COMMA = 82, + Comma = 82, /** * For any country/region, the '-' key * For the US standard keyboard, the '-_' key */ - US_MINUS = 83, + Minus = 83, /** * For any country/region, the '.' key * For the US standard keyboard, the '.>' key */ - US_DOT = 84, + Period = 84, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the '/?' key */ - US_SLASH = 85, + Slash = 85, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the '`~' key */ - US_BACKTICK = 86, + Backquote = 86, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the '[{' key */ - US_OPEN_SQUARE_BRACKET = 87, + BracketLeft = 87, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the '\|' key */ - US_BACKSLASH = 88, + Backslash = 88, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the ']}' key */ - US_CLOSE_SQUARE_BRACKET = 89, + BracketRight = 89, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the ''"' key */ - US_QUOTE = 90, + Quote = 90, /** * Used for miscellaneous characters; it can vary by keyboard. */ @@ -531,34 +529,48 @@ export enum KeyCode { /** * Either the angle bracket key or the backslash key on the RT 102-key keyboard. */ - OEM_102 = 92, - NUMPAD_0 = 93, - NUMPAD_1 = 94, - NUMPAD_2 = 95, - NUMPAD_3 = 96, - NUMPAD_4 = 97, - NUMPAD_5 = 98, - NUMPAD_6 = 99, - NUMPAD_7 = 100, - NUMPAD_8 = 101, - NUMPAD_9 = 102, - NUMPAD_MULTIPLY = 103, - NUMPAD_ADD = 104, + IntlBackslash = 92, + Numpad0 = 93, + Numpad1 = 94, + Numpad2 = 95, + Numpad3 = 96, + Numpad4 = 97, + Numpad5 = 98, + Numpad6 = 99, + Numpad7 = 100, + Numpad8 = 101, + Numpad9 = 102, + NumpadMultiply = 103, + NumpadAdd = 104, NUMPAD_SEPARATOR = 105, - NUMPAD_SUBTRACT = 106, - NUMPAD_DECIMAL = 107, - NUMPAD_DIVIDE = 108, + NumpadSubtract = 106, + NumpadDecimal = 107, + NumpadDivide = 108, /** * Cover all key codes when IME is processing input. */ KEY_IN_COMPOSITION = 109, ABNT_C1 = 110, ABNT_C2 = 111, + AudioVolumeMute = 112, + AudioVolumeUp = 113, + AudioVolumeDown = 114, + BrowserSearch = 115, + BrowserHome = 116, + BrowserBack = 117, + BrowserForward = 118, + MediaTrackNext = 119, + MediaTrackPrevious = 120, + MediaStop = 121, + MediaPlayPause = 122, + LaunchMediaPlayer = 123, + LaunchMail = 124, + LaunchApp2 = 125, /** * Placed last to cover the length of the enum. * Please do not depend on this value! */ - MAX_VALUE = 112 + MAX_VALUE = 126 } export enum MarkerSeverity { @@ -843,4 +855,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 dddecf179f..ab523fa645 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -45,6 +45,7 @@ export const editorUnnecessaryCodeOpacity = registerColor('editorUnnecessaryCode 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.')); +export const ghostTextBackground = registerColor('editorGhostText.background', { dark: null, light: null, hc: null }, nls.localize('editorGhostTextBackground', 'Background 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); @@ -52,15 +53,30 @@ export const overviewRulerError = registerColor('editorOverviewRuler.errorForegr export const overviewRulerWarning = registerColor('editorOverviewRuler.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Overview ruler marker color for warnings.')); export const overviewRulerInfo = registerColor('editorOverviewRuler.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoBorder }, nls.localize('overviewRuleInfo', 'Overview ruler marker color for infos.')); -export const editorBracketHighlightingForeground1 = registerColor('editorBracketHighlight.foreground1', { dark: '#FFD700', light: '#0431FAFF', hc: '#FFD700' }, nls.localize('editorBracketHighlightForeground1', 'Foreground color of brackets (1).')); -export const editorBracketHighlightingForeground2 = registerColor('editorBracketHighlight.foreground2', { dark: '#DA70D6', light: '#319331FF', hc: '#DA70D6' }, nls.localize('editorBracketHighlightForeground2', 'Foreground color of brackets (2).')); -export const editorBracketHighlightingForeground3 = registerColor('editorBracketHighlight.foreground3', { dark: '#179FFF', light: '#7B3814FF', hc: '#87CEFA' }, nls.localize('editorBracketHighlightForeground3', 'Foreground color of brackets (3).')); -export const editorBracketHighlightingForeground4 = registerColor('editorBracketHighlight.foreground4', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground4', 'Foreground color of brackets (4).')); -export const editorBracketHighlightingForeground5 = registerColor('editorBracketHighlight.foreground5', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground5', 'Foreground color of brackets (5).')); -export const editorBracketHighlightingForeground6 = registerColor('editorBracketHighlight.foreground6', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground6', 'Foreground color of brackets (6).')); +export const editorBracketHighlightingForeground1 = registerColor('editorBracketHighlight.foreground1', { dark: '#FFD700', light: '#0431FAFF', hc: '#FFD700' }, nls.localize('editorBracketHighlightForeground1', 'Foreground color of brackets (1). Requires enabling bracket pair colorization.')); +export const editorBracketHighlightingForeground2 = registerColor('editorBracketHighlight.foreground2', { dark: '#DA70D6', light: '#319331FF', hc: '#DA70D6' }, nls.localize('editorBracketHighlightForeground2', 'Foreground color of brackets (2). Requires enabling bracket pair colorization.')); +export const editorBracketHighlightingForeground3 = registerColor('editorBracketHighlight.foreground3', { dark: '#179FFF', light: '#7B3814FF', hc: '#87CEFA' }, nls.localize('editorBracketHighlightForeground3', 'Foreground color of brackets (3). Requires enabling bracket pair colorization.')); +export const editorBracketHighlightingForeground4 = registerColor('editorBracketHighlight.foreground4', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground4', 'Foreground color of brackets (4). Requires enabling bracket pair colorization.')); +export const editorBracketHighlightingForeground5 = registerColor('editorBracketHighlight.foreground5', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground5', 'Foreground color of brackets (5). Requires enabling bracket pair colorization.')); +export const editorBracketHighlightingForeground6 = registerColor('editorBracketHighlight.foreground6', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground6', 'Foreground color of brackets (6). Requires enabling bracket pair colorization.')); export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hc: new Color(new RGBA(255, 50, 50, 1)) }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.')); +export const editorBracketPairGuideBackground1 = registerColor('editorBracketPairGuide.background1', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background1', 'Background color of inactive bracket pair guides (1). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground2 = registerColor('editorBracketPairGuide.background2', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background2', 'Background color of inactive bracket pair guides (2). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground3 = registerColor('editorBracketPairGuide.background3', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background3', 'Background color of inactive bracket pair guides (3). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground4 = registerColor('editorBracketPairGuide.background4', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background4', 'Background color of inactive bracket pair guides (4). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground5 = registerColor('editorBracketPairGuide.background5', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background5', 'Background color of inactive bracket pair guides (5). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideBackground6 = registerColor('editorBracketPairGuide.background6', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.background6', 'Background color of inactive bracket pair guides (6). Requires enabling bracket pair guides.')); + +export const editorBracketPairGuideActiveBackground1 = registerColor('editorBracketPairGuide.activeBackground1', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground1', 'Background color of active bracket pair guides (1). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground2 = registerColor('editorBracketPairGuide.activeBackground2', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground2', 'Background color of active bracket pair guides (2). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground3 = registerColor('editorBracketPairGuide.activeBackground3', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground3', 'Background color of active bracket pair guides (3). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground4 = registerColor('editorBracketPairGuide.activeBackground4', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground4', 'Background color of active bracket pair guides (4). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground5 = registerColor('editorBracketPairGuide.activeBackground5', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground5', 'Background color of active bracket pair guides (5). Requires enabling bracket pair guides.')); +export const editorBracketPairGuideActiveBackground6 = registerColor('editorBracketPairGuide.activeBackground6', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketPairGuide.activeBackground6', 'Background color of active bracket pair guides (6). Requires enabling bracket pair guides.')); + + // contains all color rules that used to defined in editor/browser/widget/editor.css registerThemingParticipant((theme, collector) => { const background = theme.getColor(editorBackground); diff --git a/src/vs/editor/common/view/renderingContext.ts b/src/vs/editor/common/view/renderingContext.ts index 7cddf8e172..705fa37c86 100644 --- a/src/vs/editor/common/view/renderingContext.ts +++ b/src/vs/editor/common/view/renderingContext.ts @@ -91,9 +91,20 @@ export class LineVisibleRanges { } export class HorizontalRange { + _horizontalRangeBrand: void = undefined; + public left: number; public width: number; + public static from(ranges: FloatHorizontalRange[]): HorizontalRange[] { + const result = new Array(ranges.length); + for (let i = 0, len = ranges.length; i < len; i++) { + const range = ranges[i]; + result[i] = new HorizontalRange(range.left, range.width); + } + return result; + } + constructor(left: number, width: number) { this.left = Math.round(left); this.width = Math.round(width); @@ -104,20 +115,45 @@ export class HorizontalRange { } } +export class FloatHorizontalRange { + _floatHorizontalRangeBrand: void = undefined; + + public left: number; + public width: number; + + constructor(left: number, width: number) { + this.left = left; + this.width = width; + } + + public toString(): string { + return `[${this.left},${this.width}]`; + } + + public static compare(a: FloatHorizontalRange, b: FloatHorizontalRange): number { + return a.left - b.left; + } +} + export class HorizontalPosition { public outsideRenderedLine: boolean; + /** + * Math.round(this.originalLeft) + */ public left: number; + public originalLeft: number; constructor(outsideRenderedLine: boolean, left: number) { this.outsideRenderedLine = outsideRenderedLine; - this.left = Math.round(left); + this.originalLeft = left; + this.left = Math.round(this.originalLeft); } } export class VisibleRanges { constructor( public readonly outsideRenderedLine: boolean, - public readonly ranges: HorizontalRange[] + public readonly ranges: FloatHorizontalRange[] ) { } } diff --git a/src/vs/editor/common/viewModel/prefixSumComputer.ts b/src/vs/editor/common/viewModel/prefixSumComputer.ts index 197caa45a1..12662e325b 100644 --- a/src/vs/editor/common/viewModel/prefixSumComputer.ts +++ b/src/vs/editor/common/viewModel/prefixSumComputer.ts @@ -3,20 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { arrayInsert } from 'vs/base/common/arrays'; import { toUint32 } from 'vs/base/common/uint'; -export class PrefixSumIndexOfResult { - _prefixSumIndexOfResultBrand: void = undefined; - - index: number; - remainder: number; - - constructor(index: number, remainder: number) { - this.index = index; - this.remainder = remainder; - } -} - export class PrefixSumComputer { /** @@ -71,7 +60,7 @@ export class PrefixSumComputer { return true; } - public changeValue(index: number, value: number): boolean { + public setValue(index: number, value: number): boolean { index = toUint32(index); value = toUint32(value); @@ -187,3 +176,119 @@ export class PrefixSumComputer { return new PrefixSumIndexOfResult(mid, sum - midStart); } } + +/** + * {@link getIndexOf} has an amortized runtime complexity of O(1). + * + * ({@link PrefixSumComputer.getIndexOf} is just O(log n)) +*/ +export class ConstantTimePrefixSumComputer { + private _values: number[]; + private _isValid: boolean; + private _validEndIndex: number; + + /** + * _prefixSum[i] = SUM(values[j]), 0 <= j <= i + */ + private _prefixSum: number[]; + + /** + * _indexBySum[sum] = idx => _prefixSum[idx - 1] <= sum < _prefixSum[idx] + */ + private _indexBySum: number[]; + + constructor(values: number[]) { + this._values = values; + this._isValid = false; + this._validEndIndex = -1; + this._prefixSum = []; + this._indexBySum = []; + } + + /** + * @returns SUM(0 <= j < values.length, values[j]) + */ + public getTotalSum(): number { + this._ensureValid(); + return this._indexBySum.length; + } + + /** + * @returns `SUM(0 <= j <= index, values[j])`. Includes `values[index]`! + */ + public getPrefixSum(index: number): number { + this._ensureValid(); + return this._prefixSum[index]; + } + + /** + * @returns `result`, such that `getPrefixSum(result.index - 1) + result.remainder = sum` + */ + public getIndexOf(sum: number): PrefixSumIndexOfResult { + this._ensureValid(); + const idx = this._indexBySum[sum]; + const viewLinesAbove = idx > 0 ? this._prefixSum[idx - 1] : 0; + return new PrefixSumIndexOfResult(idx, sum - viewLinesAbove); + } + + public removeValues(start: number, deleteCount: number): void { + this._values.splice(start, deleteCount); + this._invalidate(start); + } + + public insertValues(insertIndex: number, insertArr: number[]): void { + this._values = arrayInsert(this._values, insertIndex, insertArr); + this._invalidate(insertIndex); + } + + private _invalidate(index: number): void { + this._isValid = false; + this._validEndIndex = Math.min(this._validEndIndex, index - 1); + } + + private _ensureValid(): void { + if (this._isValid) { + return; + } + + for (let i = this._validEndIndex + 1, len = this._values.length; i < len; i++) { + const value = this._values[i]; + const sumAbove = i > 0 ? this._prefixSum[i - 1] : 0; + + this._prefixSum[i] = sumAbove + value; + for (let j = 0; j < value; j++) { + this._indexBySum[sumAbove + j] = i; + } + } + + // trim things + this._prefixSum.length = this._values.length; + this._indexBySum.length = this._prefixSum[this._prefixSum.length - 1]; + + // mark as valid + this._isValid = true; + this._validEndIndex = this._values.length - 1; + } + + public setValue(index: number, value: number): void { + if (this._values[index] === value) { + // no change + return; + } + this._values[index] = value; + this._invalidate(index); + } +} + + +export class PrefixSumIndexOfResult { + _prefixSumIndexOfResultBrand: void = undefined; + + constructor( + public readonly index: number, + public readonly remainder: number + ) { + this.index = index; + this.remainder = remainder; + } +} diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index f246fdb85b..68f1564225 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -6,17 +6,16 @@ import * as arrays from 'vs/base/common/arrays'; import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { IViewLineTokens, LineTokens } from 'vs/editor/common/core/lineTokens'; -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 { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel, PositionAffinity } from 'vs/editor/common/model'; -import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel'; +import { BracketGuideOptions, EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, IndentGuide, IndentGuideHorizontalLine, ITextModel, PositionAffinity } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, IOverviewRulerDecorations, LineBreakData, SingleLineInlineDecoration, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; +import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, LineBreakData, SingleLineInlineDecoration, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { IDisposable } from 'vs/base/common/lifecycle'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; -import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; +import { ConstantTimePrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; export interface ILineBreaksComputerFactory { createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer; @@ -31,9 +30,9 @@ export interface ISimpleModel { getValueInRange(range: IRange, eol?: EndOfLinePreference): string; } -export interface ISplitLine { +export interface IModelLineProjection { isVisible(): boolean; - setVisible(isVisible: boolean): ISplitLine; + setVisible(isVisible: boolean): IModelLineProjection; getLineBreakData(): LineBreakData | null; getViewLineCount(): number; @@ -70,6 +69,7 @@ export interface IViewModelLinesCollection extends IDisposable { getViewLineCount(): number; getActiveIndentGuide(viewLineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo; getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[]; + getViewLinesBracketGuides(startLineNumber: number, endLineNumber: number, activePosition: IPosition | null, options: BracketGuideOptions): IndentGuide[][]; getViewLineContent(viewLineNumber: number): string; getViewLineLength(viewLineNumber: number): number; getViewLineMinColumn(viewLineNumber: number): number; @@ -77,7 +77,6 @@ export interface IViewModelLinesCollection extends IDisposable { getViewLineData(viewLineNumber: number): ViewLineData; getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): Array; - getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: EditorTheme): IOverviewRulerDecorations; getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): IModelDecoration[]; getInjectedTextAt(viewPosition: Position): InjectedText | null; @@ -133,6 +132,10 @@ export class CoordinatesConverter implements ICoordinatesConverter { public getModelLineViewLineCount(modelLineNumber: number): number { return this._lines.getModelLineViewLineCount(modelLineNumber); } + + public getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number { + return this._lines.getViewLineNumberOfModelPosition(modelLineNumber, modelColumn); + } } const enum IndentGuideRepeatOption { @@ -141,89 +144,6 @@ const enum IndentGuideRepeatOption { BlockAll = 2 } -class LineNumberMapper { - - private _counts: number[]; - private _isValid: boolean; - private _validEndIndex: number; - - private _modelToView: number[]; - private _viewToModel: number[]; - - constructor(viewLineCounts: number[]) { - this._counts = viewLineCounts; - this._isValid = false; - this._validEndIndex = -1; - this._modelToView = []; - this._viewToModel = []; - } - - private _invalidate(index: number): void { - this._isValid = false; - this._validEndIndex = Math.min(this._validEndIndex, index - 1); - } - - private _ensureValid(): void { - if (this._isValid) { - return; - } - - for (let i = this._validEndIndex + 1, len = this._counts.length; i < len; i++) { - const viewLineCount = this._counts[i]; - const viewLinesAbove = (i > 0 ? this._modelToView[i - 1] : 0); - - this._modelToView[i] = viewLinesAbove + viewLineCount; - for (let j = 0; j < viewLineCount; j++) { - this._viewToModel[viewLinesAbove + j] = i; - } - } - - // trim things - this._modelToView.length = this._counts.length; - this._viewToModel.length = this._modelToView[this._modelToView.length - 1]; - - // mark as valid - this._isValid = true; - this._validEndIndex = this._counts.length - 1; - } - - public changeValue(index: number, value: number): void { - if (this._counts[index] === value) { - // no change - return; - } - this._counts[index] = value; - this._invalidate(index); - } - - public removeValues(start: number, deleteCount: number): void { - this._counts.splice(start, deleteCount); - this._invalidate(start); - } - - public insertValues(insertIndex: number, insertArr: number[]): void { - this._counts = arrays.arrayInsert(this._counts, insertIndex, insertArr); - this._invalidate(insertIndex); - } - - public getTotalValue(): number { - this._ensureValid(); - return this._viewToModel.length; - } - - public getAccumulatedValue(index: number): number { - this._ensureValid(); - return this._modelToView[index]; - } - - public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult { - this._ensureValid(); - const modelLineIndex = this._viewToModel[accumulatedValue]; - const viewLinesAbove = (modelLineIndex > 0 ? this._modelToView[modelLineIndex - 1] : 0); - return new PrefixSumIndexOfResult(modelLineIndex, accumulatedValue - viewLinesAbove); - } -} - export class SplitLinesCollection implements IViewModelLinesCollection { private readonly _editorId: number; @@ -238,9 +158,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection { private wrappingColumn: number; private wrappingIndent: WrappingIndent; private wrappingStrategy: 'simple' | 'advanced'; - private lines!: ISplitLine[]; - private prefixSumComputer!: LineNumberMapper; + private modelLineProjections!: IModelLineProjection[]; + + /** + * Reflects the sum of the line counts of all . + */ + private projectedModelLineLineCounts!: ConstantTimePrefixSumComputer; private hiddenAreasIds!: string[]; @@ -278,7 +202,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } private _constructLines(resetHiddenAreas: boolean, previousLineBreaks: ((LineBreakData | null)[]) | null): void { - this.lines = []; + this.modelLineProjections = []; if (resetHiddenAreas) { this.hiddenAreasIds = []; @@ -314,14 +238,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } let isInHiddenArea = (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd); - let line = createSplitLine(linesBreaks[i], !isInHiddenArea); + let line = createModelLineProjection(linesBreaks[i], !isInHiddenArea); values[i] = line.getViewLineCount(); - this.lines[i] = line; + this.modelLineProjections[i] = line; } this._validModelVersionId = this.model.getVersionId(); - this.prefixSumComputer = new LineNumberMapper(values); + this.projectedModelLineLineCounts = new ConstantTimePrefixSumComputer(values); } public getHiddenAreas(): Range[] { @@ -389,37 +313,37 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let hiddenAreas = newRanges; let hiddenAreaStart = 1, hiddenAreaEnd = 0; let hiddenAreaIdx = -1; - let nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.lines.length + 2; + let nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.modelLineProjections.length + 2; let hasVisibleLine = false; - for (let i = 0; i < this.lines.length; i++) { + for (let i = 0; i < this.modelLineProjections.length; i++) { let lineNumber = i + 1; if (lineNumber === nextLineNumberToUpdateHiddenArea) { hiddenAreaIdx++; hiddenAreaStart = hiddenAreas[hiddenAreaIdx].startLineNumber; hiddenAreaEnd = hiddenAreas[hiddenAreaIdx].endLineNumber; - nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.lines.length + 2; + nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.modelLineProjections.length + 2; } let lineChanged = false; if (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd) { // Line should be hidden - if (this.lines[i].isVisible()) { - this.lines[i] = this.lines[i].setVisible(false); + if (this.modelLineProjections[i].isVisible()) { + this.modelLineProjections[i] = this.modelLineProjections[i].setVisible(false); lineChanged = true; } } else { hasVisibleLine = true; // Line should be visible - if (!this.lines[i].isVisible()) { - this.lines[i] = this.lines[i].setVisible(true); + if (!this.modelLineProjections[i].isVisible()) { + this.modelLineProjections[i] = this.modelLineProjections[i].setVisible(true); lineChanged = true; } } if (lineChanged) { - let newOutputLineCount = this.lines[i].getViewLineCount(); - this.prefixSumComputer.changeValue(i, newOutputLineCount); + let newOutputLineCount = this.modelLineProjections[i].getViewLineCount(); + this.projectedModelLineLineCounts.setValue(i, newOutputLineCount); } } @@ -432,19 +356,19 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public modelPositionIsVisible(modelLineNumber: number, _modelColumn: number): boolean { - if (modelLineNumber < 1 || modelLineNumber > this.lines.length) { + if (modelLineNumber < 1 || modelLineNumber > this.modelLineProjections.length) { // invalid arguments return false; } - return this.lines[modelLineNumber - 1].isVisible(); + return this.modelLineProjections[modelLineNumber - 1].isVisible(); } public getModelLineViewLineCount(modelLineNumber: number): number { - if (modelLineNumber < 1 || modelLineNumber > this.lines.length) { + if (modelLineNumber < 1 || modelLineNumber > this.modelLineProjections.length) { // invalid arguments return 1; } - return this.lines[modelLineNumber - 1].getViewLineCount(); + return this.modelLineProjections[modelLineNumber - 1].getViewLineCount(); } public setTabSize(newTabSize: number): boolean { @@ -477,8 +401,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let previousLineBreaks: ((LineBreakData | null)[]) | null = null; if (onlyWrappingColumnChanged) { previousLineBreaks = []; - for (let i = 0, len = this.lines.length; i < len; i++) { - previousLineBreaks[i] = this.lines[i].getLineBreakData(); + for (let i = 0, len = this.modelLineProjections.length; i < len; i++) { + previousLineBreaks[i] = this.modelLineProjections[i].getLineBreakData(); } } @@ -507,11 +431,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return null; } - let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); - let outputToLineNumber = this.prefixSumComputer.getAccumulatedValue(toLineNumber - 1); + let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(fromLineNumber - 2) + 1); + let outputToLineNumber = this.projectedModelLineLineCounts.getPrefixSum(toLineNumber - 1); - this.lines.splice(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); - this.prefixSumComputer.removeValues(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); + this.modelLineProjections.splice(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); + this.projectedModelLineLineCounts.removeValues(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); return new viewEvents.ViewLinesDeletedEvent(outputFromLineNumber, outputToLineNumber); } @@ -524,16 +448,16 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } // cannot use this.getHiddenAreas() because those decorations have already seen the effect of this model change - const isInHiddenArea = (fromLineNumber > 2 && !this.lines[fromLineNumber - 2].isVisible()); + const isInHiddenArea = (fromLineNumber > 2 && !this.modelLineProjections[fromLineNumber - 2].isVisible()); - let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); + let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(fromLineNumber - 2) + 1); let totalOutputLineCount = 0; - let insertLines: ISplitLine[] = []; + let insertLines: IModelLineProjection[] = []; let insertPrefixSumValues: number[] = []; for (let i = 0, len = lineBreaks.length; i < len; i++) { - let line = createSplitLine(lineBreaks[i], !isInHiddenArea); + let line = createModelLineProjection(lineBreaks[i], !isInHiddenArea); insertLines.push(line); let outputLineCount = line.getViewLineCount(); @@ -542,9 +466,9 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } // TODO@Alex: use arrays.arrayInsert - this.lines = this.lines.slice(0, fromLineNumber - 1).concat(insertLines).concat(this.lines.slice(fromLineNumber - 1)); + this.modelLineProjections = this.modelLineProjections.slice(0, fromLineNumber - 1).concat(insertLines).concat(this.modelLineProjections.slice(fromLineNumber - 1)); - this.prefixSumComputer.insertValues(fromLineNumber - 1, insertPrefixSumValues); + this.projectedModelLineLineCounts.insertValues(fromLineNumber - 1, insertPrefixSumValues); return new viewEvents.ViewLinesInsertedEvent(outputFromLineNumber, outputFromLineNumber + totalOutputLineCount - 1); } @@ -558,11 +482,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let lineIndex = lineNumber - 1; - let oldOutputLineCount = this.lines[lineIndex].getViewLineCount(); - let isVisible = this.lines[lineIndex].isVisible(); - let line = createSplitLine(lineBreakData, isVisible); - this.lines[lineIndex] = line; - let newOutputLineCount = this.lines[lineIndex].getViewLineCount(); + let oldOutputLineCount = this.modelLineProjections[lineIndex].getViewLineCount(); + let isVisible = this.modelLineProjections[lineIndex].isVisible(); + let line = createModelLineProjection(lineBreakData, isVisible); + this.modelLineProjections[lineIndex] = line; + let newOutputLineCount = this.modelLineProjections[lineIndex].getViewLineCount(); let lineMappingChanged = false; let changeFrom = 0; @@ -573,23 +497,23 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let deleteTo = -1; if (oldOutputLineCount > newOutputLineCount) { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + newOutputLineCount - 1; deleteFrom = changeTo + 1; deleteTo = deleteFrom + (oldOutputLineCount - newOutputLineCount) - 1; lineMappingChanged = true; } else if (oldOutputLineCount < newOutputLineCount) { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + oldOutputLineCount - 1; insertFrom = changeTo + 1; insertTo = insertFrom + (newOutputLineCount - oldOutputLineCount) - 1; lineMappingChanged = true; } else { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.projectedModelLineLineCounts.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + newOutputLineCount - 1; } - this.prefixSumComputer.changeValue(lineIndex, newOutputLineCount); + this.projectedModelLineLineCounts.setValue(lineIndex, newOutputLineCount); const viewLinesChangedEvent = (changeFrom <= changeTo ? new viewEvents.ViewLinesChangedEvent(changeFrom, changeTo) : null); const viewLinesInsertedEvent = (insertFrom <= insertTo ? new viewEvents.ViewLinesInsertedEvent(insertFrom, insertTo) : null); @@ -600,14 +524,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { public acceptVersionId(versionId: number): void { this._validModelVersionId = versionId; - if (this.lines.length === 1 && !this.lines[0].isVisible()) { + if (this.modelLineProjections.length === 1 && !this.modelLineProjections[0].isVisible()) { // At least one line must be visible => reset hidden areas this.setHiddenAreas([]); } } public getViewLineCount(): number { - return this.prefixSumComputer.getTotalValue(); + return this.projectedModelLineLineCounts.getTotalSum(); } private _toValidViewLineNumber(viewLineNumber: number): number { @@ -640,7 +564,143 @@ export class SplitLinesCollection implements IViewModelLinesCollection { }; } + // #region ViewLineInfo + + private getViewLineInfo(viewLineNumber: number): ViewLineInfo { + viewLineNumber = this._toValidViewLineNumber(viewLineNumber); + let r = this.projectedModelLineLineCounts.getIndexOf(viewLineNumber - 1); + let lineIndex = r.index; + let remainder = r.remainder; + return new ViewLineInfo(lineIndex + 1, remainder); + } + + private getMinColumnOfViewLine(viewLineInfo: ViewLineInfo): number { + return this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewLineMinColumn( + this.model, + viewLineInfo.modelLineNumber, + viewLineInfo.modelLineWrappedLineIdx + ); + } + + private getModelStartPositionOfViewLine(viewLineInfo: ViewLineInfo): Position { + const line = this.modelLineProjections[viewLineInfo.modelLineNumber - 1]; + const minViewColumn = line.getViewLineMinColumn( + this.model, + viewLineInfo.modelLineNumber, + viewLineInfo.modelLineWrappedLineIdx + ); + const column = line.getModelColumnOfViewPosition( + viewLineInfo.modelLineWrappedLineIdx, + minViewColumn + ); + return new Position(viewLineInfo.modelLineNumber, column); + } + + private getModelEndPositionOfViewLine(viewLineInfo: ViewLineInfo): Position { + const line = this.modelLineProjections[viewLineInfo.modelLineNumber - 1]; + const maxViewColumn = line.getViewLineMaxColumn( + this.model, + viewLineInfo.modelLineNumber, + viewLineInfo.modelLineWrappedLineIdx + ); + const column = line.getModelColumnOfViewPosition( + viewLineInfo.modelLineWrappedLineIdx, + maxViewColumn + ); + return new Position(viewLineInfo.modelLineNumber, column); + } + + private getViewLineInfosGroupedByModelRanges(viewStartLineNumber: number, viewEndLineNumber: number): ViewLineInfoGroupedByModelRange[] { + const startViewLine = this.getViewLineInfo(viewStartLineNumber); + const endViewLine = this.getViewLineInfo(viewEndLineNumber); + + const result = new Array(); + let lastVisibleModelPos: Position | null = this.getModelStartPositionOfViewLine(startViewLine); + let viewLines = new Array(); + + for (let curModelLine = startViewLine.modelLineNumber; curModelLine <= endViewLine.modelLineNumber; curModelLine++) { + const line = this.modelLineProjections[curModelLine - 1]; + + if (line.isVisible()) { + let startOffset = + curModelLine === startViewLine.modelLineNumber + ? startViewLine.modelLineWrappedLineIdx + : 0; + + let endOffset = + curModelLine === endViewLine.modelLineNumber + ? endViewLine.modelLineWrappedLineIdx + 1 + : line.getViewLineCount(); + + for (let i = startOffset; i < endOffset; i++) { + viewLines.push(new ViewLineInfo(curModelLine, i)); + } + } + + if (!line.isVisible() && lastVisibleModelPos) { + const lastVisibleModelPos2 = new Position(curModelLine - 1, this.model.getLineMaxColumn(curModelLine - 1) + 1); + + const modelRange = Range.fromPositions(lastVisibleModelPos, lastVisibleModelPos2); + result.push(new ViewLineInfoGroupedByModelRange(modelRange, viewLines)); + viewLines = []; + + lastVisibleModelPos = null; + } else if (line.isVisible() && !lastVisibleModelPos) { + lastVisibleModelPos = new Position(curModelLine, 1); + } + } + + if (lastVisibleModelPos) { + const modelRange = Range.fromPositions(lastVisibleModelPos, this.getModelEndPositionOfViewLine(endViewLine)); + result.push(new ViewLineInfoGroupedByModelRange(modelRange, viewLines)); + } + + return result; + } + + // #endregion + + public getViewLinesBracketGuides(viewStartLineNumber: number, viewEndLineNumber: number, activeViewPosition: IPosition | null, options: BracketGuideOptions): IndentGuide[][] { + const modelActivePosition = activeViewPosition ? this.convertViewPositionToModelPosition(activeViewPosition.lineNumber, activeViewPosition.column) : null; + const resultPerViewLine: IndentGuide[][] = []; + + for (const group of this.getViewLineInfosGroupedByModelRanges(viewStartLineNumber, viewEndLineNumber)) { + const modelRangeStartLineNumber = group.modelRange.startLineNumber; + + const bracketGuidesPerModelLine = this.model.getLinesBracketGuides( + modelRangeStartLineNumber, + group.modelRange.endLineNumber, + modelActivePosition, + options + ); + + for (const viewLineInfo of group.viewLines) { + if (viewLineInfo.isWrappedLineContinuation && this.getMinColumnOfViewLine(viewLineInfo) === 1) { + // Don't add indent guides when the wrapped line continuation has no wrapping-indentation. + resultPerViewLine.push([]); + } else { + let bracketGuides = bracketGuidesPerModelLine[viewLineInfo.modelLineNumber - modelRangeStartLineNumber]; + + // visibleColumns stay as they are (this is a bug and needs to be fixed, but it is not a regression) + // model-columns must be converted to view-model columns. + bracketGuides = bracketGuides.map(g => g.horizontalLine ? + new IndentGuide(g.visibleColumn, g.className, + new IndentGuideHorizontalLine(g.horizontalLine.top, + this.convertModelPositionToViewPosition(viewLineInfo.modelLineNumber, g.horizontalLine.endColumn).column + ) + ) : g); + resultPerViewLine.push(bracketGuides); + } + } + } + + return resultPerViewLine; + } + public getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[] { + // TODO: Use the same code as in `getViewLinesBracketGuides`. + // Future TODO: Merge with `getViewLinesBracketGuides`. + // However, this requires more refactoring of indent guides. viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber); viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber); @@ -655,7 +715,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let reqStart: Position | null = null; for (let modelLineIndex = modelStartLineIndex; modelLineIndex <= modelEndLineIndex; modelLineIndex++) { - const line = this.lines[modelLineIndex]; + const line = this.modelLineProjections[modelLineIndex]; if (line.isVisible()) { let viewLineStartIndex = line.getViewLineNumberOfModelPosition(0, modelLineIndex === modelStartLineIndex ? modelStart.column : 1); let viewLineEndIndex = line.getViewLineNumberOfModelPosition(0, this.model.getLineMaxColumn(modelLineIndex + 1)); @@ -711,48 +771,28 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineContent(viewLineNumber: number): string { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineContent(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineContent(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLineLength(viewLineNumber: number): number { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineLength(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineLength(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLineMinColumn(viewLineNumber: number): number { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineMinColumn(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineMinColumn(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLineMaxColumn(viewLineNumber: number): number { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineMaxColumn(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineMaxColumn(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLineData(viewLineNumber: number): ViewLineData { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - return this.lines[lineIndex].getViewLineData(this.model, lineIndex + 1, remainder); + const info = this.getViewLineInfo(viewLineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getViewLineData(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx); } public getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[] { @@ -760,14 +800,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber); viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber); - let start = this.prefixSumComputer.getIndexOf(viewStartLineNumber - 1); + let start = this.projectedModelLineLineCounts.getIndexOf(viewStartLineNumber - 1); let viewLineNumber = viewStartLineNumber; let startModelLineIndex = start.index; let startRemainder = start.remainder; let result: ViewLineData[] = []; for (let modelLineIndex = startModelLineIndex, len = this.model.getLineCount(); modelLineIndex < len; modelLineIndex++) { - let line = this.lines[modelLineIndex]; + let line = this.modelLineProjections[modelLineIndex]; if (!line.isVisible()) { continue; } @@ -796,11 +836,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { public validateViewPosition(viewLineNumber: number, viewColumn: number, expectedModelPosition: Position): Position { viewLineNumber = this._toValidViewLineNumber(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); + let r = this.projectedModelLineLineCounts.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; let remainder = r.remainder; - let line = this.lines[lineIndex]; + let line = this.modelLineProjections[lineIndex]; let minColumn = line.getViewLineMinColumn(this.model, lineIndex + 1, remainder); let maxColumn = line.getViewLineMaxColumn(this.model, lineIndex + 1, remainder); @@ -828,15 +868,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public convertViewPositionToModelPosition(viewLineNumber: number, viewColumn: number): Position { - viewLineNumber = this._toValidViewLineNumber(viewLineNumber); + const info = this.getViewLineInfo(viewLineNumber); - let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - let lineIndex = r.index; - let remainder = r.remainder; - - let inputColumn = this.lines[lineIndex].getModelColumnOfViewPosition(remainder, viewColumn); + let inputColumn = this.modelLineProjections[info.modelLineNumber - 1].getModelColumnOfViewPosition(info.modelLineWrappedLineIdx, viewColumn); // console.log('out -> in ' + viewLineNumber + ',' + viewColumn + ' ===> ' + (lineIndex+1) + ',' + inputColumn); - return this.model.validatePosition(new Position(lineIndex + 1, inputColumn)); + return this.model.validatePosition(new Position(info.modelLineNumber, inputColumn)); } public convertViewRangeToModelRange(viewRange: Range): Range { @@ -852,22 +888,22 @@ export class SplitLinesCollection implements IViewModelLinesCollection { const inputColumn = validPosition.column; let lineIndex = inputLineNumber - 1, lineIndexChanged = false; - while (lineIndex > 0 && !this.lines[lineIndex].isVisible()) { + while (lineIndex > 0 && !this.modelLineProjections[lineIndex].isVisible()) { lineIndex--; lineIndexChanged = true; } - if (lineIndex === 0 && !this.lines[lineIndex].isVisible()) { + if (lineIndex === 0 && !this.modelLineProjections[lineIndex].isVisible()) { // Could not reach a real line // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + 1 + ',' + 1); return new Position(1, 1); } - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.projectedModelLineLineCounts.getPrefixSum(lineIndex - 1)); let r: Position; if (lineIndexChanged) { - r = this.lines[lineIndex].getViewPositionOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1), affinity); + r = this.modelLineProjections[lineIndex].getViewPositionOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1), affinity); } else { - r = this.lines[inputLineNumber - 1].getViewPositionOfModelPosition(deltaLineNumber, inputColumn, affinity); + r = this.modelLineProjections[inputLineNumber - 1].getViewPositionOfModelPosition(deltaLineNumber, inputColumn, affinity); } // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + r.lineNumber + ',' + r); @@ -888,42 +924,24 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } } - private _getViewLineNumberForModelPosition(inputLineNumber: number, inputColumn: number): number { - let lineIndex = inputLineNumber - 1; - if (this.lines[lineIndex].isVisible()) { + public getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number { + let lineIndex = modelLineNumber - 1; + if (this.modelLineProjections[lineIndex].isVisible()) { // this model line is visible - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); - return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, inputColumn); + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.projectedModelLineLineCounts.getPrefixSum(lineIndex - 1)); + return this.modelLineProjections[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, modelColumn); } // this model line is not visible - while (lineIndex > 0 && !this.lines[lineIndex].isVisible()) { + while (lineIndex > 0 && !this.modelLineProjections[lineIndex].isVisible()) { lineIndex--; } - if (lineIndex === 0 && !this.lines[lineIndex].isVisible()) { + if (lineIndex === 0 && !this.modelLineProjections[lineIndex].isVisible()) { // Could not reach a real line return 1; } - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); - return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); - } - - public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: EditorTheme): IOverviewRulerDecorations { - const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); - const result = new OverviewRulerDecorations(); - for (const decoration of decorations) { - const opts = decoration.options.overviewRuler; - const lane = opts ? opts.position : 0; - if (lane === 0) { - continue; - } - const color = opts.getColor(theme); - const viewStartLineNumber = this._getViewLineNumberForModelPosition(decoration.range.startLineNumber, decoration.range.startColumn); - const viewEndLineNumber = this._getViewLineNumberForModelPosition(decoration.range.endLineNumber, decoration.range.endColumn); - - result.accept(color, viewStartLineNumber, viewEndLineNumber, lane); - } - return result.result; + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.projectedModelLineLineCounts.getPrefixSum(lineIndex - 1)); + return this.modelLineProjections[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); } public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): IModelDecoration[] { @@ -942,7 +960,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let reqStart: Position | null = null; for (let modelLineIndex = modelStartLineIndex; modelLineIndex <= modelEndLineIndex; modelLineIndex++) { - const line = this.lines[modelLineIndex]; + const line = this.modelLineProjections[modelLineIndex]; if (line.isVisible()) { // merge into previous request if (reqStart === null) { @@ -994,31 +1012,19 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getInjectedTextAt(position: Position): InjectedText | null { - const viewLineNumber = this._toValidViewLineNumber(position.lineNumber); - const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - const lineIndex = r.index; - const remainder = r.remainder; - - return this.lines[lineIndex].getInjectedTextAt(remainder, position.column); + const info = this.getViewLineInfo(position.lineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].getInjectedTextAt(info.modelLineWrappedLineIdx, position.column); } normalizePosition(position: Position, affinity: PositionAffinity): Position { - const viewLineNumber = this._toValidViewLineNumber(position.lineNumber); - const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - const lineIndex = r.index; - const remainder = r.remainder; - - return this.lines[lineIndex].normalizePosition(this.model, lineIndex + 1, remainder, position, affinity); + const info = this.getViewLineInfo(position.lineNumber); + return this.modelLineProjections[info.modelLineNumber - 1].normalizePosition(this.model, info.modelLineNumber, info.modelLineWrappedLineIdx, position, affinity); } public getLineIndentColumn(lineNumber: number): number { - const viewLineNumber = this._toValidViewLineNumber(lineNumber); - const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); - const lineIndex = r.index; - const remainder = r.remainder; - - if (remainder === 0) { - return this.model.getLineIndentColumn(lineIndex + 1); + const info = this.getViewLineInfo(lineNumber); + if (info.modelLineWrappedLineIdx === 0) { + return this.model.getLineIndentColumn(info.modelLineNumber); } // wrapped lines have no indentation. @@ -1028,9 +1034,34 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } } -class VisibleIdentitySplitLine implements ISplitLine { +/** + * Represents a view line. Can be used to efficiently query more information about it. + */ +class ViewLineInfo { + public get isWrappedLineContinuation(): boolean { + return this.modelLineWrappedLineIdx > 0; + } - public static readonly INSTANCE = new VisibleIdentitySplitLine(); + constructor( + public readonly modelLineNumber: number, + public readonly modelLineWrappedLineIdx: number, + ) { } +} + +/** + * A list of view lines that have a contiguous span in the model. +*/ +class ViewLineInfoGroupedByModelRange { + constructor(public readonly modelRange: Range, public readonly viewLines: ViewLineInfo[]) { + } +} + +/** + * This projection does not change the model line. +*/ +class IdentityModelLineProjection implements IModelLineProjection { + + public static readonly INSTANCE = new IdentityModelLineProjection(); private constructor() { } @@ -1038,11 +1069,11 @@ class VisibleIdentitySplitLine implements ISplitLine { return true; } - public setVisible(isVisible: boolean): ISplitLine { + public setVisible(isVisible: boolean): IModelLineProjection { if (isVisible) { return this; } - return InvisibleIdentitySplitLine.INSTANCE; + return HiddenModelLineProjection.INSTANCE; } public getLineBreakData(): LineBreakData | null { @@ -1112,9 +1143,12 @@ class VisibleIdentitySplitLine implements ISplitLine { } } -class InvisibleIdentitySplitLine implements ISplitLine { +/** + * This projection hides the model line. + */ +class HiddenModelLineProjection implements IModelLineProjection { - public static readonly INSTANCE = new InvisibleIdentitySplitLine(); + public static readonly INSTANCE = new HiddenModelLineProjection(); private constructor() { } @@ -1122,11 +1156,11 @@ class InvisibleIdentitySplitLine implements ISplitLine { return false; } - public setVisible(isVisible: boolean): ISplitLine { + public setVisible(isVisible: boolean): IModelLineProjection { if (!isVisible) { return this; } - return VisibleIdentitySplitLine.INSTANCE; + return IdentityModelLineProjection.INSTANCE; } public getLineBreakData(): LineBreakData | null { @@ -1182,7 +1216,12 @@ class InvisibleIdentitySplitLine implements ISplitLine { } } -export class SplitLine implements ISplitLine { +/** + * This projection is used to + * * wrap model lines + * * inject text + */ +export class ModelLineProjection implements IModelLineProjection { private readonly _lineBreakData: LineBreakData; private _isVisible: boolean; @@ -1196,7 +1235,7 @@ export class SplitLine implements ISplitLine { return this._isVisible; } - public setVisible(isVisible: boolean): ISplitLine { + public setVisible(isVisible: boolean): IModelLineProjection { this._isVisible = isVisible; return this; } @@ -1483,15 +1522,15 @@ function _makeSpaces(count: number): string { return new Array(count + 1).join(' '); } -function createSplitLine(lineBreakData: LineBreakData | null, isVisible: boolean): ISplitLine { +function createModelLineProjection(lineBreakData: LineBreakData | null, isVisible: boolean): IModelLineProjection { if (lineBreakData === null) { // No mapping needed if (isVisible) { - return VisibleIdentitySplitLine.INSTANCE; + return IdentityModelLineProjection.INSTANCE; } - return InvisibleIdentitySplitLine.INSTANCE; + return HiddenModelLineProjection.INSTANCE; } else { - return new SplitLine(lineBreakData, isVisible); + return new ModelLineProjection(lineBreakData, isVisible); } } @@ -1551,6 +1590,10 @@ export class IdentityCoordinatesConverter implements ICoordinatesConverter { public getModelLineViewLineCount(modelLineNumber: number): number { return 1; } + + public getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number { + return modelLineNumber; + } } export class IdentityLinesCollection implements IViewModelLinesCollection { @@ -1626,6 +1669,10 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { }; } + public getViewLinesBracketGuides(startLineNumber: number, endLineNumber: number, activePosition: IPosition | null): IndentGuide[][] { + return new Array(endLineNumber - startLineNumber + 1).fill([]); + } + public getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[] { const viewLineCount = viewEndLineNumber - viewStartLineNumber + 1; let result = new Array(viewLineCount); @@ -1682,24 +1729,6 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return result; } - public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: EditorTheme): IOverviewRulerDecorations { - const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); - const result = new OverviewRulerDecorations(); - for (const decoration of decorations) { - const opts = decoration.options.overviewRuler; - const lane = opts ? opts.position : 0; - if (lane === 0) { - continue; - } - const color = opts.getColor(theme); - const viewStartLineNumber = decoration.range.startLineNumber; - const viewEndLineNumber = decoration.range.endLineNumber; - - result.accept(color, viewStartLineNumber, viewEndLineNumber, lane); - } - return result.result; - } - public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): IModelDecoration[] { return this.model.getDecorationsInRange(range, ownerId, filterOutValidation); } @@ -1717,29 +1746,3 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return null; } } - -class OverviewRulerDecorations { - - readonly result: IOverviewRulerDecorations = Object.create(null); - - public accept(color: string, startLineNumber: number, endLineNumber: number, lane: number): void { - let prev = this.result[color]; - - if (prev) { - const prevLane = prev[prev.length - 3]; - const prevEndLineNumber = prev[prev.length - 1]; - if (prevLane === lane && prevEndLineNumber + 1 >= startLineNumber) { - // merge into prev - if (endLineNumber > prevEndLineNumber) { - prev[prev.length - 1] = endLineNumber; - } - return; - } - - // push - prev.push(lane, startLineNumber, endLineNumber); - } else { - this.result[color] = [lane, startLineNumber, endLineNumber]; - } - } -} diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index d4ed521003..d9c7255738 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -9,7 +9,7 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { INewScrollPosition, ScrollType } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions, ITextModel, InjectedTextOptions, PositionAffinity } from 'vs/editor/common/model'; +import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions, ITextModel, InjectedTextOptions, PositionAffinity, IndentGuide, BracketGuideOptions } from 'vs/editor/common/model'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; @@ -89,6 +89,7 @@ export interface ICoordinatesConverter { convertModelRangeToViewRange(modelRange: Range, affinity?: PositionAffinity): Range; modelPositionIsVisible(modelPosition: Position): boolean; getModelLineViewLineCount(modelLineNumber: number): number; + getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number; } export class OutputPosition { @@ -333,11 +334,12 @@ export interface IViewModel extends ICursorSimpleModel { getLineLength(lineNumber: number): number; getActiveIndentGuide(lineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo; getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[]; + getBracketGuidesInRangeByLine(startLineNumber: number, endLineNumber: number, activePosition: IPosition | null, options: BracketGuideOptions): IndentGuide[][]; getLineMinColumn(lineNumber: number): number; getLineMaxColumn(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; - getAllOverviewRulerDecorations(theme: EditorTheme): IOverviewRulerDecorations; + getAllOverviewRulerDecorations(theme: EditorTheme): OverviewRulerDecorationsGroup[]; invalidateOverviewRulerColorCache(): void; invalidateMinimapColorCache(): void; getValueInRange(range: Range, eol: EndOfLinePreference): string; @@ -586,12 +588,30 @@ export class ViewModelDecoration { } } -/** - * Decorations are encoded in a number array using the following scheme: - * - 3*i = lane - * - 3*i+1 = startLineNumber - * - 3*i+2 = endLineNumber - */ -export interface IOverviewRulerDecorations { - [color: string]: number[]; +export class OverviewRulerDecorationsGroup { + + constructor( + public readonly color: string, + public readonly zIndex: number, + /** + * Decorations are encoded in a number array using the following scheme: + * - 3*i = lane + * - 3*i+1 = startLineNumber + * - 3*i+2 = endLineNumber + */ + public readonly data: number[] + ) { } + + public static cmp(a: OverviewRulerDecorationsGroup, b: OverviewRulerDecorationsGroup): number { + if (a.zIndex === b.zIndex) { + if (a.color < b.color) { + return -1; + } + if (a.color > b.color) { + return 1; + } + return 0; + } + return a.zIndex - b.zIndex; + } } diff --git a/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts b/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts index 018b1cb5a4..d3b3590b1d 100644 --- a/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts +++ b/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts @@ -176,6 +176,7 @@ export const enum OutgoingViewModelEventKind { FocusChanged, ScrollChanged, ViewZonesChanged, + HiddenAreasChanged, ReadOnlyEditAttempt, CursorStateChanged, } @@ -308,6 +309,22 @@ export class ViewZonesChangedEvent { } } +export class HiddenAreasChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.HiddenAreasChanged; + + constructor() { + } + + public isNoOp(): boolean { + return false; + } + + public merge(other: OutgoingViewModelEvent): HiddenAreasChangedEvent { + return this; + } +} + export class CursorStateChangedEvent { public readonly kind = OutgoingViewModelEventKind.CursorStateChanged; @@ -388,6 +405,7 @@ export type OutgoingViewModelEvent = ( | FocusChangedEvent | ScrollChangedEvent | ViewZonesChangedEvent + | HiddenAreasChangedEvent | ReadOnlyEditAttemptEvent | CursorStateChangedEvent ); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index ab7dfb12a0..5ccfe63c48 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -12,16 +12,16 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IConfiguration, IViewState, ScrollType, ICursorState, ICommand, INewScrollPosition } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions, IIdentifiedSingleEditOperation, ICursorStateComputer, PositionAffinity } from 'vs/editor/common/model'; -import { ModelDecorationOverviewRulerOptions, ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'; +import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions, IIdentifiedSingleEditOperation, ICursorStateComputer, PositionAffinity, IndentGuide, BracketGuideOptions } from 'vs/editor/common/model'; +import { ModelDecorationOverviewRulerOptions, ModelDecorationMinimapOptions, ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; -import { ColorId, LanguageId, TokenizationRegistry } from 'vs/editor/common/modes'; +import { ColorId, TokenizationRegistry } from 'vs/editor/common/modes'; import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTokensColorTracker'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout'; import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/splitLinesCollection'; -import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, IOverviewRulerDecorations, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel'; +import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration, OverviewRulerDecorationsGroup } from 'vs/editor/common/viewModel/viewModel'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as platform from 'vs/base/common/platform'; @@ -32,6 +32,7 @@ import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; import { ViewModelEventDispatcher, OutgoingViewModelEvent, FocusChangedEvent, ScrollChangedEvent, ViewZonesChangedEvent, ViewModelEventsCollector, ReadOnlyEditAttemptEvent } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; +import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; const USE_IDENTITY_LINES_COLLECTION = true; @@ -70,7 +71,7 @@ export class ViewModel extends Disposable implements IViewModel { this.model = model; this._eventDispatcher = new ViewModelEventDispatcher(); this.onEvent = this._eventDispatcher.onEvent; - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration); this._tokenizeViewportSoon = this._register(new RunOnceScheduler(() => this.tokenizeViewport(), 50)); this._updateConfigurationViewLineCount = this._register(new RunOnceScheduler(() => this._updateConfigurationViewLineCountNow(), 0)); this._hasFocus = false; @@ -172,9 +173,17 @@ export class ViewModel extends Disposable implements IViewModel { public tokenizeViewport(): void { const linesViewportData = this.viewLayout.getLinesViewportData(); - const startPosition = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(linesViewportData.startLineNumber, 1)); - const endPosition = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(linesViewportData.endLineNumber, 1)); - this.model.tokenizeViewport(startPosition.lineNumber, endPosition.lineNumber); + const viewVisibleRange = new Range( + linesViewportData.startLineNumber, + this.getLineMinColumn(linesViewportData.startLineNumber), + linesViewportData.endLineNumber, + this.getLineMaxColumn(linesViewportData.endLineNumber) + ); + const modelVisibleRanges = this._toModelVisibleRanges(viewVisibleRange); + + for (const modelVisibleRange of modelVisibleRanges) { + this.model.tokenizeViewport(modelVisibleRange.startLineNumber, modelVisibleRange.endLineNumber); + } } public setHasFocus(hasFocus: boolean): void { @@ -244,7 +253,7 @@ export class ViewModel extends Disposable implements IViewModel { } if (CursorConfiguration.shouldRecreate(e)) { - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration); this._cursor.updateConfiguration(this.cursorConfig); } } @@ -405,12 +414,12 @@ export class ViewModel extends Disposable implements IViewModel { this._register(this.model.onDidChangeLanguageConfiguration((e) => { this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewLanguageConfigurationEvent()); - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration); this._cursor.updateConfiguration(this.cursorConfig); })); this._register(this.model.onDidChangeLanguage((e) => { - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration); this._cursor.updateConfiguration(this.cursorConfig); })); @@ -431,7 +440,7 @@ export class ViewModel extends Disposable implements IViewModel { this._updateConfigurationViewLineCount.schedule(); } - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration); this._cursor.updateConfiguration(this.cursorConfig); })); @@ -442,9 +451,10 @@ export class ViewModel extends Disposable implements IViewModel { } public setHiddenAreas(ranges: Range[]): void { + let lineMappingChanged = false; try { const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); - let lineMappingChanged = this._lines.setHiddenAreas(ranges); + lineMappingChanged = this._lines.setHiddenAreas(ranges); if (lineMappingChanged) { eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); @@ -458,6 +468,10 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.endEmitViewEvents(); } this._updateConfigurationViewLineCount.schedule(); + + if (lineMappingChanged) { + this._eventDispatcher.emitOutgoingEvent(new ViewZonesChangedEvent()); + } } public getVisibleRangesPlusViewportAboveBelow(): Range[] { @@ -614,6 +628,10 @@ export class ViewModel extends Disposable implements IViewModel { return this._lines.getViewLinesIndentGuides(startLineNumber, endLineNumber); } + public getBracketGuidesInRangeByLine(startLineNumber: number, endLineNumber: number, activePosition: IPosition | null, options: BracketGuideOptions): IndentGuide[][] { + return this._lines.getViewLinesBracketGuides(startLineNumber, endLineNumber, activePosition, options); + } + public getLineContent(lineNumber: number): string { return this._lines.getViewLineContent(lineNumber); } @@ -697,8 +715,26 @@ export class ViewModel extends Disposable implements IViewModel { ); } - public getAllOverviewRulerDecorations(theme: EditorTheme): IOverviewRulerDecorations { - return this._lines.getAllOverviewRulerDecorations(this._editorId, filterValidationDecorations(this._configuration.options), theme); + public getAllOverviewRulerDecorations(theme: EditorTheme): OverviewRulerDecorationsGroup[] { + const decorations = this.model.getOverviewRulerDecorations(this._editorId, filterValidationDecorations(this._configuration.options)); + const result = new OverviewRulerDecorations(); + for (const decoration of decorations) { + const decorationOptions = decoration.options; + const opts = decorationOptions.overviewRuler; + if (!opts) { + continue; + } + const lane = opts.position; + if (lane === 0) { + continue; + } + const color = opts.getColor(theme); + const viewStartLineNumber = this.coordinatesConverter.getViewLineNumberOfModelPosition(decoration.range.startLineNumber, decoration.range.startColumn); + const viewEndLineNumber = this.coordinatesConverter.getViewLineNumberOfModelPosition(decoration.range.endLineNumber, decoration.range.endColumn); + + result.accept(color, decorationOptions.zIndex, viewStartLineNumber, viewEndLineNumber, lane); + } + return result.asArray; } public invalidateOverviewRulerColorCache(): void { @@ -820,8 +856,8 @@ export class ViewModel extends Disposable implements IViewModel { } public getRichTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean): { html: string, mode: string } | null { - const languageId = this.model.getLanguageIdentifier(); - if (languageId.id === LanguageId.PlainText) { + const languageId = this.model.getLanguageId(); + if (languageId === PLAINTEXT_MODE_ID) { return null; } @@ -861,7 +897,7 @@ export class ViewModel extends Disposable implements IViewModel { } return { - mode: languageId.language, + mode: languageId, html: ( `
'] }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new BlockCommentCommand(sel, true), expectedLines, expectedSelection); + testCommand(lines, mode.languageId, selection, (sel) => new BlockCommentCommand(sel, true), expectedLines, expectedSelection); mode.dispose(); } @@ -475,7 +475,7 @@ suite('Editor Contrib - Block Comment Command', () => { test('insertSpace false', () => { function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { let mode = new CommentMode({ lineComment: '!@#', blockComment: ['<0', '0>'] }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new BlockCommentCommand(sel, false), expectedLines, expectedSelection); + testCommand(lines, mode.languageId, selection, (sel) => new BlockCommentCommand(sel, false), expectedLines, expectedSelection); mode.dispose(); } @@ -494,7 +494,7 @@ suite('Editor Contrib - Block Comment Command', () => { test('insertSpace false does not remove space', () => { function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { let mode = new CommentMode({ lineComment: '!@#', blockComment: ['<0', '0>'] }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new BlockCommentCommand(sel, false), expectedLines, expectedSelection); + testCommand(lines, mode.languageId, selection, (sel) => new BlockCommentCommand(sel, false), expectedLines, expectedSelection); mode.dispose(); } diff --git a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts index 0198e5f02b..5f0490eadd 100644 --- a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts @@ -4,30 +4,41 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Selection } from 'vs/editor/common/core/selection'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import * as modes from 'vs/editor/common/modes'; +import { ICommand } from 'vs/editor/common/editorCommon'; +import { ColorId, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/modes'; import { CommentRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { ILinePreflightData, IPreflightData, ISimpleModel, LineCommentCommand, Type } from 'vs/editor/contrib/comment/lineCommentCommand'; import { testCommand } from 'vs/editor/test/browser/testCommand'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { CommentMode } from 'vs/editor/test/common/commentMode'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +function createTestCommandHelper(commentsConfig: CommentRule, commandFactory: (selection: Selection) => ICommand): (lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection) => void { + return (lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection) => { + const setup = (accessor: ServicesAccessor, disposables: DisposableStore) => { + disposables.add(new CommentMode(commentsConfig)); + }; + testCommand(lines, CommentMode.id, selection, commandFactory, expectedLines, expectedSelection, false, setup); + }; +} + suite('Editor Contrib - Line Comment Command', () => { - function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '!@#', blockComment: [''] }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true), expectedLines, expectedSelection); - mode.dispose(); - } + const testLineCommentCommand = createTestCommandHelper( + { lineComment: '!@#', blockComment: [''] }, + (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true) + ); - function testAddLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '!@#', blockComment: [''] }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.ForceAdd, true, true), expectedLines, expectedSelection); - mode.dispose(); - } + const testAddLineCommentCommand = createTestCommandHelper( + { lineComment: '!@#', blockComment: [''] }, + (sel) => new LineCommentCommand(sel, 4, Type.ForceAdd, true, true) + ); test('comment single line', function () { testLineCommentCommand( @@ -45,11 +56,10 @@ suite('Editor Contrib - Line Comment Command', () => { }); test('case insensitive', function () { - function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: 'rem' }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true), expectedLines, expectedSelection); - mode.dispose(); - } + const testLineCommentCommand = createTestCommandHelper( + { lineComment: 'rem' }, + (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true) + ); testLineCommentCommand( [ @@ -629,11 +639,10 @@ suite('Editor Contrib - Line Comment Command', () => { }); test('insertSpace false', () => { - function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '!@#' }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle, false, true), expectedLines, expectedSelection); - mode.dispose(); - } + const testLineCommentCommand = createTestCommandHelper( + { lineComment: '!@#' }, + (sel) => new LineCommentCommand(sel, 4, Type.Toggle, false, true) + ); testLineCommentCommand( [ @@ -648,11 +657,10 @@ suite('Editor Contrib - Line Comment Command', () => { }); test('insertSpace false does not remove space', () => { - function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '!@#' }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle, false, true), expectedLines, expectedSelection); - mode.dispose(); - } + const testLineCommentCommand = createTestCommandHelper( + { lineComment: '!@#' }, + (sel) => new LineCommentCommand(sel, 4, Type.Toggle, false, true) + ); testLineCommentCommand( [ @@ -667,11 +675,11 @@ suite('Editor Contrib - Line Comment Command', () => { }); suite('ignoreEmptyLines false', () => { - function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '!@#', blockComment: [''] }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, false), expectedLines, expectedSelection); - mode.dispose(); - } + + const testLineCommentCommand = createTestCommandHelper( + { lineComment: '!@#', blockComment: [''] }, + (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, false) + ); test('does not ignore whitespace lines', () => { testLineCommentCommand( @@ -760,11 +768,10 @@ suite('Editor Contrib - Line Comment Command', () => { suite('Editor Contrib - Line Comment As Block Comment', () => { - function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '', blockComment: ['(', ')'] }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true), expectedLines, expectedSelection); - mode.dispose(); - } + const testLineCommentCommand = createTestCommandHelper( + { lineComment: '', blockComment: ['(', ')'] }, + (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true) + ); test('fall back to block comment command', function () { testLineCommentCommand( @@ -871,11 +878,11 @@ suite('Editor Contrib - Line Comment As Block Comment', () => { }); suite('Editor Contrib - Line Comment As Block Comment 2', () => { - function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: null, blockComment: [''] }); - testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true), expectedLines, expectedSelection); - mode.dispose(); - } + + const testLineCommentCommand = createTestCommandHelper( + { lineComment: null, blockComment: [''] }, + (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true) + ); test('no selection => uses indentation', function () { testLineCommentCommand( @@ -1068,29 +1075,33 @@ suite('Editor Contrib - Line Comment As Block Comment 2', () => { suite('Editor Contrib - Line Comment in mixed modes', () => { - const OUTER_LANGUAGE_ID = new modes.LanguageIdentifier('outerMode', 3); - const INNER_LANGUAGE_ID = new modes.LanguageIdentifier('innerMode', 4); + const OUTER_LANGUAGE_ID = 'outerMode'; + const INNER_LANGUAGE_ID = 'innerMode'; class OuterMode extends MockMode { - constructor(commentsConfig: CommentRule) { + constructor( + commentsConfig: CommentRule, + @IModeService modeService: IModeService + ) { super(OUTER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { comments: commentsConfig })); - this._register(modes.TokenizationRegistry.register(this.getLanguageIdentifier().language, { - getInitialState: (): modes.IState => NULL_STATE, + this._register(TokenizationRegistry.register(this.languageId, { + getInitialState: (): IState => NULL_STATE, tokenize: () => { throw new Error('not implemented'); }, - tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { - let languageId = (/^ /.test(line) ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID); + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { + const languageId = (/^ /.test(line) ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID); + const encodedLanguageId = modeService.languageIdCodec.encodeLanguageId(languageId); - let tokens = new Uint32Array(1 << 1); + const tokens = new Uint32Array(1 << 1); tokens[(0 << 1)] = 0; tokens[(0 << 1) + 1] = ( - (modes.ColorId.DefaultForeground << modes.MetadataConsts.FOREGROUND_OFFSET) - | (languageId.id << modes.MetadataConsts.LANGUAGEID_OFFSET) + (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) + | (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) ); return new TokenizationResult2(tokens, state); } @@ -1101,26 +1112,30 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { class InnerMode extends MockMode { constructor(commentsConfig: CommentRule) { super(INNER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { comments: commentsConfig })); } } function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let outerMode = new OuterMode({ lineComment: '//', blockComment: ['/*', '*/'] }); - let innerMode = new InnerMode({ lineComment: null, blockComment: ['{/*', '*/}'] }); + + const setup = (accessor: ServicesAccessor, disposables: DisposableStore) => { + const instantiationService = accessor.get(IInstantiationService); + disposables.add(instantiationService.createInstance(OuterMode, { lineComment: '//', blockComment: ['/*', '*/'] })); + disposables.add(instantiationService.createInstance(InnerMode, { lineComment: null, blockComment: ['{/*', '*/}'] })); + }; + testCommand( lines, - outerMode.getLanguageIdentifier(), + OUTER_LANGUAGE_ID, selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle, true, true), expectedLines, expectedSelection, - true + true, + setup ); - innerMode.dispose(); - outerMode.dispose(); } test('issue #24047 (part 1): Commenting code in JSX files', () => { diff --git a/src/vs/editor/contrib/contextmenu/contextmenu.ts b/src/vs/editor/contrib/contextmenu/contextmenu.ts index 27970cc894..2d4fbaa0a4 100644 --- a/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -3,27 +3,28 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; -import { KeyCode, KeyMod, ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isIOS } from 'vs/base/common/platform'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITextModel } from 'vs/editor/common/model'; +import * as nls from 'vs/nls'; import { IMenuService, MenuId, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -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 { diff --git a/src/vs/editor/contrib/cursorUndo/cursorUndo.ts b/src/vs/editor/contrib/cursorUndo/cursorUndo.ts index c72ee3a5e2..d9908f3562 100644 --- a/src/vs/editor/contrib/cursorUndo/cursorUndo.ts +++ b/src/vs/editor/contrib/cursorUndo/cursorUndo.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Selection } from 'vs/editor/common/core/selection'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import * as nls from 'vs/nls'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; class CursorState { @@ -134,7 +134,7 @@ export class CursorUndo extends EditorAction { precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_U, + primary: KeyMod.CtrlCmd | KeyCode.KeyU, weight: KeybindingWeight.EditorContrib } }); diff --git a/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts b/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts index 10f4f875fd..938bf9faf4 100644 --- a/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts +++ b/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { CoreEditingCommands, CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; import { Selection } from 'vs/editor/common/core/selection'; -import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { CursorUndo, CursorUndoRedoController } from 'vs/editor/contrib/cursorUndo/cursorUndo'; import { Handler } from 'vs/editor/common/editorCommon'; -import { CoreNavigationCommands, CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import { CursorUndo, CursorUndoRedoController } from 'vs/editor/contrib/cursorUndo/cursorUndo'; +import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; suite('FindController', () => { diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index 85916d6d76..253db08759 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -3,24 +3,24 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./dnd'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { ICodeEditor, IEditorMouseEvent, IMouseTarget, MouseTargetType, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; +import 'vs/css!./dnd'; +import { ICodeEditor, IEditorMouseEvent, IMouseTarget, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; -import { DragAndDropCommand } from 'vs/editor/contrib/dnd/dragAndDropCommand'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { DragAndDropCommand } from 'vs/editor/contrib/dnd/dragAndDropCommand'; function hasTriggerModifier(e: IKeyboardEvent | IMouseEvent): boolean { if (isMacintosh) { diff --git a/src/vs/editor/contrib/dnd/dragAndDropCommand.ts b/src/vs/editor/contrib/dnd/dragAndDropCommand.ts index 87d8e91521..51d5435cea 100644 --- a/src/vs/editor/contrib/dnd/dragAndDropCommand.ts +++ b/src/vs/editor/contrib/dnd/dragAndDropCommand.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; -import { Selection } from 'vs/editor/common/core/selection'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/editor/contrib/documentSymbols/documentSymbols.ts b/src/vs/editor/contrib/documentSymbols/documentSymbols.ts index 23e431cddd..e9e71618ef 100644 --- a/src/vs/editor/contrib/documentSymbols/documentSymbols.ts +++ b/src/vs/editor/contrib/documentSymbols/documentSymbols.ts @@ -3,15 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { assertType } from 'vs/base/common/types'; export async function getDocumentSymbols(document: ITextModel, flat: boolean, token: CancellationToken): Promise { const model = await OutlineModel.create(document, token); diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index 53743dc554..0d6239a49a 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -6,16 +6,16 @@ import { binarySearch, coalesceInPlace, equals } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { Iterable } from 'vs/base/common/iterator'; import { LRUCache } from 'vs/base/common/map'; import { commonPrefixLength } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; -import { MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { Iterable } from 'vs/base/common/iterator'; import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; -import { URI } from 'vs/base/common/uri'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; export abstract class TreeElement { diff --git a/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts b/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts index d43ccdb899..8a21516bfc 100644 --- a/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts +++ b/src/vs/editor/contrib/documentSymbols/test/outlineModel.test.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { OutlineElement, OutlineGroup, OutlineModel } from '../outlineModel'; -import { SymbolKind, DocumentSymbol, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; -import { Range } from 'vs/editor/common/core/range'; -import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { URI } from 'vs/base/common/uri'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { Range } from 'vs/editor/common/core/range'; +import { DocumentSymbol, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { OutlineElement, OutlineGroup, OutlineModel } from '../outlineModel'; suite('OutlineModel', function () { @@ -38,6 +38,7 @@ suite('OutlineModel', function () { assert.strictEqual(count, 2); reg.dispose(); + model.dispose(); }); test('OutlineModel#create, cached/cancel', async function () { @@ -69,6 +70,7 @@ suite('OutlineModel', function () { assert.strictEqual(isCancelled, true); reg.dispose(); + model.dispose(); }); function fakeSymbolInformation(range: Range, name: string = 'foo'): DocumentSymbol { diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index c41e2297b3..f3f8f6bb92 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -3,29 +3,29 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { Delayer } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution, MultiEditorAction, registerMultiEditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorCommand, MultiEditorAction, registerEditorAction, registerEditorCommand, registerEditorContribution, registerMultiEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding, CONTEXT_REPLACE_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel'; +import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, CONTEXT_REPLACE_INPUT_FOCUSED, FindModelBoundToEditorModel, FIND_IDS, ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; import { FindOptionsWidget } from 'vs/editor/contrib/find/findOptionsWidget'; import { FindReplaceState, FindReplaceStateChangedEvent, INewFindReplaceState } from 'vs/editor/contrib/find/findState'; import { FindWidget, IFindController } from 'vs/editor/contrib/find/findWidget'; +import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; const SEARCH_STRING_MAX_LENGTH = 524288; @@ -492,7 +492,7 @@ export const StartFindAction = registerMultiEditorAction(new MultiEditorAction({ precondition: ContextKeyExpr.or(EditorContextKeys.focus, ContextKeyExpr.has('editorIsOpen')), kbOpts: { kbExpr: null, - primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + primary: KeyMod.CtrlCmd | KeyCode.KeyF, weight: KeybindingWeight.EditorContrib }, menuOpts: { @@ -532,7 +532,7 @@ export class StartFindWithSelectionAction extends EditorAction { kbExpr: null, primary: 0, mac: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_E, + primary: KeyMod.CtrlCmd | KeyCode.KeyE, }, weight: KeybindingWeight.EditorContrib } @@ -589,7 +589,7 @@ export class NextMatchFindAction extends MatchFindAction { kbOpts: [{ kbExpr: EditorContextKeys.focus, primary: KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] }, + mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyG, secondary: [KeyCode.F3] }, weight: KeybindingWeight.EditorContrib }, { kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), @@ -621,7 +621,7 @@ export class PreviousMatchFindAction extends MatchFindAction { kbOpts: [{ kbExpr: EditorContextKeys.focus, primary: KeyMod.Shift | KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] }, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG, secondary: [KeyMod.Shift | KeyCode.F3] }, weight: KeybindingWeight.EditorContrib }, { kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), @@ -719,8 +719,8 @@ export const StartFindReplaceAction = registerMultiEditorAction(new MultiEditorA precondition: ContextKeyExpr.or(EditorContextKeys.focus, ContextKeyExpr.has('editorIsOpen')), kbOpts: { kbExpr: null, - primary: KeyMod.CtrlCmd | KeyCode.KEY_H, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_F }, + primary: KeyMod.CtrlCmd | KeyCode.KeyH, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyF }, weight: KeybindingWeight.EditorContrib }, menuOpts: { @@ -869,7 +869,7 @@ registerEditorCommand(new FindCommand({ kbOpts: { weight: KeybindingWeight.EditorContrib + 5, kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_1 + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit1 } })); diff --git a/src/vs/editor/contrib/find/findDecorations.ts b/src/vs/editor/contrib/find/findDecorations.ts index f9313f0a24..076cc8e020 100644 --- a/src/vs/editor/contrib/find/findDecorations.ts +++ b/src/vs/editor/contrib/find/findDecorations.ts @@ -7,9 +7,9 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { FindMatch, IModelDecorationsChangeAccessor, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, MinimapPosition } from 'vs/editor/common/model'; +import { FindMatch, IModelDecorationsChangeAccessor, IModelDeltaDecoration, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { overviewRulerFindMatchForeground, minimapFindMatch } from 'vs/platform/theme/common/colorRegistry'; +import { minimapFindMatch, overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; export class FindDecorations implements IDisposable { @@ -294,6 +294,7 @@ export class FindDecorations implements IDisposable { public static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({ description: 'find-match', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + zIndex: 10, className: 'findMatch', showIfCollapsed: true, overviewRuler: { diff --git a/src/vs/editor/contrib/find/findModel.ts b/src/vs/editor/contrib/find/findModel.ts index 56fa65a3d4..0a5517c88b 100644 --- a/src/vs/editor/contrib/find/findModel.ts +++ b/src/vs/editor/contrib/find/findModel.ts @@ -3,27 +3,27 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { findFirstInSorted } from 'vs/base/common/arrays'; import { RunOnceScheduler, TimeoutTimer } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Constants } from 'vs/base/common/uint'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { Constants } from 'vs/base/common/uint'; -import { ScrollType, ICommand } from 'vs/editor/common/editorCommon'; +import { ICommand, ScrollType } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, FindMatch, ITextModel } from 'vs/editor/common/model'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState'; import { ReplaceAllCommand } from 'vs/editor/contrib/find/replaceAllCommand'; -import { ReplacePattern, parseReplaceString } from 'vs/editor/contrib/find/replacePattern'; +import { parseReplaceString, ReplacePattern } from 'vs/editor/contrib/find/replacePattern'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { findFirstInSorted } from 'vs/base/common/arrays'; export const CONTEXT_FIND_WIDGET_VISIBLE = new RawContextKey('findWidgetVisible', false); export const CONTEXT_FIND_WIDGET_NOT_VISIBLE = CONTEXT_FIND_WIDGET_VISIBLE.toNegated(); @@ -32,24 +32,24 @@ export const CONTEXT_FIND_INPUT_FOCUSED = new RawContextKey('findInputF export const CONTEXT_REPLACE_INPUT_FOCUSED = new RawContextKey('replaceInputFocussed', false); export const ToggleCaseSensitiveKeybinding: IKeybindings = { - primary: KeyMod.Alt | KeyCode.KEY_C, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C } + primary: KeyMod.Alt | KeyCode.KeyC, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC } }; export const ToggleWholeWordKeybinding: IKeybindings = { - primary: KeyMod.Alt | KeyCode.KEY_W, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_W } + primary: KeyMod.Alt | KeyCode.KeyW, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyW } }; export const ToggleRegexKeybinding: IKeybindings = { - primary: KeyMod.Alt | KeyCode.KEY_R, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R } + primary: KeyMod.Alt | KeyCode.KeyR, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyR } }; export const ToggleSearchScopeKeybinding: IKeybindings = { - primary: KeyMod.Alt | KeyCode.KEY_L, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L } + primary: KeyMod.Alt | KeyCode.KeyL, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyL } }; export const TogglePreserveCaseKeybinding: IKeybindings = { - primary: KeyMod.Alt | KeyCode.KEY_P, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_P } + primary: KeyMod.Alt | KeyCode.KeyP, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyP } }; export const FIND_IDS = { diff --git a/src/vs/editor/contrib/find/findOptionsWidget.ts b/src/vs/editor/contrib/find/findOptionsWidget.ts index 78792824be..d6bb6289ea 100644 --- a/src/vs/editor/contrib/find/findOptionsWidget.ts +++ b/src/vs/editor/contrib/find/findOptionsWidget.ts @@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPosit import { FIND_IDS } from 'vs/editor/contrib/find/findModel'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { contrastBorder, editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, widgetShadow, editorWidgetForeground, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorWidgetBackground, editorWidgetForeground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; export class FindOptionsWidget extends Widget implements IOverlayWidget { diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index bd10db4910..ee300dde38 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -153,6 +153,7 @@ left: 3px; width: 18px; height: 100%; + border-radius: 0; box-sizing: border-box; } diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 2aa4d9b86f..d3ec953e1e 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -3,41 +3,42 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./findWidget'; -import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; -import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; -import { IVerticalSashLayoutProvider, ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; +import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; +import { ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { Widget } from 'vs/base/browser/ui/widget'; import { Delayer } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { toDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; +import 'vs/css!./findWidget'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IViewZone, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_REPLACE_INPUT_FOCUSED, FIND_IDS, MATCHES_LIMIT } from 'vs/editor/contrib/find/findModel'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState'; +import * as nls from 'vs/nls'; +import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; +import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget'; +import { showHistoryKeybindingHint } from 'vs/platform/browser/historyWidgetKeybindingHint'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { contrastBorder, editorFindMatch, editorFindMatchBorder, editorFindMatchHighlight, editorFindMatchHighlightBorder, editorFindRangeHighlight, editorFindRangeHighlightBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetResizeBorder, errorForeground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, focusBorder, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget'; -import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Codicon } from 'vs/base/common/codicons'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { contrastBorder, editorFindMatch, editorFindMatchBorder, editorFindMatchHighlight, editorFindMatchHighlightBorder, editorFindRangeHighlight, editorFindRangeHighlightBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, editorWidgetResizeBorder, errorForeground, focusBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, toolbarHoverBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon, widgetClose } from 'vs/platform/theme/common/iconRegistry'; +import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); const findCollapsedIcon = registerIcon('find-collapsed', Codicon.chevronRight, nls.localize('findCollapsedIcon', 'Icon to indicate that the editor find widget is collapsed.')); @@ -851,9 +852,14 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL private _onFindInputKeyDown(e: IKeyboardEvent): void { if (e.equals(ctrlKeyMod | KeyCode.Enter)) { - this._findInput.inputBox.insertAtCursor('\n'); - e.preventDefault(); - return; + if (this._keybindingService.dispatchEvent(e, e.target)) { + e.preventDefault(); + return; + } else { + this._findInput.inputBox.insertAtCursor('\n'); + e.preventDefault(); + return; + } } if (e.equals(KeyCode.Tab)) { @@ -883,21 +889,26 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL private _onReplaceInputKeyDown(e: IKeyboardEvent): void { if (e.equals(ctrlKeyMod | KeyCode.Enter)) { - if (platform.isWindows && platform.isNative && !this._ctrlEnterReplaceAllWarningPrompted) { - // this is the first time when users press Ctrl + Enter to replace all - this._notificationService.info( - nls.localize('ctrlEnter.keybindingChanged', - 'Ctrl+Enter now inserts line break instead of replacing all. You can modify the keybinding for editor.action.replaceAll to override this behavior.') - ); + if (this._keybindingService.dispatchEvent(e, e.target)) { + e.preventDefault(); + return; + } else { + if (platform.isWindows && platform.isNative && !this._ctrlEnterReplaceAllWarningPrompted) { + // this is the first time when users press Ctrl + Enter to replace all + this._notificationService.info( + nls.localize('ctrlEnter.keybindingChanged', + 'Ctrl+Enter now inserts line break instead of replacing all. You can modify the keybinding for editor.action.replaceAll to override this behavior.') + ); - this._ctrlEnterReplaceAllWarningPrompted = true; - this._storageService.store(ctrlEnterReplaceAllWarningPromptedKey, true, StorageScope.GLOBAL, StorageTarget.USER); + this._ctrlEnterReplaceAllWarningPrompted = true; + this._storageService.store(ctrlEnterReplaceAllWarningPromptedKey, true, StorageScope.GLOBAL, StorageTarget.USER); + } + this._replaceInput.inputBox.insertAtCursor('\n'); + e.preventDefault(); + return; } - this._replaceInput.inputBox.insertAtCursor('\n'); - e.preventDefault(); - return; } if (e.equals(KeyCode.Tab)) { @@ -966,7 +977,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL }, flexibleHeight, flexibleWidth, - flexibleMaxHeight: 118 + flexibleMaxHeight: 118, + showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService) }, this._contextKeyService, true)); this._findInput.setRegex(!!this._state.isRegex); this._findInput.setCaseSensitive(!!this._state.matchCase); @@ -1105,7 +1117,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL history: [], flexibleHeight, flexibleWidth, - flexibleMaxHeight: 118 + flexibleMaxHeight: 118, + showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService) }, this._contextKeyService, true)); this._replaceInput.setPreserveCase(!!this._state.preserveCase); this._register(this._replaceInput.onKeyDown((e) => this._onReplaceInputKeyDown(e))); diff --git a/src/vs/editor/contrib/find/replaceAllCommand.ts b/src/vs/editor/contrib/find/replaceAllCommand.ts index 1d8291e522..1adc05f377 100644 --- a/src/vs/editor/contrib/find/replaceAllCommand.ts +++ b/src/vs/editor/contrib/find/replaceAllCommand.ts @@ -5,7 +5,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; +import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; interface IEditOperation { diff --git a/src/vs/editor/contrib/find/test/findModel.test.ts b/src/vs/editor/contrib/find/test/findModel.test.ts index 91cdb30b02..cbe80098d2 100644 --- a/src/vs/editor/contrib/find/test/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/findModel.test.ts @@ -5,18 +5,14 @@ import * as assert from 'assert'; import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; -import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { FindModelBoundToEditorModel } from 'vs/editor/contrib/find/findModel'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; -import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; suite('FindModel', () => { @@ -44,10 +40,9 @@ suite('FindModel', () => { ptBuilder.acceptChunk(text.substr(94, 101)); ptBuilder.acceptChunk(text.substr(195, 59)); const factory = ptBuilder.finish(); - withTestCodeEditor([], - { - model: new TextModel(factory, TextModel.DEFAULT_CREATION_OPTIONS, null, null, new UndoRedoService(new TestDialogService(), new TestNotificationService())) - }, + withTestCodeEditor( + factory, + {}, (editor) => callback(editor as IActiveCodeEditor) ); }); diff --git a/src/vs/editor/contrib/find/test/replacePattern.test.ts b/src/vs/editor/contrib/find/test/replacePattern.test.ts index 20a051bf4a..4064ba4bf4 100644 --- a/src/vs/editor/contrib/find/test/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/replacePattern.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ReplacePattern, ReplacePiece, parseReplaceString } from 'vs/editor/contrib/find/replacePattern'; import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search'; +import { parseReplaceString, ReplacePattern, ReplacePiece } from 'vs/editor/contrib/find/replacePattern'; suite('Replace Pattern test', () => { diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index e7e13e4484..3248f1a29b 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -3,38 +3,38 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./folding'; -import * as nls from 'vs/nls'; -import * as types from 'vs/base/common/types'; -import { escapeRegExpCharacters } from 'vs/base/common/strings'; -import { RunOnceScheduler, Delayer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; -import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ScrollType, IEditorContribution } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, registerInstantiatedEditorAction } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { FoldingModel, setCollapseStateAtLevel, CollapseMemento, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateForType, setCollapseStateForRest, toggleCollapseState, setCollapseStateUp, getParentFoldLine as getParentFoldLine, getPreviousFoldLine, getNextFoldLine } from 'vs/editor/contrib/folding/foldingModel'; -import { FoldingDecorationProvider, foldingCollapsedIcon, foldingExpandedIcon } from './foldingDecorations'; -import { FoldingRegions, FoldingRegion } from './foldingRanges'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { IMarginData, IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; -import { HiddenRangeModel } from 'vs/editor/contrib/folding/hiddenRangeModel'; -import { IRange } from 'vs/editor/common/core/range'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { IndentRangeProvider } from 'vs/editor/contrib/folding/indentRangeProvider'; -import { IPosition } from 'vs/editor/common/core/position'; -import { FoldingRangeProviderRegistry, FoldingRangeKind } from 'vs/editor/common/modes'; -import { SyntaxRangeProvider, ID_SYNTAX_PROVIDER } from './syntaxRangeProvider'; +import { CancelablePromise, createCancelablePromise, Delayer, RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { InitializingRangeProvider, ID_INIT_PROVIDER } from 'vs/editor/contrib/folding/intializingRangeProvider'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { registerColor, editorSelectionBackground, transparent, iconForeground } from 'vs/platform/theme/common/colorRegistry'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import * as types from 'vs/base/common/types'; +import 'vs/css!./folding'; +import { IEmptyContentData, IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction, registerEditorContribution, registerInstantiatedEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IRange } from 'vs/editor/common/core/range'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITextModel } from 'vs/editor/common/model'; +import { FoldingRangeKind, FoldingRangeProviderRegistry } from 'vs/editor/common/modes'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { CollapseMemento, FoldingModel, getNextFoldLine, getParentFoldLine as getParentFoldLine, getPreviousFoldLine, setCollapseStateAtLevel, setCollapseStateForMatchingLines, setCollapseStateForRest, setCollapseStateForType, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateUp, toggleCollapseState } from 'vs/editor/contrib/folding/foldingModel'; +import { HiddenRangeModel } from 'vs/editor/contrib/folding/hiddenRangeModel'; +import { IndentRangeProvider } from 'vs/editor/contrib/folding/indentRangeProvider'; +import { ID_INIT_PROVIDER, InitializingRangeProvider } from 'vs/editor/contrib/folding/intializingRangeProvider'; +import * as nls from 'vs/nls'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { editorSelectionBackground, iconForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { foldingCollapsedIcon, FoldingDecorationProvider, foldingExpandedIcon } from './foldingDecorations'; +import { FoldingRegion, FoldingRegions } from './foldingRanges'; +import { ID_SYNTAX_PROVIDER, SyntaxRangeProvider } from './syntaxRangeProvider'; const CONTEXT_FOLDING_ENABLED = new RawContextKey('foldingEnabled', false); @@ -575,9 +575,9 @@ class UnfoldAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketRight, mac: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketRight }, weight: KeybindingWeight.EditorContrib }, @@ -639,7 +639,7 @@ class UnFoldRecursivelyAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_CLOSE_SQUARE_BRACKET), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.BracketRight), weight: KeybindingWeight.EditorContrib } }); @@ -660,9 +660,9 @@ class FoldAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketLeft, mac: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketLeft }, weight: KeybindingWeight.EditorContrib }, @@ -732,7 +732,7 @@ class ToggleFoldAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_L), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyL), weight: KeybindingWeight.EditorContrib } }); @@ -755,7 +755,7 @@ class FoldRecursivelyAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_OPEN_SQUARE_BRACKET), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.BracketLeft), weight: KeybindingWeight.EditorContrib } }); @@ -777,7 +777,7 @@ class FoldAllBlockCommentsAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_SLASH), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Slash), weight: KeybindingWeight.EditorContrib } }); @@ -791,7 +791,7 @@ class FoldAllBlockCommentsAction extends FoldingAction { if (!editorModel) { return; } - let comments = LanguageConfigurationRegistry.getComments(editorModel.getLanguageIdentifier().id); + const comments = LanguageConfigurationRegistry.getComments(editorModel.getLanguageId()); if (comments && comments.blockCommentStartToken) { let regExp = new RegExp('^\\s*' + escapeRegExpCharacters(comments.blockCommentStartToken)); setCollapseStateForMatchingLines(foldingModel, regExp, true); @@ -810,7 +810,7 @@ class FoldAllRegionsAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_8), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Digit8), weight: KeybindingWeight.EditorContrib } }); @@ -824,7 +824,7 @@ class FoldAllRegionsAction extends FoldingAction { if (!editorModel) { return; } - let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageIdentifier().id); + const foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageId()); if (foldingRules && foldingRules.markers && foldingRules.markers.start) { let regExp = new RegExp(foldingRules.markers.start); setCollapseStateForMatchingLines(foldingModel, regExp, true); @@ -843,7 +843,7 @@ class UnfoldAllRegionsAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_9), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Digit9), weight: KeybindingWeight.EditorContrib } }); @@ -857,7 +857,7 @@ class UnfoldAllRegionsAction extends FoldingAction { if (!editorModel) { return; } - let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageIdentifier().id); + const foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageId()); if (foldingRules && foldingRules.markers && foldingRules.markers.start) { let regExp = new RegExp(foldingRules.markers.start); setCollapseStateForMatchingLines(foldingModel, regExp, false); @@ -876,7 +876,7 @@ class FoldAllRegionsExceptAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_MINUS), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Minus), weight: KeybindingWeight.EditorContrib } }); @@ -899,7 +899,7 @@ class UnfoldAllRegionsExceptAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_EQUAL), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Equal), weight: KeybindingWeight.EditorContrib } }); @@ -921,7 +921,7 @@ class FoldAllAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_0), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Digit0), weight: KeybindingWeight.EditorContrib } }); @@ -942,7 +942,7 @@ class UnfoldAllAction extends FoldingAction { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_J), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyJ), weight: KeybindingWeight.EditorContrib } }); @@ -1002,8 +1002,8 @@ class GotoPreviousFoldAction extends FoldingAction { constructor() { super({ id: 'editor.gotoPreviousFold', - label: nls.localize('gotoPreviousFold.label', "Go to Previous Fold"), - alias: 'Go to Previous Fold', + label: nls.localize('gotoPreviousFold.label', "Go to Previous Folding Range"), + alias: 'Go to Previous Folding Range', precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, @@ -1033,8 +1033,8 @@ class GotoNextFoldAction extends FoldingAction { constructor() { super({ id: 'editor.gotoNextFold', - label: nls.localize('gotoNextFold.label', "Go to Next Fold"), - alias: 'Go to Next Fold', + label: nls.localize('gotoNextFold.label', "Go to Next Folding Range"), + alias: 'Go to Next Folding Range', precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, @@ -1085,7 +1085,7 @@ for (let i = 1; i <= 7; i++) { precondition: CONTEXT_FOLDING_ENABLED, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | (KeyCode.KEY_0 + i)), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | (KeyCode.Digit0 + i)), weight: KeybindingWeight.EditorContrib } }) diff --git a/src/vs/editor/contrib/folding/foldingDecorations.ts b/src/vs/editor/contrib/folding/foldingDecorations.ts index 9abf14611d..33a10ddb70 100644 --- a/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; +import { Codicon } from 'vs/base/common/codicons'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IDecorationProvider } from 'vs/editor/contrib/folding/foldingModel'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Codicon } from 'vs/base/common/codicons'; import { localize } from 'vs/nls'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts index 2460293978..9755267ec3 100644 --- a/src/vs/editor/contrib/folding/foldingModel.ts +++ b/src/vs/editor/contrib/folding/foldingModel.ts @@ -3,9 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITextModel, IModelDecorationOptions, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; -import { Event, Emitter } from 'vs/base/common/event'; -import { FoldingRegions, ILineRange, FoldingRegion } from './foldingRanges'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { FoldingRegion, FoldingRegions, ILineRange } from './foldingRanges'; export interface IDecorationProvider { getDecorationOption(isCollapsed: boolean, isHidden: boolean): IModelDecorationOptions; @@ -454,7 +454,8 @@ export function getParentFoldLine(lineNumber: number, foldingModel: FoldingModel */ export function getPreviousFoldLine(lineNumber: number, foldingModel: FoldingModel): number | null { let foldingRegion = foldingModel.getRegionAtLine(lineNumber); - if (foldingRegion !== null) { + // If on the folding range start line, go to previous sibling. + if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) { // If current line is not the start of the current fold, go to top line of current fold. If not, go to previous fold. if (lineNumber !== foldingRegion.startLineNumber) { return foldingRegion.startLineNumber; @@ -487,8 +488,8 @@ export function getPreviousFoldLine(lineNumber: number, foldingModel: FoldingMod if (foldingModel.regions.length > 0) { foldingRegion = foldingModel.regions.toRegion(foldingModel.regions.length - 1); while (foldingRegion !== null) { - // Found non-parent fold before current line. - if (foldingRegion.parentIndex === -1 && foldingRegion.startLineNumber < lineNumber) { + // Found fold before current line. + if (foldingRegion.startLineNumber < lineNumber) { return foldingRegion.startLineNumber; } if (foldingRegion.regionIndex > 0) { @@ -511,7 +512,8 @@ export function getPreviousFoldLine(lineNumber: number, foldingModel: FoldingMod */ export function getNextFoldLine(lineNumber: number, foldingModel: FoldingModel): number | null { let foldingRegion = foldingModel.getRegionAtLine(lineNumber); - if (foldingRegion !== null) { + // If on the folding range start line, go to next sibling. + if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) { // Find max line number to stay within parent. let expectedParentIndex = foldingRegion.parentIndex; let maxLineNumber = 0; @@ -543,8 +545,8 @@ export function getNextFoldLine(lineNumber: number, foldingModel: FoldingModel): if (foldingModel.regions.length > 0) { foldingRegion = foldingModel.regions.toRegion(0); while (foldingRegion !== null) { - // Found non-parent fold after current line. - if (foldingRegion.parentIndex === -1 && foldingRegion.startLineNumber > lineNumber) { + // Found fold after current line. + if (foldingRegion.startLineNumber > lineNumber) { return foldingRegion.startLineNumber; } if (foldingRegion.regionIndex < foldingModel.regions.length) { diff --git a/src/vs/editor/contrib/folding/hiddenRangeModel.ts b/src/vs/editor/contrib/folding/hiddenRangeModel.ts index b73d8313ae..c930f196fd 100644 --- a/src/vs/editor/contrib/folding/hiddenRangeModel.ts +++ b/src/vs/editor/contrib/folding/hiddenRangeModel.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { FoldingModel, CollapseMemento } from 'vs/editor/contrib/folding/foldingModel'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { Selection } from 'vs/editor/common/core/selection'; import { findFirstInSorted } from 'vs/base/common/arrays'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { CollapseMemento, FoldingModel } from 'vs/editor/contrib/folding/foldingModel'; export class HiddenRangeModel { private readonly _foldingModel: FoldingModel; diff --git a/src/vs/editor/contrib/folding/indentRangeProvider.ts b/src/vs/editor/contrib/folding/indentRangeProvider.ts index e9a7e3d60a..99b88bc3b2 100644 --- a/src/vs/editor/contrib/folding/indentRangeProvider.ts +++ b/src/vs/editor/contrib/folding/indentRangeProvider.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITextModel } from 'vs/editor/common/model'; -import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration'; -import { FoldingRegions, MAX_LINE_NUMBER } from 'vs/editor/contrib/folding/foldingRanges'; -import { TextModel } from 'vs/editor/common/model/textModel'; -import { RangeProvider } from './folding'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ITextModel } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { FoldingRegions, MAX_LINE_NUMBER } from 'vs/editor/contrib/folding/foldingRanges'; +import { RangeProvider } from './folding'; const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000; @@ -25,7 +25,7 @@ export class IndentRangeProvider implements RangeProvider { } compute(cancelationToken: CancellationToken): Promise { - let foldingRules = LanguageConfigurationRegistry.getFoldingRules(this.editorModel.getLanguageIdentifier().id); + let foldingRules = LanguageConfigurationRegistry.getFoldingRules(this.editorModel.getLanguageId()); let offSide = foldingRules && !!foldingRules.offSide; let markers = foldingRules && foldingRules.markers; return Promise.resolve(computeRanges(this.editorModel, offSide, markers)); diff --git a/src/vs/editor/contrib/folding/intializingRangeProvider.ts b/src/vs/editor/contrib/folding/intializingRangeProvider.ts index 5a0ea0c8dd..41c07a7d0c 100644 --- a/src/vs/editor/contrib/folding/intializingRangeProvider.ts +++ b/src/vs/editor/contrib/folding/intializingRangeProvider.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { FoldingRegions, ILineRange } from 'vs/editor/contrib/folding/foldingRanges'; -import { RangeProvider } from './folding'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IModelDeltaDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { FoldingRegions, ILineRange } from 'vs/editor/contrib/folding/foldingRanges'; import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider'; +import { RangeProvider } from './folding'; export const ID_INIT_PROVIDER = 'init'; diff --git a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts index 62e9441d32..1bbc88b41f 100644 --- a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FoldingRangeProvider, FoldingRange, FoldingContext } from 'vs/editor/common/modes'; -import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { ITextModel } from 'vs/editor/common/model'; -import { RangeProvider } from './folding'; -import { MAX_LINE_NUMBER, FoldingRegions } from './foldingRanges'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ITextModel } from 'vs/editor/common/model'; +import { FoldingContext, FoldingRange, FoldingRangeProvider } from 'vs/editor/common/modes'; +import { RangeProvider } from './folding'; +import { FoldingRegions, MAX_LINE_NUMBER } from './foldingRanges'; const MAX_FOLDING_REGIONS = 5000; diff --git a/src/vs/editor/contrib/folding/test/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/foldingModel.test.ts index 16e0150f77..d59d71c5b1 100644 --- a/src/vs/editor/contrib/folding/test/foldingModel.test.ts +++ b/src/vs/editor/contrib/folding/test/foldingModel.test.ts @@ -3,16 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { FoldingModel, setCollapseStateAtLevel, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateUp, setCollapseStateForRest, getParentFoldLine, getPreviousFoldLine, getNextFoldLine } from 'vs/editor/contrib/folding/foldingModel'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; -import { TrackedRangeStickiness, IModelDeltaDecoration, ITextModel, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { FoldingModel, getNextFoldLine, getParentFoldLine, getPreviousFoldLine, setCollapseStateAtLevel, setCollapseStateForMatchingLines, setCollapseStateForRest, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateUp } from 'vs/editor/contrib/folding/foldingModel'; import { FoldingRegion } from 'vs/editor/contrib/folding/foldingRanges'; -import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; interface ExpectedRegion { @@ -875,12 +875,19 @@ suite('Folding Model', () => { assert.strictEqual(getPreviousFoldLine(9, foldingModel), 5); assert.strictEqual(getPreviousFoldLine(5, foldingModel), 3); assert.strictEqual(getPreviousFoldLine(3, foldingModel), null); + // Test when not on a folding region start line. + assert.strictEqual(getPreviousFoldLine(4, foldingModel), 3); + assert.strictEqual(getPreviousFoldLine(7, foldingModel), 6); + assert.strictEqual(getPreviousFoldLine(8, foldingModel), 6); // Test jump to next. assert.strictEqual(getNextFoldLine(3, foldingModel), 5); - assert.strictEqual(getNextFoldLine(4, foldingModel), 5); assert.strictEqual(getNextFoldLine(5, foldingModel), 9); assert.strictEqual(getNextFoldLine(9, foldingModel), null); + // Test when not on a folding region start line. + assert.strictEqual(getNextFoldLine(4, foldingModel), 5); + assert.strictEqual(getNextFoldLine(7, foldingModel), 9); + assert.strictEqual(getNextFoldLine(8, foldingModel), 9); } finally { textModel.dispose(); diff --git a/src/vs/editor/contrib/folding/test/foldingRanges.test.ts b/src/vs/editor/contrib/folding/test/foldingRanges.test.ts index d36ebf3684..62cbe6f575 100644 --- a/src/vs/editor/contrib/folding/test/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/foldingRanges.test.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration'; import { MAX_FOLDING_REGIONS } from 'vs/editor/contrib/folding/foldingRanges'; +import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; let markers: FoldingMarkers = { start: /^\s*#region\b/, @@ -34,6 +34,7 @@ suite('FoldingRanges', () => { assert.strictEqual(actual.getEndLineNumber(i), nRegions * 2 - i, 'end' + i); assert.strictEqual(actual.getParentIndex(i), i - 1, 'parent' + i); } + model.dispose(); }); @@ -100,5 +101,6 @@ suite('FoldingRanges', () => { for (let i = 0; i < nRegions; i++) { assert.strictEqual(actual.isCollapsed(i), i % 3 === 0, 'line' + i); } + model.dispose(); }); }); diff --git a/src/vs/editor/contrib/folding/test/hiddenRangeModel.test.ts b/src/vs/editor/contrib/folding/test/hiddenRangeModel.test.ts index 43486a5b7e..4397ffc512 100644 --- a/src/vs/editor/contrib/folding/test/hiddenRangeModel.test.ts +++ b/src/vs/editor/contrib/folding/test/hiddenRangeModel.test.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { FoldingModel } from 'vs/editor/contrib/folding/foldingModel'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; -import { TestDecorationProvider } from './foldingModel.test'; -import { HiddenRangeModel } from 'vs/editor/contrib/folding/hiddenRangeModel'; import { IRange } from 'vs/editor/common/core/range'; +import { FoldingModel } from 'vs/editor/contrib/folding/foldingModel'; +import { HiddenRangeModel } from 'vs/editor/contrib/folding/hiddenRangeModel'; +import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { TestDecorationProvider } from './foldingModel.test'; interface ExpectedRange { @@ -91,6 +91,8 @@ suite('Hidden Range Model', () => { assert.strictEqual(hiddenRangeModel.isHidden(9), false); assert.strictEqual(hiddenRangeModel.isHidden(10), false); + textModel.dispose(); + }); diff --git a/src/vs/editor/contrib/folding/test/indentFold.test.ts b/src/vs/editor/contrib/folding/test/indentFold.test.ts index 1dbd88458c..d1ba15c0dd 100644 --- a/src/vs/editor/contrib/folding/test/indentFold.test.ts +++ b/src/vs/editor/contrib/folding/test/indentFold.test.ts @@ -70,6 +70,8 @@ suite('Indentation Folding', () => { assertLimit(2, [r1, r9], '2'); assertLimit(1, [r1], '1'); assertLimit(0, [], '0'); + + model.dispose(); }); }); diff --git a/src/vs/editor/contrib/folding/test/indentRangeProvider.test.ts b/src/vs/editor/contrib/folding/test/indentRangeProvider.test.ts index e59b71d247..d8e6bae401 100644 --- a/src/vs/editor/contrib/folding/test/indentRangeProvider.test.ts +++ b/src/vs/editor/contrib/folding/test/indentRangeProvider.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration'; +import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; interface ExpectedIndentRange { startLineNumber: number; diff --git a/src/vs/editor/contrib/folding/test/syntaxFold.test.ts b/src/vs/editor/contrib/folding/test/syntaxFold.test.ts index ffd09ca5e2..06b657d621 100644 --- a/src/vs/editor/contrib/folding/test/syntaxFold.test.ts +++ b/src/vs/editor/contrib/folding/test/syntaxFold.test.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { SyntaxRangeProvider } from 'vs/editor/contrib/folding/syntaxRangeProvider'; -import { FoldingRangeProvider, FoldingRange, FoldingContext, ProviderResult } from 'vs/editor/common/modes'; -import { ITextModel } from 'vs/editor/common/model'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ITextModel } from 'vs/editor/common/model'; +import { FoldingContext, FoldingRange, FoldingRangeProvider, ProviderResult } from 'vs/editor/common/modes'; +import { SyntaxRangeProvider } from 'vs/editor/contrib/folding/syntaxRangeProvider'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; interface IndentRange { start: number; @@ -95,6 +95,8 @@ suite('Syntax folding', () => { await assertLimit(2, [r1, r9], '2'); await assertLimit(1, [r1], '1'); await assertLimit(0, [], '0'); + + model.dispose(); }); }); diff --git a/src/vs/editor/contrib/fontZoom/fontZoom.ts b/src/vs/editor/contrib/fontZoom/fontZoom.ts index bee8d01a4d..b6c94a3537 100644 --- a/src/vs/editor/contrib/fontZoom/fontZoom.ts +++ b/src/vs/editor/contrib/fontZoom/fontZoom.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 { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; +import * as nls from 'vs/nls'; class EditorFontZoomIn extends EditorAction { diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 49e0d65b0a..ed716407d1 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -6,7 +6,11 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { asArray, isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { Iterable } from 'vs/base/common/iterator'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; +import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource, TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState'; import { IActiveCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -18,17 +22,13 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { ISingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { DocumentFormattingEditProvider, DocumentFormattingEditProviderRegistry, DocumentRangeFormattingEditProvider, DocumentRangeFormattingEditProviderRegistry, FormattingOptions, OnTypeFormattingEditProviderRegistry, TextEdit } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { IModelService } from 'vs/editor/common/services/modelService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit'; import * as nls from 'vs/nls'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { LinkedList } from 'vs/base/common/linkedList'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { assertType } from 'vs/base/common/types'; import { IProgress } from 'vs/platform/progress/common/progress'; -import { Iterable } from 'vs/base/common/iterator'; export function alertFormattingEdits(edits: ISingleEditOperation[]): void { @@ -425,40 +425,47 @@ export function getOnTypeFormattingEdits( }); } -CommandsRegistry.registerCommand('_executeFormatRangeProvider', function (accessor, ...args) { +CommandsRegistry.registerCommand('_executeFormatRangeProvider', async function (accessor, ...args) { const [resource, range, options] = args; assertType(URI.isUri(resource)); assertType(Range.isIRange(range)); - const model = accessor.get(IModelService).getModel(resource); - if (!model) { - throw illegalArgument('resource'); + const resolverService = accessor.get(ITextModelService); + const workerService = accessor.get(IEditorWorkerService); + const reference = await resolverService.createModelReference(resource); + try { + return getDocumentRangeFormattingEditsUntilResult(workerService, reference.object.textEditorModel, Range.lift(range), options, CancellationToken.None); + } finally { + reference.dispose(); } - return getDocumentRangeFormattingEditsUntilResult(accessor.get(IEditorWorkerService), model, Range.lift(range), options, CancellationToken.None); }); -CommandsRegistry.registerCommand('_executeFormatDocumentProvider', function (accessor, ...args) { +CommandsRegistry.registerCommand('_executeFormatDocumentProvider', async function (accessor, ...args) { const [resource, options] = args; assertType(URI.isUri(resource)); - const model = accessor.get(IModelService).getModel(resource); - if (!model) { - throw illegalArgument('resource'); + const resolverService = accessor.get(ITextModelService); + const workerService = accessor.get(IEditorWorkerService); + const reference = await resolverService.createModelReference(resource); + try { + return getDocumentFormattingEditsUntilResult(workerService, reference.object.textEditorModel, options, CancellationToken.None); + } finally { + reference.dispose(); } - - return getDocumentFormattingEditsUntilResult(accessor.get(IEditorWorkerService), model, options, CancellationToken.None); }); -CommandsRegistry.registerCommand('_executeFormatOnTypeProvider', function (accessor, ...args) { +CommandsRegistry.registerCommand('_executeFormatOnTypeProvider', async function (accessor, ...args) { const [resource, position, ch, options] = args; assertType(URI.isUri(resource)); assertType(Position.isIPosition(position)); assertType(typeof ch === 'string'); - const model = accessor.get(IModelService).getModel(resource); - if (!model) { - throw illegalArgument('resource'); + const resolverService = accessor.get(ITextModelService); + const workerService = accessor.get(IEditorWorkerService); + const reference = await resolverService.createModelReference(resource); + try { + return getOnTypeFormattingEdits(workerService, reference.object.textEditorModel, Position.lift(position), ch, options); + } finally { + reference.dispose(); } - - return getOnTypeFormattingEdits(accessor.get(IEditorWorkerService), model, Position.lift(position), ch, options); }); diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 372a7a6f27..4399be19a8 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -5,27 +5,27 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { alertFormattingEdits, formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode, getOnTypeFormattingEdits } from 'vs/editor/contrib/format/format'; import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit'; import * as nls from 'vs/nls'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Progress, IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IEditorProgressService, Progress } from 'vs/platform/progress/common/progress'; class FormatOnType implements IEditorContribution { @@ -92,7 +92,7 @@ class FormatOnType implements IEditorContribution { return; } - if (this._editor.getSelections().length > 1) { + if (this._editor.getSelections().length > 1 || !this._editor.getSelection().isEmpty()) { return; } @@ -216,8 +216,8 @@ class FormatDocumentAction extends EditorAction { precondition: ContextKeyExpr.and(EditorContextKeys.notInCompositeEditor, EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I }, + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI }, weight: KeybindingWeight.EditorContrib }, contextMenuOpts: { @@ -249,7 +249,7 @@ class FormatSelectionAction extends EditorAction { precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyF), weight: KeybindingWeight.EditorContrib }, contextMenuOpts: { diff --git a/src/vs/editor/contrib/gotoError/gotoError.ts b/src/vs/editor/contrib/gotoError/gotoError.ts index 0e3f814901..69de022578 100644 --- a/src/vs/editor/contrib/gotoError/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/gotoError.ts @@ -3,27 +3,27 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IMarker } from 'vs/platform/markers/common/markers'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, IActionOptions, registerEditorAction, registerEditorCommand, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, IActionOptions, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { MarkerNavigationWidget } from './gotoErrorWidget'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; -import { Codicon } from 'vs/base/common/codicons'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMarkerNavigationService, MarkerList } from 'vs/editor/contrib/gotoError/markerNavigationService'; +import * as nls from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IMarker } from 'vs/platform/markers/common/markers'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { MarkerNavigationWidget } from './gotoErrorWidget'; export class MarkerController implements IEditorContribution { @@ -200,7 +200,7 @@ export class NextMarkerAction extends MarkerNavigationAction { menuOpts: { menuId: MarkerNavigationWidget.TitleMenu, title: NextMarkerAction.LABEL, - icon: registerIcon('marker-navigation-next', Codicon.chevronDown, nls.localize('nextMarkerIcon', 'Icon for goto next marker.')), + icon: registerIcon('marker-navigation-next', Codicon.arrowDown, nls.localize('nextMarkerIcon', 'Icon for goto next marker.')), group: 'navigation', order: 1 } @@ -225,7 +225,7 @@ class PrevMarkerAction extends MarkerNavigationAction { menuOpts: { menuId: MarkerNavigationWidget.TitleMenu, title: NextMarkerAction.LABEL, - icon: registerIcon('marker-navigation-previous', Codicon.chevronUp, nls.localize('previousMarkerIcon', 'Icon for goto previous marker.')), + icon: registerIcon('marker-navigation-previous', Codicon.arrowUp, nls.localize('previousMarkerIcon', 'Icon for goto previous marker.')), group: 'navigation', order: 2 } diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 868a0e8048..3c020065d6 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -3,34 +3,34 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/gotoErrorWidget'; -import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; -import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; -import { Range } from 'vs/editor/common/core/range'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { registerColor, oneOf, textLinkForeground, editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder, editorInfoForeground, editorInfoBorder, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { Color } from 'vs/base/common/color'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { ScrollType } from 'vs/editor/common/editorCommon'; -import { getBaseLabel } from 'vs/base/common/labels'; -import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { Event, Emitter } from 'vs/base/common/event'; -import { PeekViewWidget, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/peekView'; -import { basename } from 'vs/base/common/resources'; import { IAction } from 'vs/base/common/actions'; -import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { Color } from 'vs/base/common/color'; +import { Emitter, Event } from 'vs/base/common/event'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { basename } from 'vs/base/common/resources'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { splitLines } from 'vs/base/common/strings'; +import 'vs/css!./media/gotoErrorWidget'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView'; +import * as nls from 'vs/nls'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IMarker, IRelatedInformation, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; +import { contrastBorder, editorBackground, editorErrorBorder, editorErrorForeground, editorInfoBorder, editorInfoForeground, editorWarningBorder, editorWarningForeground, oneOf, registerColor, textLinkActiveForeground, textLinkForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; class MessageWidget { @@ -252,7 +252,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILabelService private readonly _labelService: ILabelService ) { - super(editor, { showArrow: true, showFrame: true, isAccessible: true }, instantiationService); + super(editor, { showArrow: true, showFrame: true, isAccessible: true, frameWidth: 1 }, instantiationService); this._severity = MarkerSeverity.Warning; this._backgroundColor = Color.white; @@ -265,16 +265,23 @@ export class MarkerNavigationWidget extends PeekViewWidget { private _applyTheme(theme: IColorTheme) { this._backgroundColor = theme.getColor(editorMarkerNavigationBackground); let colorId = editorMarkerNavigationError; + let headerBackground = editorMarkerNavigationErrorHeader; + if (this._severity === MarkerSeverity.Warning) { colorId = editorMarkerNavigationWarning; + headerBackground = editorMarkerNavigationWarningHeader; } else if (this._severity === MarkerSeverity.Info) { colorId = editorMarkerNavigationInfo; + headerBackground = editorMarkerNavigationInfoHeader; } + const frameColor = theme.getColor(colorId); + const headerBg = theme.getColor(headerBackground); + this.style({ arrowColor: frameColor, frameColor: frameColor, - headerBackgroundColor: this._backgroundColor, + headerBackgroundColor: headerBg, primaryHeadingColor: theme.getColor(peekViewTitleForeground), secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground) }); // style() will trigger _applyStyles @@ -395,10 +402,16 @@ let errorDefault = oneOf(editorErrorForeground, editorErrorBorder); let warningDefault = oneOf(editorWarningForeground, editorWarningBorder); let infoDefault = oneOf(editorInfoForeground, editorInfoBorder); -export const editorMarkerNavigationError = registerColor('editorMarkerNavigationError.background', { dark: errorDefault, light: errorDefault, hc: errorDefault }, nls.localize('editorMarkerNavigationError', 'Editor marker navigation widget error color.')); -export const editorMarkerNavigationWarning = registerColor('editorMarkerNavigationWarning.background', { dark: warningDefault, light: warningDefault, hc: warningDefault }, nls.localize('editorMarkerNavigationWarning', 'Editor marker navigation widget warning color.')); -export const editorMarkerNavigationInfo = registerColor('editorMarkerNavigationInfo.background', { dark: infoDefault, light: infoDefault, hc: infoDefault }, nls.localize('editorMarkerNavigationInfo', 'Editor marker navigation widget info color.')); -export const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: '#2D2D30', light: Color.white, hc: '#0C141F' }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); +export const editorMarkerNavigationError = registerColor('editorMarkerNavigationError.background', { dark: errorDefault, light: errorDefault, hc: contrastBorder }, nls.localize('editorMarkerNavigationError', 'Editor marker navigation widget error color.')); +export const editorMarkerNavigationErrorHeader = registerColor('editorMarkerNavigationError.headerBackground', { dark: transparent(editorMarkerNavigationError, .1), light: transparent(editorMarkerNavigationError, .1), hc: null }, nls.localize('editorMarkerNavigationErrorHeaderBackground', 'Editor marker navigation widget error heading background.')); + +export const editorMarkerNavigationWarning = registerColor('editorMarkerNavigationWarning.background', { dark: warningDefault, light: warningDefault, hc: contrastBorder }, nls.localize('editorMarkerNavigationWarning', 'Editor marker navigation widget warning color.')); +export const editorMarkerNavigationWarningHeader = registerColor('editorMarkerNavigationWarning.headerBackground', { dark: transparent(editorMarkerNavigationWarning, .1), light: transparent(editorMarkerNavigationWarning, .1), hc: '#0C141F' }, nls.localize('editorMarkerNavigationWarningBackground', 'Editor marker navigation widget warning heading background.')); + +export const editorMarkerNavigationInfo = registerColor('editorMarkerNavigationInfo.background', { dark: infoDefault, light: infoDefault, hc: contrastBorder }, nls.localize('editorMarkerNavigationInfo', 'Editor marker navigation widget info color.')); +export const editorMarkerNavigationInfoHeader = registerColor('editorMarkerNavigationInfo.headerBackground', { dark: transparent(editorMarkerNavigationInfo, .1), light: transparent(editorMarkerNavigationInfo, .1), hc: null }, nls.localize('editorMarkerNavigationInfoHeaderBackground', 'Editor marker navigation widget info heading background.')); + +export const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: editorBackground, light: editorBackground, hc: editorBackground }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); registerThemingParticipant((theme, collector) => { const linkFg = theme.getColor(textLinkForeground); diff --git a/src/vs/editor/contrib/gotoError/markerNavigationService.ts b/src/vs/editor/contrib/gotoError/markerNavigationService.ts index 53d54b2ed4..9c73b94362 100644 --- a/src/vs/editor/contrib/gotoError/markerNavigationService.ts +++ b/src/vs/editor/contrib/gotoError/markerNavigationService.ts @@ -3,18 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMarkerService, MarkerSeverity, IMarker } from 'vs/platform/markers/common/markers'; -import { URI } from 'vs/base/common/uri'; +import { binarySearch } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; +import { compare } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { compare } from 'vs/base/common/strings'; -import { binarySearch } from 'vs/base/common/arrays'; import { ITextModel } from 'vs/editor/common/model'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { LinkedList } from 'vs/base/common/linkedList'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IMarker, IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class MarkerCoordinate { constructor( @@ -38,6 +39,7 @@ export class MarkerList { constructor( resourceFilter: URI | ((uri: URI) => boolean) | undefined, @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configService: IConfigurationService, ) { if (URI.isUri(resourceFilter)) { this._resourceFilter = uri => uri.toString() === resourceFilter.toString(); @@ -45,6 +47,17 @@ export class MarkerList { this._resourceFilter = resourceFilter; } + const compareOrder = this._configService.getValue('problems.compareOrder'); + const compareMarker = (a: IMarker, b: IMarker): number => { + let res = compare(a.resource.toString(), b.resource.toString()); + if (compareOrder === 'position') { + res = Range.compareRangesUsingStarts(a, b) || MarkerSeverity.compare(a.severity, b.severity); + } else { + res = MarkerSeverity.compare(a.severity, b.severity) || Range.compareRangesUsingStarts(a, b); + } + return res; + }; + const updateMarker = () => { this._markers = this._markerService.read({ resource: URI.isUri(resourceFilter) ? resourceFilter : undefined, @@ -53,7 +66,7 @@ export class MarkerList { if (typeof resourceFilter === 'function') { this._markers = this._markers.filter(m => this._resourceFilter!(m.resource)); } - this._markers.sort(MarkerList._compareMarker); + this._markers.sort(compareMarker); }; updateMarker(); @@ -164,17 +177,6 @@ export class MarkerList { } return undefined; } - - private static _compareMarker(a: IMarker, b: IMarker): number { - let res = compare(a.resource.toString(), b.resource.toString()); - if (res === 0) { - res = MarkerSeverity.compare(a.severity, b.severity); - } - if (res === 0) { - res = Range.compareRangesUsingStarts(a, b); - } - return res; - } } export const IMarkerNavigationService = createDecorator('IMarkerNavigationService'); @@ -195,7 +197,10 @@ class MarkerNavigationService implements IMarkerNavigationService, IMarkerListPr private readonly _provider = new LinkedList(); - constructor(@IMarkerService private readonly _markerService: IMarkerService) { } + constructor( + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configService: IConfigurationService, + ) { } registerProvider(provider: IMarkerListProvider): IDisposable { const remove = this._provider.unshift(provider); @@ -210,7 +215,7 @@ class MarkerNavigationService implements IMarkerNavigationService, IMarkerListPr } } // default - return new MarkerList(resource, this._markerService); + return new MarkerList(resource, this._markerService, this._configService); } } diff --git a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts index 24ca59345c..6f1089c026 100644 --- a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts @@ -3,41 +3,41 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isStandalone } from 'vs/base/browser/browser'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { createCancelablePromise, raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { isWeb } from 'vs/base/common/platform'; -import { ICodeEditor, isCodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/browser/core/editorState'; +import { IActiveCodeEditor, ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, IActionOptions, registerInstantiatedEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EditorOption, GoToLocationValues } from 'vs/editor/common/config/editorOptions'; import * as corePosition from 'vs/editor/common/core/position'; -import { Range, IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { IEditorAction, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel, IWordAtPosition } from 'vs/editor/common/model'; -import { LocationLink, Location, isLocationLink } from 'vs/editor/common/modes'; -import { MessageController } from 'vs/editor/contrib/message/messageController'; -import { PeekContext } from 'vs/editor/contrib/peekView/peekView'; +import { isLocationLink, Location, LocationLink } from 'vs/editor/common/modes'; import { ReferencesController } from 'vs/editor/contrib/gotoSymbol/peek/referencesController'; import { ReferencesModel } from 'vs/editor/contrib/gotoSymbol/referencesModel'; +import { ISymbolNavigationService } from 'vs/editor/contrib/gotoSymbol/symbolNavigation'; +import { MessageController } from 'vs/editor/contrib/message/messageController'; +import { PeekContext } from 'vs/editor/contrib/peekView/peekView'; import * as nls from 'vs/nls'; -import { MenuId, MenuRegistry, ISubmenuItem } from 'vs/platform/actions/common/actions'; +import { ISubmenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; -import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition, getDeclarationsAtPosition, getReferencesAtPosition } from './goToSymbol'; -import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { EditorStateCancellationTokenSource, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; -import { ISymbolNavigationService } from 'vs/editor/contrib/gotoSymbol/symbolNavigation'; -import { EditorOption, GoToLocationValues } from 'vs/editor/common/config/editorOptions'; -import { isStandalone } from 'vs/base/browser/browser'; -import { URI } from 'vs/base/common/uri'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ScrollType, IEditorAction } from 'vs/editor/common/editorCommon'; -import { assertType } from 'vs/base/common/types'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { getDeclarationsAtPosition, getDefinitionsAtPosition, getImplementationsAtPosition, getReferencesAtPosition, getTypeDefinitionsAtPosition } from './goToSymbol'; MenuRegistry.appendMenuItem(MenuId.EditorContext, { @@ -262,13 +262,7 @@ registerGoToAction(class GoToDefinitionAction extends DefinitionAction { contextMenuOpts: { group: 'navigation', order: 1.1 - }, - /*menuOpts: { {{SQL CARBON EDIT}} remove entry - menuId: MenuId.MenubarGoMenu, - group: '4_symbol_nav', - order: 2, - title: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition") - }*/ + } }); CommandsRegistry.registerCommandAlias('editor.action.goToDeclaration', GoToDefinitionAction.id); } @@ -292,7 +286,7 @@ registerGoToAction(class OpenDefinitionToSideAction extends DefinitionAction { EditorContextKeys.isInWalkThroughSnippet.toNegated()), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, goToDefinitionKb), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, goToDefinitionKb), weight: KeybindingWeight.EditorContrib } }); @@ -380,12 +374,6 @@ registerGoToAction(class GoToDeclarationAction extends DeclarationAction { group: 'navigation', order: 1.3 }, - /*menuOpts: { {{SQL CARBON EDIT}} remove entry - menuId: MenuId.MenubarGoMenu, - group: '4_symbol_nav', - order: 3, - title: nls.localize({ key: 'miGotoDeclaration', comment: ['&& denotes a mnemonic'] }, "Go to &&Declaration") - },*/ }); } @@ -469,13 +457,7 @@ registerGoToAction(class GoToTypeDefinitionAction extends TypeDefinitionAction { contextMenuOpts: { group: 'navigation', order: 1.4 - }, - /*menuOpts: { {{SQL CARBON EDIT}} remove entry - menuId: MenuId.MenubarGoMenu, - group: '4_symbol_nav', - order: 3, - title: nls.localize({ key: 'miGotoTypeDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Type Definition") - }*/ + } }); } }); @@ -553,12 +535,6 @@ registerGoToAction(class GoToImplementationAction extends ImplementationAction { primary: KeyMod.CtrlCmd | KeyCode.F12, weight: KeybindingWeight.EditorContrib }, - /*menuOpts: { {{SQL CARBON EDIT}} remove entry - menuId: MenuId.MenubarGoMenu, - group: '4_symbol_nav', - order: 4, - title: nls.localize({ key: 'miGotoImplementation', comment: ['&& denotes a mnemonic'] }, "Go to &&Implementations") - },*/ contextMenuOpts: { group: 'navigation', order: 1.45 @@ -644,13 +620,7 @@ registerGoToAction(class GoToReferencesAction extends ReferencesAction { contextMenuOpts: { group: 'navigation', order: 1.45 - }, - /*menuOpts: { {{SQL CARBON EDIT}} remove entry - menuId: MenuId.MenubarGoMenu, - group: '4_symbol_nav', - order: 5, - title: nls.localize({ key: 'miGotoReference', comment: ['&& denotes a mnemonic'] }, "Go to &&References") - },*/ + } }); } @@ -702,8 +672,8 @@ class GenericGoToLocationAction extends SymbolNavigationAction { ) { super(config, { id: 'editor.action.goToLocation', - label: nls.localize('label.generic', "Go To Any Symbol"), - alias: 'Go To Any Symbol', + label: nls.localize('label.generic', "Go to Any Symbol"), + alias: 'Go to Any Symbol', precondition: ContextKeyExpr.and( PeekContext.notInPeekEditor, EditorContextKeys.isInWalkThroughSnippet.toNegated() @@ -818,3 +788,64 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommandAlias('editor.action.showReferences', 'editor.action.peekLocations'); //#endregion + +// -- unconditionally register goto-action + +MenuRegistry.appendMenuItems([ + { + id: MenuId.MenubarGoMenu, + item: { + command: { + id: 'editor.action.revealDefinition', + title: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition") + }, + group: '4_symbol_nav', + order: 2, + }, + }, + { + id: MenuId.MenubarGoMenu, + item: { + command: { + id: 'editor.action.revealDeclaration', + title: nls.localize({ key: 'miGotoDeclaration', comment: ['&& denotes a mnemonic'] }, "Go to &&Declaration") + }, + group: '4_symbol_nav', + order: 3, + + }, + }, + { + id: MenuId.MenubarGoMenu, + item: { + command: { + id: 'editor.action.goToTypeDefinition', + title: nls.localize({ key: 'miGotoTypeDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Type Definition") + }, + group: '4_symbol_nav', + order: 3, + }, + }, + { + id: MenuId.MenubarGoMenu, + item: { + command: { + id: 'editor.action.goToImplementation', + title: nls.localize({ key: 'miGotoImplementation', comment: ['&& denotes a mnemonic'] }, "Go to &&Implementations") + }, + group: '4_symbol_nav', + order: 4, + }, + }, + { + id: MenuId.MenubarGoMenu, + item: { + command: { + id: 'editor.action.goToReferences', + title: nls.localize({ key: 'miGotoReference', comment: ['&& denotes a mnemonic'] }, "Go to &&References") + }, + group: '4_symbol_nav', + order: 5, + }, + }, +]); diff --git a/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts b/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts index 6f90b00272..26bad4e20d 100644 --- a/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts +++ b/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts @@ -8,7 +8,7 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { registerModelAndPositionCommand } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; -import { LocationLink, DefinitionProviderRegistry, ImplementationProviderRegistry, TypeDefinitionProviderRegistry, DeclarationProviderRegistry, ProviderResult, ReferenceProviderRegistry } from 'vs/editor/common/modes'; +import { DeclarationProviderRegistry, DefinitionProviderRegistry, ImplementationProviderRegistry, LocationLink, ProviderResult, ReferenceProviderRegistry, TypeDefinitionProviderRegistry } from 'vs/editor/common/modes'; import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry'; import { ReferencesModel } from 'vs/editor/contrib/gotoSymbol/referencesModel'; diff --git a/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts b/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts index 3ce36d2f3b..8f7061e34d 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ICodeEditor, IEditorMouseEvent, IMouseTarget } from 'vs/editor/browser/editorBrowser'; +import { Emitter, Event } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; -import { Event, Emitter } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; +import { ICodeEditor, IEditorMouseEvent, IMouseTarget } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; function hasModifier(e: { ctrlKey: boolean; shiftKey: boolean; altKey: boolean; metaKey: boolean }, modifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'): boolean { return !!e[modifier]; diff --git a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts index 34acfc8c6f..fcae4fe99d 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts @@ -3,34 +3,35 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./goToDefinitionAtPosition'; -import * as nls from 'vs/nls'; -import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { DefinitionProviderRegistry, LocationLink } from 'vs/editor/common/modes'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import 'vs/css!./goToDefinitionAtPosition'; +import { CodeEditorStateFlag, EditorState } from 'vs/editor/browser/core/editorState'; import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { getDefinitionsAtPosition } from '../goToSymbol'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { EditorState, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; -import { DefinitionAction } from '../goToCommands'; -import { ClickLinkGesture, ClickLinkMouseEvent, ClickLinkKeyboardEvent } from 'vs/editor/contrib/gotoSymbol/link/clickLinkGesture'; -import { IWordAtPosition, IModelDeltaDecoration, ITextModel, IFoundBracket } from 'vs/editor/common/model'; -import { Position } from 'vs/editor/common/core/position'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel, IWordAtPosition } from 'vs/editor/common/model'; +import { IFoundBracket } from 'vs/editor/common/model/bracketPairs/bracketPairs'; +import { DefinitionProviderRegistry, LocationLink } from 'vs/editor/common/modes'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ClickLinkGesture, ClickLinkKeyboardEvent, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/link/clickLinkGesture'; import { PeekContext } from 'vs/editor/contrib/peekView/peekView'; +import * as nls from 'vs/nls'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { DefinitionAction } from '../goToCommands'; +import { getDefinitionsAtPosition } from '../goToSymbol'; export class GotoDefinitionAtPositionEditorContribution implements IEditorContribution { @@ -203,10 +204,10 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri wordRange = new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn); } - const modeId = this.modeService.getModeIdByFilepathOrFirstLine(textEditorModel.uri); + const languageId = this.modeService.getModeIdByFilepathOrFirstLine(textEditorModel.uri); this.addDecoration( wordRange, - new MarkdownString().appendCodeblock(modeId ? modeId : '', previewValue) + new MarkdownString().appendCodeblock(languageId ? languageId : '', previewValue) ); ref.dispose(); }); @@ -260,7 +261,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri const brackets: IFoundBracket[] = []; let ignoreFirstEmpty = true; - let currentBracket = textEditorModel.findNextBracket(new Position(startLineNumber, 1)); + let currentBracket = textEditorModel.bracketPairs.findNextBracket(new Position(startLineNumber, 1)); while (currentBracket !== null) { if (brackets.length === 0) { @@ -294,7 +295,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri return new Range(startLineNumber, 1, maxLineNumber + 1, 1); } - currentBracket = textEditorModel.findNextBracket(new Position(nextLineNumber, nextColumn)); + currentBracket = textEditorModel.bracketPairs.findNextBracket(new Position(nextLineNumber, nextColumn)); } return new Range(startLineNumber, 1, maxLineNumber + 1, 1); diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts index 93ad2ce1c9..bfdb5b7d30 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts @@ -3,29 +3,29 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IContextKey, IContextKeyService, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ReferencesModel, OneReference } from '../referencesModel'; -import { ReferenceWidget, LayoutData } from './referencesWidget'; -import { Range } from 'vs/editor/common/core/range'; -import { Position } from 'vs/editor/common/core/position'; -import { Location } from 'vs/editor/common/modes'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; -import { getOuterEditor, PeekContext } from 'vs/editor/contrib/peekView/peekView'; -import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; 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 { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { Location } from 'vs/editor/common/modes'; +import { getOuterEditor, PeekContext } from 'vs/editor/contrib/peekView/peekView'; +import * as nls from 'vs/nls'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { OneReference, ReferencesModel } from '../referencesModel'; +import { LayoutData, ReferenceWidget } from './referencesWidget'; export const ctxReferenceSearchVisible = new RawContextKey('referenceSearchVisible', false, nls.localize('referenceSearchVisible', "Whether reference peek is visible, like 'Peek References' or 'Peek Definition'")); @@ -311,7 +311,7 @@ function withController(accessor: ServicesAccessor, fn: (controller: ReferencesC KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'togglePeekWidgetFocus', weight: KeybindingWeight.EditorContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.F2), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.F2), when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor), handler(accessor) { withController(accessor, controller => { diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesTree.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesTree.ts index c814099e0b..6a574172c5 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesTree.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesTree.ts @@ -3,26 +3,26 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ReferencesModel, FileReferences, OneReference } from '../referencesModel'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import * as dom from 'vs/base/browser/dom'; -import { localize } from 'vs/nls'; -import { getBaseLabel } from 'vs/base/common/labels'; -import { dirname, basename } from 'vs/base/common/resources'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { createMatches, FuzzyScore, IMatch } from 'vs/base/common/filters'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { basename, dirname } from 'vs/base/common/resources'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { FileReferences, OneReference, ReferencesModel } from '../referencesModel'; //#region data source diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 666f411418..51289c0c04 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -3,15 +3,18 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./referencesWidget'; import * as dom from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; +import { Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; -import { dispose, IDisposable, IReference, DisposableStore } from 'vs/base/common/lifecycle'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; +import 'vs/css!./referencesWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -20,21 +23,20 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/modes'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, OneReferenceRenderer, TreeElement, StringRepresentationProvider, IdentityProvider } from 'vs/editor/contrib/gotoSymbol/peek/referencesTree'; +import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, IdentityProvider, OneReferenceRenderer, StringRepresentationProvider, TreeElement } from 'vs/editor/contrib/gotoSymbol/peek/referencesTree'; +import * as peekView from 'vs/editor/contrib/peekView/peekView'; import * as nls from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; -import { WorkbenchAsyncDataTree, IWorkbenchAsyncDataTreeOptions } from 'vs/platform/list/browser/listService'; +import { IWorkbenchAsyncDataTreeOptions, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import * as peekView from 'vs/editor/contrib/peekView/peekView'; -import { FileReferences, OneReference, ReferencesModel } from '../referencesModel'; -import { FuzzyScore } from 'vs/base/common/filters'; -import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { KeyCode } from 'vs/base/common/keyCodes'; +import { FileReferences, OneReference, ReferencesModel } from '../referencesModel'; class DecorationsManager implements IDisposable { @@ -223,8 +225,10 @@ export class ReferenceWidget extends peekView.PeekViewWidget { @ILabelService private readonly _uriLabel: ILabelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IModeService private readonly _modeService: IModeService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { - super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }, _instantiationService); + super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, supportOnTitleClick: true }, _instantiationService); this._applyTheme(themeService.getColorTheme()); this._callOnDispose.add(themeService.onDidColorThemeChange(this._applyTheme.bind(this))); @@ -311,7 +315,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { }; this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, this.editor); dom.hide(this._previewContainer); - this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), TextModel.DEFAULT_CREATION_OPTIONS, null, null, this._undoRedoService); + this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), TextModel.DEFAULT_CREATION_OPTIONS, null, null, this._undoRedoService, this._modeService, this._languageConfigurationService); // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); diff --git a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts index 66420fdf49..f4ef01480b 100644 --- a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts +++ b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts @@ -3,21 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { Event, Emitter } from 'vs/base/common/event'; -import { basename, extUri } from 'vs/base/common/resources'; -import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; -import * as strings from 'vs/base/common/strings'; -import { URI } from 'vs/base/common/uri'; -import { defaultGenerator } from 'vs/base/common/idGenerator'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { Location, LocationLink } from 'vs/editor/common/modes'; -import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; -import { Position } from 'vs/editor/common/core/position'; -import { IMatch } from 'vs/base/common/filters'; -import { Constants } from 'vs/base/common/uint'; -import { ResourceMap } from 'vs/base/common/map'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IMatch } from 'vs/base/common/filters'; +import { defaultGenerator } from 'vs/base/common/idGenerator'; +import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { basename, extUri } from 'vs/base/common/resources'; +import * as strings from 'vs/base/common/strings'; +import { Constants } from 'vs/base/common/uint'; +import { URI } from 'vs/base/common/uri'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { Location, LocationLink } from 'vs/editor/common/modes'; +import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; export class OneReference { diff --git a/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts b/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts index 51afb93145..8dbaaf2f6e 100644 --- a/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts +++ b/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts @@ -3,23 +3,23 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ReferencesModel, OneReference } from 'vs/editor/contrib/gotoSymbol/referencesModel'; -import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { registerEditorCommand, EditorCommand } from 'vs/editor/browser/editorExtensions'; +import { combinedDisposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; -import { dispose, IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Emitter, Event } from 'vs/base/common/event'; +import { OneReference, ReferencesModel } from 'vs/editor/contrib/gotoSymbol/referencesModel'; import { localize } from 'vs/nls'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { isEqual } from 'vs/base/common/resources'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export const ctxHasSymbols = new RawContextKey('hasSymbols', false, localize('hasSymbols', "Whether there are symbol locations that can be navigated via keyboard-only.")); diff --git a/src/vs/editor/contrib/gotoSymbol/test/referencesModel.test.ts b/src/vs/editor/contrib/gotoSymbol/test/referencesModel.test.ts index 40aba4044a..3c6f9a566a 100644 --- a/src/vs/editor/contrib/gotoSymbol/test/referencesModel.test.ts +++ b/src/vs/editor/contrib/gotoSymbol/test/referencesModel.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; import { ReferencesModel } from 'vs/editor/contrib/gotoSymbol/referencesModel'; suite('references', function () { diff --git a/src/vs/editor/contrib/hover/colorHoverParticipant.ts b/src/vs/editor/contrib/hover/colorHoverParticipant.ts index b53c3ccd76..0134838e05 100644 --- a/src/vs/editor/contrib/hover/colorHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/colorHoverParticipant.ts @@ -3,19 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Color, RGBA } from 'vs/base/common/color'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; 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 { DocumentColorProvider, IColorInformation } from 'vs/editor/common/modes'; 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 { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; import { IThemeService } from 'vs/platform/theme/common/themeService'; export class ColorHover implements IHoverPart { @@ -60,11 +60,16 @@ export class ColorHoverParticipant implements IEditorHoverParticipant { - if (e.hasChanged(EditorOption.fontInfo)) { - this.updateFont(); - } - })); - - this._editor.addOverlayWidget(this); - } - - protected get isVisible(): boolean { - return this._isVisible; - } - - protected set isVisible(value: boolean) { - this._isVisible = value; - this._domNode.classList.toggle('hidden', !this._isVisible); - } - - public getId(): string { - return this._id; - } - - public getDomNode(): HTMLElement { - return this._domNode; - } - - public showAt(lineNumber: number): void { - this._showAtLineNumber = lineNumber; - - if (!this.isVisible) { - this.isVisible = true; - } - - const editorLayout = this._editor.getLayoutInfo(); - const topForLineNumber = this._editor.getTopForLineNumber(this._showAtLineNumber); - const editorScrollTop = this._editor.getScrollTop(); - const lineHeight = this._editor.getOption(EditorOption.lineHeight); - const nodeHeight = this._domNode.clientHeight; - const top = topForLineNumber - editorScrollTop - ((nodeHeight - lineHeight) / 2); - - this._domNode.style.left = `${editorLayout.glyphMarginLeft + editorLayout.glyphMarginWidth}px`; - this._domNode.style.top = `${Math.max(Math.round(top), 0)}px`; - } - - public hide(): void { - if (!this.isVisible) { - return; - } - this.isVisible = false; - } - - public getPosition(): IOverlayWidgetPosition | null { - return null; - } - - public override dispose(): void { - this._editor.removeOverlayWidget(this); - super.dispose(); - } - - private updateFont(): void { - const codeTags: HTMLElement[] = Array.prototype.slice.call(this._domNode.getElementsByTagName('code')); - const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._domNode.getElementsByClassName('code')); - - [...codeTags, ...codeClasses].forEach(node => this._editor.applyFontInfo(node)); - } - - protected updateContents(node: Node): void { - this._domNode.textContent = ''; - this._domNode.appendChild(node); - this.updateFont(); - } -} diff --git a/src/vs/editor/contrib/hover/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts index a87b20fff6..0e6d5e46c6 100644 --- a/src/vs/editor/contrib/hover/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts @@ -3,23 +3,23 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { IMarkdownString, MarkdownString, isEmptyMarkdownString } from 'vs/base/common/htmlContent'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Range } from 'vs/editor/common/core/range'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { asArray } from 'vs/base/common/arrays'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IModelDecoration } from 'vs/editor/common/model'; -import { 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 { IMarkdownString, isEmptyMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { HoverProviderRegistry } from 'vs/editor/common/modes'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { getHover } from 'vs/editor/contrib/hover/getHover'; +import { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; +import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const $ = dom.$; @@ -76,10 +76,13 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant('editor.maxTokenizationLineLength', { + overrideIdentifier: languageId + }); if (typeof maxTokenizationLineLength === 'number' && lineLength >= maxTokenizationLineLength) { - result.push(new MarkdownHover(this, new Range(lineNumber, 1, lineNumber, lineLength + 1), [{ + result.push(new MarkdownHover(this, anchor.range, [{ value: nls.localize('too many characters', "Tokenization is skipped for long lines for performance reasons. This can be configured via `editor.maxTokenizationLineLength`.") }])); } diff --git a/src/vs/editor/contrib/hover/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/markerHoverParticipant.ts index ad9054c7f1..ad5c99e7f2 100644 --- a/src/vs/editor/contrib/hover/markerHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/markerHoverParticipant.ts @@ -3,30 +3,30 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Range } from 'vs/editor/common/core/range'; -import { CodeActionTriggerType } from 'vs/editor/common/modes'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { basename } from 'vs/base/common/resources'; -import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; -import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { basename } from 'vs/base/common/resources'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { CodeActionTriggerType } from 'vs/editor/common/modes'; +import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; +import { CodeActionSet, getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; -import { IModelDecoration } from 'vs/editor/common/model'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Progress } from 'vs/platform/progress/common/progress'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; import { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import * as nls from 'vs/nls'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Progress } from 'vs/platform/progress/common/progress'; import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; const $ = dom.$; @@ -231,7 +231,7 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant, @IInstantiationService instantiationService: IInstantiationService, @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, ) { super(); @@ -216,13 +218,14 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I instantiationService.createInstance(MarkerHoverParticipant, editor, this), ]; - this._hover = this._register(new HoverWidget()); - this._id = ModesContentHoverWidget.ID; this._editor = editor; this._isVisible = false; this._stoleFocus = false; this._renderDisposable = null; + this._hover = this._register(new HoverWidget()); + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + this.onkeydown(this._hover.containerDomNode, (e: IKeyboardEvent) => { if (e.equals(KeyCode.Escape)) { this.hide(); @@ -250,6 +253,7 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I this._isChangingDecorations = false; this._shouldFocus = false; this._colorPicker = null; + this._preferAbove = this._editor.getOption(EditorOption.hover).above; this._hoverOperation = new HoverOperation( this._computer, @@ -269,6 +273,7 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I })); this._register(editor.onDidChangeConfiguration(() => { this._hoverOperation.setHoverTime(this._editor.getOption(EditorOption.hover).delay); + this._preferAbove = this._editor.getOption(EditorOption.hover).above; })); this._register(TokenizationRegistry.onDidChange(() => { if (this._isVisible && this._lastAnchor && this._messages.length > 0) { @@ -285,7 +290,7 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I } public getId(): string { - return this._id; + return ModesContentHoverWidget.ID; } public getDomNode(): HTMLElement { @@ -365,13 +370,21 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I public getPosition(): IContentWidgetPosition | null { if (this._isVisible) { + let preferAbove = this._preferAbove; + if (!preferAbove && this._contextKeyService.getContextKeyValue(SuggestContext.Visible.key)) { + // Prefer rendering above if the suggest widget is visible + preferAbove = true; + } return { position: this._showAtPosition, range: this._showAtRange, - preference: [ + preference: preferAbove ? [ ContentWidgetPositionPreference.ABOVE, - ContentWidgetPositionPreference.BELOW - ] + ContentWidgetPositionPreference.BELOW, + ] : [ + ContentWidgetPositionPreference.BELOW, + ContentWidgetPositionPreference.ABOVE, + ], }; } return null; @@ -396,7 +409,7 @@ export class ModesContentHoverWidget extends Widget implements IContentWidget, I const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); this._hover.contentsDomNode.style.fontSize = `${fontSize}px`; - this._hover.contentsDomNode.style.lineHeight = `${lineHeight}px`; + this._hover.contentsDomNode.style.lineHeight = `${lineHeight / fontSize}`; this._hover.contentsDomNode.style.maxHeight = `${height}px`; this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; } diff --git a/src/vs/editor/contrib/hover/modesGlyphHover.ts b/src/vs/editor/contrib/hover/modesGlyphHover.ts index a0df89ba53..3c05c3f662 100644 --- a/src/vs/editor/contrib/hover/modesGlyphHover.ts +++ b/src/vs/editor/contrib/hover/modesGlyphHover.ts @@ -3,16 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $ } from 'vs/base/browser/dom'; +import * as dom from 'vs/base/browser/dom'; +import { asArray } from 'vs/base/common/arrays'; import { IMarkdownString, isEmptyMarkdownString } from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation'; -import { GlyphHoverWidget } from 'vs/editor/contrib/hover/hoverWidgets'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation'; +import { Widget } from 'vs/base/browser/ui/widget'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; -import { asArray } from 'vs/base/common/arrays'; +import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; + +const $ = dom.$; export interface IHoverMessage { value: IMarkdownString; @@ -83,9 +87,14 @@ class MarginComputer implements IHoverComputer { } } -export class ModesGlyphHoverWidget extends GlyphHoverWidget { +export class ModesGlyphHoverWidget extends Widget implements IOverlayWidget { public static readonly ID = 'editor.contrib.modesGlyphHoverWidget'; + + private readonly _editor: ICodeEditor; + private readonly _hover: HoverWidget; + + private _isVisible: boolean; private _messages: IHoverMessage[]; private _lastLineNumber: number; @@ -99,14 +108,18 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { modeService: IModeService, openerService: IOpenerService = NullOpenerService, ) { - super(ModesGlyphHoverWidget.ID, editor); + super(); + this._editor = editor; + this._isVisible = false; this._messages = []; this._lastLineNumber = -1; + this._hover = this._register(new HoverWidget()); + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + this._markdownRenderer = this._register(new MarkdownRenderer({ editor: this._editor }, modeService, openerService)); this._computer = new MarginComputer(this._editor); - this._hoverOperation = new HoverOperation( this._computer, (result: IHoverMessage[]) => this._withResult(result), @@ -115,15 +128,63 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { 300 ); + this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (e.hasChanged(EditorOption.fontInfo)) { + this._updateFont(); + } + })); + + this._editor.addOverlayWidget(this); } public override dispose(): void { this._hoverOperation.cancel(); + this._editor.removeOverlayWidget(this); super.dispose(); } + public getId(): string { + return ModesGlyphHoverWidget.ID; + } + + public getDomNode(): HTMLElement { + return this._hover.containerDomNode; + } + + public getPosition(): IOverlayWidgetPosition | null { + return null; + } + + private _showAt(lineNumber: number): void { + if (!this._isVisible) { + this._isVisible = true; + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + } + + const editorLayout = this._editor.getLayoutInfo(); + const topForLineNumber = this._editor.getTopForLineNumber(lineNumber); + const editorScrollTop = this._editor.getScrollTop(); + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const nodeHeight = this._hover.containerDomNode.clientHeight; + const top = topForLineNumber - editorScrollTop - ((nodeHeight - lineHeight) / 2); + + this._hover.containerDomNode.style.left = `${editorLayout.glyphMarginLeft + editorLayout.glyphMarginWidth}px`; + this._hover.containerDomNode.style.top = `${Math.max(Math.round(top), 0)}px`; + } + + private _updateFont(): void { + const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); + codeClasses.forEach(node => this._editor.applyFontInfo(node)); + } + + private _updateContents(node: Node): void { + this._hover.contentsDomNode.textContent = ''; + this._hover.contentsDomNode.appendChild(node); + this._updateFont(); + } + public onModelDecorationsChanged(): void { - if (this.isVisible) { + if (this._isVisible) { // The decorations have changed and the hover is visible, // we need to recompute the displayed text this._hoverOperation.cancel(); @@ -147,13 +208,17 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { this._hoverOperation.start(HoverStartMode.Delayed); } - public override hide(): void { + public hide(): void { this._lastLineNumber = -1; this._hoverOperation.cancel(); - super.hide(); + if (!this._isVisible) { + return; + } + this._isVisible = false; + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); } - public _withResult(result: IHoverMessage[]): void { + private _withResult(result: IHoverMessage[]): void { this._messages = result; if (this._messages.length > 0) { @@ -169,12 +234,14 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { const fragment = document.createDocumentFragment(); for (const msg of messages) { - const renderedContents = this._markdownRenderer.render(msg.value); - this._renderDisposeables.add(renderedContents); - fragment.appendChild($('div.hover-row', undefined, renderedContents.element)); + const markdownHoverElement = $('div.hover-row.markdown-hover'); + const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); + const renderedContents = this._renderDisposeables.add(this._markdownRenderer.render(msg.value)); + hoverContentsElement.appendChild(renderedContents.element); + fragment.appendChild(markdownHoverElement); } - this.updateContents(fragment); - this.showAt(lineNumber); + this._updateContents(fragment); + this._showAt(lineNumber); } } diff --git a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts index da45d07a7f..606387176e 100644 --- a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts +++ b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplace.ts @@ -3,24 +3,24 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { CodeEditorStateFlag, EditorState } from 'vs/editor/browser/core/editorState'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { registerEditorAction, ServicesAccessor, EditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IInplaceReplaceSupportResult } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { InPlaceReplaceCommand } from './inPlaceReplaceCommand'; -import { EditorState, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorBracketMatchBorder } from 'vs/editor/common/view/editorColorRegistry'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import * as nls from 'vs/nls'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { InPlaceReplaceCommand } from './inPlaceReplaceCommand'; class InPlaceReplaceController implements IEditorContribution { @@ -140,7 +140,7 @@ class InPlaceReplaceUp extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_COMMA, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Comma, weight: KeybindingWeight.EditorContrib } }); @@ -165,7 +165,7 @@ class InPlaceReplaceDown extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period, weight: KeybindingWeight.EditorContrib } }); diff --git a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplaceCommand.ts b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplaceCommand.ts index 6a46686da6..a1d91ca8fc 100644 --- a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplaceCommand.ts +++ b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplaceCommand.ts @@ -3,9 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Selection } from 'vs/editor/common/core/selection'; -import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; export class InPlaceReplaceCommand implements ICommand { diff --git a/src/vs/editor/contrib/indentation/indentation.ts b/src/vs/editor/contrib/indentation/indentation.ts index 3295f72acc..4b54ce4f21 100644 --- a/src/vs/editor/contrib/indentation/indentation.ts +++ b/src/vs/editor/contrib/indentation/indentation.ts @@ -3,26 +3,26 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { DisposableStore } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, IActionOptions, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; +import { EditorAutoIndentStrategy, EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { Range, IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder, IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { IIdentifiedSingleEditOperation, ITextModel, EndOfLineSequence } from 'vs/editor/common/model'; +import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { StandardTokenType, TextEdit } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/modes/supports/indentRules'; import { IModelService } from 'vs/editor/common/services/modelService'; import * as indentUtils from 'vs/editor/contrib/indentation/indentUtils'; +import * as nls from 'vs/nls'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { EditorOption, EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; export function getReindentEditOperations(model: ITextModel, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): IIdentifiedSingleEditOperation[] { if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { @@ -30,7 +30,7 @@ export function getReindentEditOperations(model: ITextModel, startLineNumber: nu return []; } - let indentationRules = LanguageConfigurationRegistry.getIndentationRules(model.getLanguageIdentifier().id); + const indentationRules = LanguageConfigurationRegistry.getIndentationRules(model.getLanguageId()); if (!indentationRules) { return []; } @@ -219,7 +219,7 @@ export class ChangeIndentationSizeAction extends EditorAction { return; } - let creationOpts = modelService.getCreationOptions(model.getLanguageIdentifier().language, model.uri, model.isForSimpleWidget); + const creationOpts = modelService.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget); const picks = [1, 2, 3, 4, 5, 6, 7, 8].map(n => ({ id: n.toString(), label: n.toString(), @@ -294,7 +294,7 @@ export class DetectIndentation extends EditorAction { return; } - let creationOpts = modelService.getCreationOptions(model.getLanguageIdentifier().language, model.uri, model.isForSimpleWidget); + const creationOpts = modelService.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget); model.detectIndentation(creationOpts.insertSpaces, creationOpts.tabSize); } } @@ -499,7 +499,7 @@ export class AutoIndentOnPaste implements IEditorContribution { let firstLineText = model.getLineContent(startLineNumber); if (!/\S/.test(firstLineText.substring(0, range.startColumn - 1))) { - let indentOfFirstLine = LanguageConfigurationRegistry.getGoodIndentForLine(autoIndent, model, model.getLanguageIdentifier().id, startLineNumber, indentConverter); + const indentOfFirstLine = LanguageConfigurationRegistry.getGoodIndentForLine(autoIndent, model, model.getLanguageId(), startLineNumber, indentConverter); if (indentOfFirstLine !== null) { let oldIndentation = strings.getLeadingWhitespace(firstLineText); @@ -543,8 +543,8 @@ export class AutoIndentOnPaste implements IEditorContribution { getLineTokens: (lineNumber: number) => { return model.getLineTokens(lineNumber); }, - getLanguageIdentifier: () => { - return model.getLanguageIdentifier(); + getLanguageId: () => { + return model.getLanguageId(); }, getLanguageIdAtPosition: (lineNumber: number, column: number) => { return model.getLanguageIdAtPosition(lineNumber, column); @@ -557,7 +557,7 @@ export class AutoIndentOnPaste implements IEditorContribution { } } }; - let indentOfSecondLine = LanguageConfigurationRegistry.getGoodIndentForLine(autoIndent, virtualModel, model.getLanguageIdentifier().id, startLineNumber + 1, indentConverter); + let indentOfSecondLine = LanguageConfigurationRegistry.getGoodIndentForLine(autoIndent, virtualModel, model.getLanguageId(), startLineNumber + 1, indentConverter); if (indentOfSecondLine !== null) { let newSpaceCntOfSecondLine = indentUtils.getSpaceCnt(indentOfSecondLine, tabSize); let oldSpaceCntOfSecondLine = indentUtils.getSpaceCnt(strings.getLeadingWhitespace(model.getLineContent(startLineNumber + 1)), tabSize); diff --git a/src/vs/editor/contrib/inlayHints/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/inlayHintsController.ts index 4f81200fcb..7c43840270 100644 --- a/src/vs/editor/contrib/inlayHints/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/inlayHintsController.ts @@ -4,75 +4,120 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { hash } from 'vs/base/common/hash'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { LRUCache, ResourceMap } from 'vs/base/common/map'; +import { IRange } from 'vs/base/common/range'; +import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IContentDecorationRenderOptions, IEditorContribution } from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; -import { InlayHintsProvider, InlayHintsProviderRegistry, InlayHint } from 'vs/editor/common/modes'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { flatten } from 'vs/base/common/arrays'; -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 { 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 { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Range } from 'vs/editor/common/core/range'; +import { IContentDecorationRenderOptions, IDecorationRenderOptions, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel, IWordAtPosition, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { InlayHint, InlayHintKind, InlayHintsProvider, InlayHintsProviderRegistry } from 'vs/editor/common/modes'; +import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { editorInlayHintBackground, editorInlayHintForeground, editorInlayHintParameterBackground, editorInlayHintParameterForeground, editorInlayHintTypeBackground, editorInlayHintTypeForeground } from 'vs/platform/theme/common/colorRegistry'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -const MAX_DECORATORS = 500; +const MAX_DECORATORS = 1500; -export interface InlayHintsData { - list: InlayHint[]; - provider: InlayHintsProvider; +class RequestMap { + + private readonly _data = new ResourceMap>(); + + push(model: ITextModel, provider: T): void { + const value = this._data.get(model.uri); + if (value === undefined) { + this._data.set(model.uri, new Set([provider])); + } else { + value.add(provider); + } + } + + pop(model: ITextModel, provider: T): void { + const value = this._data.get(model.uri); + if (value) { + value.delete(provider); + if (value.size === 0) { + this._data.delete(model.uri); + } + } + } + + has(model: ITextModel, provider: T): boolean { + return Boolean(this._data.get(model.uri)?.has(provider)); + } } -export async function getInlayHints(model: ITextModel, ranges: Range[], token: CancellationToken): Promise { - const datas: InlayHintsData[] = []; +export async function getInlayHints(model: ITextModel, ranges: Range[], requests: RequestMap, token: CancellationToken): Promise { + const all: InlayHint[][] = []; const providers = InlayHintsProviderRegistry.ordered(model).reverse(); - const promises = flatten(providers.map(provider => ranges.map(range => { - return Promise.resolve(provider.provideInlayHints(model, range, token)).then(result => { - const itemsInRange = result?.filter(hint => range.containsPosition(hint.position)); - if (itemsInRange?.length) { - datas.push({ list: itemsInRange, provider }); + + const promises = providers.map(provider => ranges.map(async range => { + try { + requests.push(model, provider); + const result = await provider.provideInlayHints(model, range, token); + if (result?.length) { + all.push(result.filter(hint => range.containsPosition(hint.position))); } - }, err => { + } catch (err) { onUnexpectedExternalError(err); - }); - }))); + } finally { + requests.pop(model, provider); + } + })); - await Promise.all(promises); + await Promise.all(promises.flat()); - return datas; + return all.flat().sort((a, b) => Position.compare(a.position, b.position)); +} + +class InlayHintsCache { + + private readonly _entries = new LRUCache(50); + + get(model: ITextModel): InlayHint[] | undefined { + const key = InlayHintsCache._key(model); + return this._entries.get(key); + } + + set(model: ITextModel, value: InlayHint[]): void { + const key = InlayHintsCache._key(model); + this._entries.set(key, value); + } + + private static _key(model: ITextModel): string { + return `${model.uri.toString()}/${model.getVersionId()}`; + } } export class InlayHintsController implements IEditorContribution { static readonly ID: string = 'editor.contrib.InlayHints'; + private static _decorationOwnerIdPool = 0; + private readonly _decorationOwnerId = ++InlayHintsController._decorationOwnerIdPool; + private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); - private readonly _getInlayHintsDelays = new LanguageFeatureRequestDelays(InlayHintsProviderRegistry, 25, 2500); + private readonly _getInlayHintsDelays = new LanguageFeatureRequestDelays(InlayHintsProviderRegistry, 25, 500); + private readonly _cache = new InlayHintsCache(); - private _decorationsTypeIds: string[] = []; - private _decorationIds: string[] = []; + private _decorations = new Map(); constructor( private readonly _editor: ICodeEditor, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IThemeService private readonly _themeService: IThemeService, - @IConfigurationService private readonly _configurationService: IConfigurationService, ) { 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 => { @@ -80,7 +125,6 @@ export class InlayHintsController implements IEditorContribution { this._update(); } })); - this._update(); } @@ -92,33 +136,39 @@ export class InlayHintsController implements IEditorContribution { private _update(): void { this._sessionDisposables.clear(); + this._removeAllDecorations(); if (!this._editor.getOption(EditorOption.inlayHints).enabled) { - this._removeAllDecorations(); return; } const model = this._editor.getModel(); if (!model || !InlayHintsProviderRegistry.has(model)) { - this._removeAllDecorations(); return; } + // iff possible, quickly update from cache + const cached = this._cache.get(model); + if (cached) { + this._updateHintsDecorators([model.getFullModelRange()], cached); + } + + const requests = new RequestMap(); + const scheduler = new RunOnceScheduler(async () => { const t1 = Date.now(); const cts = new CancellationTokenSource(); this._sessionDisposables.add(toDisposable(() => cts.dispose(true))); - const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); - const result = await getInlayHints(model, visibleRanges, cts.token); - - // update moving average - const newDelay = this._getInlayHintsDelays.update(model, Date.now() - t1); - scheduler.delay = newDelay; - - // render hints - this._updateHintsDecorators(result); + const ranges = this._getHintsRanges(); + const result = await getInlayHints(model, ranges, requests, cts.token); + scheduler.delay = this._getInlayHintsDelays.update(model, Date.now() - t1); + if (cts.token.isCancellationRequested) { + return; + } + this._updateHintsDecorators(ranges, result); + this._cache.set(model, Array.from(this._decorations.values()).map(obj => obj.hint)); }, this._getInlayHintsDelays.get(model)); @@ -134,65 +184,124 @@ export class InlayHintsController implements IEditorContribution { this._sessionDisposables.add(providerListener); for (const provider of InlayHintsProviderRegistry.all(model)) { if (typeof provider.onDidChangeInlayHints === 'function') { - providerListener.add(provider.onDidChangeInlayHints(() => scheduler.schedule())); + providerListener.add(provider.onDidChangeInlayHints(() => { + if (!requests.has(model, provider)) { + scheduler.schedule(); + } + })); } } } - private _updateHintsDecorators(hintsData: InlayHintsData[]): void { + private _getHintsRanges(): Range[] { + const extra = 30; + const model = this._editor.getModel()!; + const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); + const result: Range[] = []; + for (const range of visibleRanges.sort(Range.compareRangesUsingStarts)) { + const extendedRange = model.validateRange(new Range(range.startLineNumber - extra, range.startColumn, range.endLineNumber + extra, range.endColumn)); + if (result.length === 0 || !Range.areIntersectingOrTouching(result[result.length - 1], extendedRange)) { + result.push(extendedRange); + } else { + result[result.length - 1] = Range.plusRange(result[result.length - 1], extendedRange); + } + } + return result; + } + + private _updateHintsDecorators(ranges: Range[], hints: InlayHint[]): void { + const { fontSize, fontFamily } = this._getLayoutInfo(); - const backgroundColor = this._themeService.getColorTheme().getColor(editorInlayHintBackground); - const fontColor = this._themeService.getColorTheme().getColor(editorInlayHintForeground); + const model = this._editor.getModel()!; const newDecorationsTypeIds: string[] = []; const newDecorationsData: IModelDeltaDecoration[] = []; - const fontFamilyVar = '--inlayHintsFontFamily'; + const fontFamilyVar = '--code-editorInlayHintsFontFamily'; this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily); - const key = this._configurationService.getValue('editor.useInjectedText'); - const shouldUseInjectedText = key === undefined ? true : !!key; + for (const hint of hints) { - for (const { list: hints } of hintsData) { + const { text, position, whitespaceBefore, whitespaceAfter } = hint; + const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0; + const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0; - for (let j = 0; j < hints.length && newDecorationsData.length < MAX_DECORATORS; j++) { - const { text, position, whitespaceBefore, whitespaceAfter } = hints[j]; - const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0; - const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0; + const contentOptions: IContentDecorationRenderOptions = { + contentText: fixSpace(text), + fontSize: `${fontSize}px`, + margin: `0px ${marginAfter}px 0px ${marginBefore}px`, + fontFamily: `var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}`, + padding: `1px ${Math.max(1, fontSize / 4) | 0}px`, + borderRadius: `${(fontSize / 4) | 0}px`, + verticalAlign: 'middle', + backgroundColor: themeColorFromId(editorInlayHintBackground), + color: themeColorFromId(editorInlayHintForeground) + }; - const massagedText = fixSpace(text); + if (hint.kind === InlayHintKind.Parameter) { + contentOptions.backgroundColor = themeColorFromId(editorInlayHintParameterBackground); + contentOptions.color = themeColorFromId(editorInlayHintParameterForeground); + } else if (hint.kind === InlayHintKind.Type) { + contentOptions.backgroundColor = themeColorFromId(editorInlayHintTypeBackground); + contentOptions.color = themeColorFromId(editorInlayHintTypeForeground); + } - const before: IContentDecorationRenderOptions = { - contentText: massagedText, - backgroundColor: `${backgroundColor}`, - color: `${fontColor}`, - margin: `0px ${marginAfter}px 0px ${marginBefore}px`, - fontSize: `${fontSize}px`, - fontFamily: `var(${fontFamilyVar})`, - padding: `0px ${(fontSize / 4) | 0}px`, - borderRadius: `${(fontSize / 4) | 0}px`, - verticalAlign: 'middle', - }; - const key = 'inlayHints-' + hash(before).toString(16); - this._codeEditorService.registerDecorationType('inlay-hints-controller', key, - shouldUseInjectedText ? { beforeInjectedText: { ...before, affectsLetterSpacing: true } } : { before }, undefined, this._editor); + let renderOptions: IDecorationRenderOptions = { beforeInjectedText: { ...contentOptions, affectsLetterSpacing: true } }; - // decoration types are ref-counted which means we only need to - // call register und remove equally often - newDecorationsTypeIds.push(key); + let range = Range.fromPositions(position); + let word = model.getWordAtPosition(position); + let usesWordRange = false; + if (word) { + if (word.endColumn === position.column) { + // change decoration to after + renderOptions.afterInjectedText = renderOptions.beforeInjectedText; + renderOptions.beforeInjectedText = undefined; + usesWordRange = true; + range = wordToRange(word, position.lineNumber); + } else if (word.startColumn === position.column) { + usesWordRange = true; + range = wordToRange(word, position.lineNumber); + } + } - const options = this._codeEditorService.resolveDecorationOptions(key, true); - newDecorationsData.push({ - range: Range.fromPositions(position), - options - }); + const key = 'inlayHints-' + hash(renderOptions).toString(16); + this._codeEditorService.registerDecorationType('inlay-hints-controller', key, renderOptions, undefined, this._editor); + + // decoration types are ref-counted which means we only need to + // call register und remove equally often + newDecorationsTypeIds.push(key); + + const newLen = newDecorationsData.push({ + range, + options: { + ...this._codeEditorService.resolveDecorationOptions(key, true), + showIfCollapsed: !usesWordRange, + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + } + }); + + if (newLen > MAX_DECORATORS) { + break; } } - this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); - this._decorationsTypeIds = newDecorationsTypeIds; - - this._decorationIds = this._editor.deltaDecorations(this._decorationIds, newDecorationsData); + // collect all decoration ids that are affected by the ranges + // and only update those decorations + const decorationIdsToUpdate: string[] = []; + for (const range of ranges) { + for (const { id } of model.getDecorationsInRange(range, this._decorationOwnerId, true)) { + const obj = this._decorations.get(id); + if (obj) { + decorationIdsToUpdate.push(id); + this._codeEditorService.removeDecorationType(obj.decorationTypeId); + this._decorations.delete(id); + } + } + } + const newDecorationIds = model.deltaDecorations(decorationIdsToUpdate, newDecorationsData, this._decorationOwnerId); + for (let i = 0; i < newDecorationIds.length; i++) { + this._decorations.set(newDecorationIds[i], { hint: hints[i], decorationTypeId: newDecorationsTypeIds[i] }); + } } private _getLayoutInfo() { @@ -207,12 +316,23 @@ export class InlayHintsController implements IEditorContribution { } private _removeAllDecorations(): void { - this._decorationIds = this._editor.deltaDecorations(this._decorationIds, []); - this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); - this._decorationsTypeIds = []; + this._editor.deltaDecorations(Array.from(this._decorations.keys()), []); + for (let obj of this._decorations.values()) { + this._codeEditorService.removeDecorationType(obj.decorationTypeId); + } + this._decorations.clear(); } } +function wordToRange(word: IWordAtPosition, lineNumber: number): Range { + return new Range( + lineNumber, + word.startColumn, + lineNumber, + word.endColumn + ); +} + function fixSpace(str: string): string { const noBreakWhitespace = '\xa0'; return str.replace(/[ \t]/g, noBreakWhitespace); @@ -228,8 +348,8 @@ CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, . const ref = await accessor.get(ITextModelService).createModelReference(uri); try { - 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)); + const data = await getInlayHints(ref.object.textEditorModel, [Range.lift(range)], new RequestMap(), CancellationToken.None); + return data; } finally { ref.dispose(); diff --git a/src/vs/editor/contrib/inlineCompletions/ghostText.css b/src/vs/editor/contrib/inlineCompletions/ghostText.css index 9e93209415..a8eaaa8845 100644 --- a/src/vs/editor/contrib/inlineCompletions/ghostText.css +++ b/src/vs/editor/contrib/inlineCompletions/ghostText.css @@ -23,3 +23,11 @@ opacity: 0; font-size: 0; } + +.monaco-editor .ghost-text-decoration { + font-style: italic; +} + +.monaco-editor .suggest-preview-text { + font-style: italic; +} diff --git a/src/vs/editor/contrib/inlineCompletions/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/ghostText.ts index f6a03f7a79..a4a7631316 100644 --- a/src/vs/editor/contrib/inlineCompletions/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/ghostText.ts @@ -8,7 +8,7 @@ import { Disposable } 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, IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; export class GhostText { public static equals(a: GhostText | undefined, b: GhostText | undefined): boolean { @@ -100,6 +100,10 @@ export class GhostTextPart { constructor( readonly column: number, readonly lines: readonly string[], + /** + * Indicates if this part is a preview of an inline suggestion when a suggestion is previewed. + */ + readonly preview: boolean, ) { } diff --git a/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts b/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts index 00d0bd2bea..12df3a291f 100644 --- a/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts +++ b/src/vs/editor/contrib/inlineCompletions/ghostTextController.ts @@ -4,23 +4,26 @@ *--------------------------------------------------------------------------------------------*/ 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 { firstNonWhitespaceIndex } from 'vs/base/common/strings'; 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 { CursorColumns } from 'vs/editor/common/controller/cursorColumns'; +import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/consts'; +import { GhostTextModel } from 'vs/editor/contrib/inlineCompletions/ghostTextModel'; import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget'; import * as nls from 'vs/nls'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { GhostTextModel } from 'vs/editor/contrib/inlineCompletions/ghostTextModel'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/consts'; 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")); + public static readonly inlineSuggestionHasIndentationLessThanTabSize = new RawContextKey('inlineSuggestionHasIndentationLessThanTabSize', true, nls.localize('inlineSuggestionHasIndentationLessThanTabSize', "Whether the inline suggestion starts with whitespace that is less than what would be inserted by tab")); static ID = 'editor.contrib.ghostTextController'; @@ -111,6 +114,7 @@ export class GhostTextController extends Disposable { class GhostTextContextKeys { public readonly inlineCompletionVisible = GhostTextController.inlineSuggestionVisible.bindTo(this.contextKeyService); public readonly inlineCompletionSuggestsIndentation = GhostTextController.inlineSuggestionHasIndentation.bindTo(this.contextKeyService); + public readonly inlineCompletionSuggestsIndentationLessThanTabSize = GhostTextController.inlineSuggestionHasIndentationLessThanTabSize.bindTo(this.contextKeyService); constructor(private readonly contextKeyService: IContextKeyService) { } @@ -135,6 +139,7 @@ export class ActiveGhostTextController extends Disposable { this._register(toDisposable(() => { this.contextKeys.inlineCompletionVisible.set(false); this.contextKeys.inlineCompletionSuggestsIndentation.set(false); + this.contextKeys.inlineCompletionSuggestsIndentationLessThanTabSize.set(true); })); this._register(this.model.onDidChange(() => { @@ -148,21 +153,33 @@ export class ActiveGhostTextController extends Disposable { this.model.activeInlineCompletionsModel?.ghostText !== undefined ); + let startsWithIndentation = false; + let startsWithIndentationLessThanTabSize = true; + const ghostText = this.model.inlineCompletionsModel.ghostText; - if (ghostText && ghostText.parts.length > 0) { + if (!!this.model.activeInlineCompletionsModel && ghostText && ghostText.parts.length > 0) { const { column, lines } = ghostText.parts[0]; - const suggestionStartsWithWs = lines[0].startsWith(' ') || lines[0].startsWith('\t'); + + const firstLine = lines[0]; const indentationEndColumn = this.editor.getModel().getLineIndentColumn(ghostText.lineNumber); const inIndentation = column <= indentationEndColumn; - this.contextKeys.inlineCompletionSuggestsIndentation.set( - !!this.model.activeInlineCompletionsModel - && suggestionStartsWithWs && inIndentation - ); - } else { - this.contextKeys.inlineCompletionSuggestsIndentation.set(false); + if (inIndentation) { + let firstNonWsIdx = firstNonWhitespaceIndex(firstLine); + if (firstNonWsIdx === -1) { + firstNonWsIdx = firstLine.length - 1; + } + startsWithIndentation = firstNonWsIdx > 0; + + const tabSize = this.editor.getModel().getOptions().tabSize; + const visibleColumnIndentation = CursorColumns.visibleColumnFromColumn(firstLine, firstNonWsIdx + 1, tabSize); + startsWithIndentationLessThanTabSize = visibleColumnIndentation < tabSize; + } } + + this.contextKeys.inlineCompletionSuggestsIndentation.set(startsWithIndentation); + this.contextKeys.inlineCompletionSuggestsIndentationLessThanTabSize.set(startsWithIndentationLessThanTabSize); } } @@ -184,7 +201,7 @@ KeybindingsRegistry.registerKeybindingRule({ when: ContextKeyExpr.and( commitInlineSuggestionAction.precondition, EditorContextKeys.tabMovesFocus.toNegated(), - GhostTextController.inlineSuggestionHasIndentation.toNegated() + GhostTextController.inlineSuggestionHasIndentationLessThanTabSize ), }); @@ -210,7 +227,7 @@ export class ShowNextInlineSuggestionAction extends EditorAction { precondition: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.inlineSuggestionVisible), kbOpts: { weight: 100, - primary: KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET, + primary: KeyMod.Alt | KeyCode.BracketRight, }, }); } @@ -234,7 +251,7 @@ export class ShowPreviousInlineSuggestionAction extends EditorAction { precondition: ContextKeyExpr.and(EditorContextKeys.writable, GhostTextController.inlineSuggestionVisible), kbOpts: { weight: 100, - primary: KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET, + primary: KeyMod.Alt | KeyCode.BracketLeft, }, }); } diff --git a/src/vs/editor/contrib/inlineCompletions/ghostTextModel.ts b/src/vs/editor/contrib/inlineCompletions/ghostTextModel.ts index edb78c5e7b..90c6058f56 100644 --- a/src/vs/editor/contrib/inlineCompletions/ghostTextModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/ghostTextModel.ts @@ -3,16 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter } from 'vs/base/common/event'; import { Disposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; -import { SuggestWidgetAdapterModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { Emitter } from 'vs/base/common/event'; -import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { InlineCompletionTriggerKind } from 'vs/editor/common/modes'; +import { GhostText, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostText'; +import { InlineCompletionsModel, LiveInlineCompletions, SynchronizedInlineCompletionsCache } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; +import { SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel'; import { createDisposableRef } from 'vs/editor/contrib/inlineCompletions/utils'; -import { GhostTextWidgetModel, GhostText } from 'vs/editor/contrib/inlineCompletions/ghostText'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export abstract class DelegatingModel extends Disposable implements GhostTextWidgetModel { private readonly onDidChangeEmitter = new Emitter(); @@ -65,8 +66,9 @@ export abstract class DelegatingModel extends Disposable implements GhostTextWid * A ghost text model that is both driven by inline completions and the suggest widget. */ export class GhostTextModel extends DelegatingModel implements GhostTextWidgetModel { - public readonly suggestWidgetAdapterModel = this._register(new SuggestWidgetAdapterModel(this.editor)); - public readonly inlineCompletionsModel = this._register(new InlineCompletionsModel(this.editor, this.commandService)); + public readonly sharedCache = this._register(new SharedInlineCompletionCache()); + public readonly suggestWidgetAdapterModel = this._register(new SuggestWidgetPreviewModel(this.editor, this.sharedCache)); + public readonly inlineCompletionsModel = this._register(new InlineCompletionsModel(this.editor, this.sharedCache, this.commandService)); public get activeInlineCompletionsModel(): InlineCompletionsModel | undefined { if (this.targetModel === this.inlineCompletionsModel) { @@ -105,7 +107,7 @@ export class GhostTextModel extends DelegatingModel implements GhostTextWidgetMo } public triggerInlineCompletion(): void { - this.activeInlineCompletionsModel?.trigger(); + this.activeInlineCompletionsModel?.trigger(InlineCompletionTriggerKind.Explicit); } public commitInlineCompletion(): void { @@ -129,3 +131,34 @@ export class GhostTextModel extends DelegatingModel implements GhostTextWidgetMo return result !== undefined ? result : false; } } + +export class SharedInlineCompletionCache extends Disposable { + private readonly onDidChangeEmitter = new Emitter(); + public readonly onDidChange = this.onDidChangeEmitter.event; + + private readonly cache = this._register(new MutableDisposable()); + + public get value(): SynchronizedInlineCompletionsCache | undefined { + return this.cache.value; + } + + public setValue(editor: IActiveCodeEditor, + completionsSource: LiveInlineCompletions, + triggerKind: InlineCompletionTriggerKind + ) { + this.cache.value = new SynchronizedInlineCompletionsCache( + editor, + completionsSource, + () => this.onDidChangeEmitter.fire(), + triggerKind + ); + } + + public clearAndLeak(): SynchronizedInlineCompletionsCache | undefined { + return this.cache.clearAndLeak(); + } + + public clear() { + this.cache.clear(); + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts index 764b4fcdaa..0636f3db46 100644 --- a/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts @@ -3,43 +3,46 @@ * 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 { Color, RGBA } from 'vs/base/common/color'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Range } from 'vs/editor/common/core/range'; +import * as strings from 'vs/base/common/strings'; +import 'vs/css!./ghostText'; +import { Configuration } from 'vs/editor/browser/config/configuration'; import { ContentWidgetPositionPreference, 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, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { Configuration } from 'vs/editor/browser/config/configuration'; +import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; -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'; +import { Range } from 'vs/editor/common/core/range'; +import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; -import { GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostText'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { ILanguageIdCodec } from 'vs/editor/common/modes'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ghostTextBackground, ghostTextBorder, ghostTextForeground } from 'vs/editor/common/view/editorColorRegistry'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; +import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; +import { GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostText'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value }); export class GhostTextWidget extends Disposable { private disposed = false; private readonly partsWidget = this._register(this.instantiationService.createInstance(DecorationsWidget, this.editor)); - private readonly additionalLinesWidget = this._register(new AdditionalLinesWidget(this.editor)); + private readonly additionalLinesWidget = this._register(new AdditionalLinesWidget(this.editor, this.modeService.languageIdCodec)); private viewMoreContentWidget: ViewMoreLinesContentWidget | undefined = undefined; constructor( private readonly editor: ICodeEditor, private readonly model: GhostTextWidgetModel, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IModeService private readonly modeService: IModeService, ) { super(); @@ -116,6 +119,7 @@ export class GhostTextWidget extends Disposable { inlineTexts.push({ column: part.column, text: lines[0], + preview: part.preview, }); lines = lines.slice(1); } else { @@ -192,6 +196,7 @@ interface HiddenText { interface InsertedInlineText { column: number; text: string; + preview: boolean; } class DecorationsWidget implements IDisposable { @@ -221,6 +226,7 @@ class DecorationsWidget implements IDisposable { const colorTheme = this.themeService.getColorTheme(); const foreground = colorTheme.getColor(ghostTextForeground); + let opacity: string | undefined = undefined; let color: string | undefined = undefined; if (foreground) { @@ -273,6 +279,7 @@ class DecorationsWidget implements IDisposable { opacity, color, border, + fontWeight: p.preview ? 'bold' : 'normal', }, })); @@ -280,7 +287,8 @@ class DecorationsWidget implements IDisposable { range: Range.fromPositions(new Position(lineNumber, p.column)), options: shouldUseInjectedText ? { description: 'ghost-text', - after: { content: contentText, inlineClassName: 'ghost-text-decoration' } + after: { content: contentText, inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration' }, + showIfCollapsed: true, } : { ...decorationType.resolve() } @@ -332,7 +340,10 @@ class AdditionalLinesWidget implements IDisposable { private _viewZoneId: string | undefined = undefined; public get viewZoneId(): string | undefined { return this._viewZoneId; } - constructor(private readonly editor: ICodeEditor) { } + constructor( + private readonly editor: ICodeEditor, + private readonly languageIdCodec: ILanguageIdCodec + ) { } public dispose(): void { this.clear(); @@ -364,7 +375,7 @@ class AdditionalLinesWidget implements IDisposable { const heightInLines = Math.max(additionalLines.length, minReservedLineCount); if (heightInLines > 0) { const domNode = document.createElement('div'); - renderLines(domNode, tabSize, additionalLines, this.editor.getOptions()); + renderLines(domNode, tabSize, additionalLines, this.editor.getOptions(), this.languageIdCodec); this._viewZoneId = changeAccessor.addZone({ afterLineNumber: lineNumber, @@ -381,7 +392,7 @@ interface LineData { decorations: LineDecoration[]; } -function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], opts: IComputedEditorOptions): void { +function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], opts: IComputedEditorOptions, languageIdCodec: ILanguageIdCodec): void { const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations); const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter); // To avoid visual confusion, we don't want to render visible whitespace @@ -404,7 +415,7 @@ function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], o const isBasicASCII = strings.isBasicASCII(line); const containsRTL = strings.containsRTL(line); - const lineTokens = LineTokens.createEmpty(line); + const lineTokens = LineTokens.createEmpty(line, languageIdCodec); renderViewLine(new RenderLineInput( (fontInfo.isMonospace && !disableMonospaceOptimizations), @@ -489,17 +500,24 @@ class ViewMoreLinesContentWidget extends Disposable implements IContentWidget { registerThemingParticipant((theme, collector) => { const foreground = theme.getColor(ghostTextForeground); - if (foreground) { - const opacity = String(foreground.rgba.a); - const color = Color.Format.CSS.format(opaque(foreground))!; + // `!important` ensures that other decorations don't cause a style conflict (#132017). + collector.addRule(`.monaco-editor .ghost-text-decoration { color: ${foreground.toString()} !important; }`); + collector.addRule(`.monaco-editor .ghost-text-decoration-preview { color: ${foreground.toString()} !important; }`); + collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { color: ${foreground.toString()} !important; }`); + } - collector.addRule(`.monaco-editor .ghost-text-decoration { opacity: ${opacity}; color: ${color}; }`); - collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { opacity: ${opacity}; color: ${color}; }`); + const background = theme.getColor(ghostTextBackground); + if (background) { + collector.addRule(`.monaco-editor .ghost-text-decoration { background-color: ${background.toString()}; }`); + collector.addRule(`.monaco-editor .ghost-text-decoration-preview { background-color: ${background.toString()}; }`); + collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { background-color: ${background.toString()}; }`); } const border = theme.getColor(ghostTextBorder); if (border) { - collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { border: 2px dashed ${border}; }`); + collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { border: 1px solid ${border}; }`); + collector.addRule(`.monaco-editor .ghost-text-decoration { border: 1px solid ${border}; }`); + collector.addRule(`.monaco-editor .ghost-text-decoration-preview { border: 1px solid ${border}; }`); } }); diff --git a/src/vs/editor/contrib/inlineCompletions/inlineCompletionToGhostText.ts b/src/vs/editor/contrib/inlineCompletions/inlineCompletionToGhostText.ts new file mode 100644 index 0000000000..68a5fba7eb --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/inlineCompletionToGhostText.ts @@ -0,0 +1,214 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff'; +import * as strings from 'vs/base/common/strings'; +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 } from 'vs/editor/common/modes'; +import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/ghostText'; + +export interface NormalizedInlineCompletion extends InlineCompletion { + range: Range; +} + +export function normalizedInlineCompletionsEquals(a: NormalizedInlineCompletion | undefined, b: NormalizedInlineCompletion | undefined): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.range.equalsRange(b.range) && a.text === b.text && a.command === b.command; +} + +/** + * @param previewSuffixLength Sets where to split `inlineCompletion.text`. + * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`. +*/ +export function inlineCompletionToGhostText( + inlineCompletion: NormalizedInlineCompletion, + textModel: ITextModel, + mode: 'prefix' | 'subword' | 'subwordSmart', + cursorPosition?: Position, + previewSuffixLength = 0 +): GhostText | undefined { + if (inlineCompletion.range.startLineNumber !== inlineCompletion.range.endLineNumber) { + // Only single line replacements are supported. + return undefined; + } + + const sourceLine = textModel.getLineContent(inlineCompletion.range.startLineNumber); + const sourceIndentationLength = strings.getLeadingWhitespace(sourceLine).length; + + const suggestionTouchesIndentation = inlineCompletion.range.startColumn - 1 <= sourceIndentationLength; + if (suggestionTouchesIndentation) { + // source: ··········[······abc] + // ^^^^^^^^^ inlineCompletion.range + // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength + // ^^^^^^ replacedIndentation.length + // ^^^ rangeThatDoesNotReplaceIndentation + + // inlineCompletion.text: '··foo' + // ^^ suggestionAddedIndentationLength + + const suggestionAddedIndentationLength = strings.getLeadingWhitespace(inlineCompletion.text).length; + + const replacedIndentation = sourceLine.substring(inlineCompletion.range.startColumn - 1, sourceIndentationLength); + const rangeThatDoesNotReplaceIndentation = Range.fromPositions( + inlineCompletion.range.getStartPosition().delta(0, replacedIndentation.length), + inlineCompletion.range.getEndPosition() + ); + + const suggestionWithoutIndentationChange = + inlineCompletion.text.startsWith(replacedIndentation) + // Adds more indentation without changing existing indentation: We can add ghost text for this + ? inlineCompletion.text.substring(replacedIndentation.length) + // Changes or removes existing indentation. Only add ghost text for the non-indentation part. + : inlineCompletion.text.substring(suggestionAddedIndentationLength); + + inlineCompletion = { + range: rangeThatDoesNotReplaceIndentation, + text: suggestionWithoutIndentationChange, + command: inlineCompletion.command + }; + } + + // This is a single line string + const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range); + + const changes = cachingDiff(valueToBeReplaced, inlineCompletion.text); + + if (!changes) { + // No ghost text in case the diff would be too slow to compute + return undefined; + } + + const lineNumber = inlineCompletion.range.startLineNumber; + + const parts = new Array(); + + if (mode === 'prefix') { + const filteredChanges = changes.filter(c => c.originalLength === 0); + if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) { + // Prefixes only have a single change. + return undefined; + } + } + + const previewStartInCompletionText = inlineCompletion.text.length - previewSuffixLength; + + for (const c of changes) { + const insertColumn = inlineCompletion.range.startColumn + c.originalStart + c.originalLength; + + if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === inlineCompletion.range.startLineNumber && insertColumn < cursorPosition.column) { + // No ghost text before cursor + return undefined; + } + + if (c.originalLength > 0) { + return undefined; + } + + if (c.modifiedLength === 0) { + continue; + } + + const modifiedEnd = c.modifiedStart + c.modifiedLength; + const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText)); + const nonPreviewText = inlineCompletion.text.substring(c.modifiedStart, nonPreviewTextEnd); + const italicText = inlineCompletion.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd)); + + if (nonPreviewText.length > 0) { + const lines = strings.splitLines(nonPreviewText); + parts.push(new GhostTextPart(insertColumn, lines, false)); + } + if (italicText.length > 0) { + const lines = strings.splitLines(italicText); + parts.push(new GhostTextPart(insertColumn, lines, true)); + } + } + + return new GhostText(lineNumber, parts, 0); +} + +let lastRequest: { originalValue: string; newValue: string; changes: readonly IDiffChange[] | undefined; } | undefined = undefined; +function cachingDiff(originalValue: string, newValue: string): readonly IDiffChange[] | undefined { + if (lastRequest?.originalValue === originalValue && lastRequest?.newValue === newValue) { + return lastRequest?.changes; + } else { + const changes = smartDiff(originalValue, newValue); + lastRequest = { + originalValue, + newValue, + changes + }; + return changes; + } +} + +/** + * When matching `if ()` with `if (f() = 1) { g(); }`, + * align it like this: `if ( )` + * Not like this: `if ( )` + * Also not like this: `if ( )`. + * + * The parenthesis are preprocessed to ensure that they match correctly. + */ +function smartDiff(originalValue: string, newValue: string): (readonly IDiffChange[]) | undefined { + if (originalValue.length > 5000 || newValue.length > 5000) { + // We don't want to work on strings that are too big + return undefined; + } + + function getMaxCharCode(val: string): number { + let maxCharCode = 0; + for (let i = 0, len = val.length; i < len; i++) { + const charCode = val.charCodeAt(i); + if (charCode > maxCharCode) { + maxCharCode = charCode; + } + } + return maxCharCode; + } + + const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue)); + function getUniqueCharCode(id: number): number { + if (id < 0) { + throw new Error('unexpected'); + } + return maxCharCode + id + 1; + } + + function getElements(source: string): Int32Array { + let level = 0; + let group = 0; + const characters = new Int32Array(source.length); + for (let i = 0, len = source.length; i < len; i++) { + const id = group * 100 + level; + + // TODO support more brackets + if (source[i] === '(') { + characters[i] = getUniqueCharCode(2 * id); + level++; + } else if (source[i] === ')') { + characters[i] = getUniqueCharCode(2 * id + 1); + if (level === 1) { + group++; + } + level = Math.max(level - 1, 0); + } else { + characters[i] = source.charCodeAt(i); + } + } + return characters; + } + + const elements1 = getElements(originalValue); + const elements2 = getElements(newValue); + + return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes; +} diff --git a/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts index 0546958b2e..32f0c265c4 100644 --- a/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts +++ b/src/vs/editor/contrib/inlineCompletions/inlineCompletionsHoverParticipant.ts @@ -3,23 +3,23 @@ * 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 * as dom from 'vs/base/browser/dom'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ITextContentData, IViewZoneData } from 'vs/editor/browser/controller/mouseTarget'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; 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 { DisposableStore, 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'; -import * as dom from 'vs/base/browser/dom'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; -import { MarkdownString } from 'vs/base/common/htmlContent'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { HoverAnchor, HoverAnchorType, HoverForeignElementAnchor, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; +import { commitInlineSuggestionAction, GhostTextController, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction } from 'vs/editor/contrib/inlineCompletions/ghostTextController'; +import * as nls from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class InlineCompletionsHover implements IHoverPart { constructor( diff --git a/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts index 25094a7259..c9117ce1d9 100644 --- a/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts @@ -5,11 +5,10 @@ import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDiffChange, LcsDiff } from 'vs/base/common/diff/diff'; import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import * as strings from 'vs/base/common/strings'; +import { commonPrefixLength, commonSuffixLength } from 'vs/base/common/strings'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; @@ -19,9 +18,11 @@ 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/ghostText'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { inlineSuggestCommitId } from './consts'; -import { BaseGhostTextWidgetModel, GhostText, GhostTextPart, GhostTextWidgetModel } from './ghostText'; +import { SharedInlineCompletionCache } from './ghostTextModel'; +import { inlineCompletionToGhostText, NormalizedInlineCompletion } from './inlineCompletionToGhostText'; export class InlineCompletionsModel extends Disposable implements GhostTextWidgetModel { protected readonly onDidChangeEmitter = new Emitter(); @@ -34,7 +35,8 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge constructor( private readonly editor: IActiveCodeEditor, - @ICommandService private readonly commandService: ICommandService + private readonly cache: SharedInlineCompletionCache, + @ICommandService private readonly commandService: ICommandService, ) { super(); @@ -67,6 +69,10 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge this._register(toDisposable(() => { this.disposed = true; })); + + this._register(this.editor.onDidBlurEditorWidget(() => { + this.hide(); + })); } private handleUserInput() { @@ -119,14 +125,24 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge return; } - this.trigger(); + this.trigger(InlineCompletionTriggerKind.Automatic); } - public trigger(): void { + public trigger(triggerKind: InlineCompletionTriggerKind): void { if (this.completionSession.value) { + if (triggerKind === InlineCompletionTriggerKind.Explicit) { + void this.completionSession.value.ensureUpdateWithExplicitContext(); + } return; } - this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active, this.commandService); + this.completionSession.value = new InlineCompletionsSession( + this.editor, + this.editor.getPosition(), + () => this.active, + this.commandService, + this.cache, + triggerKind + ); this.completionSession.value.takeOwnership( this.completionSession.value.onDidChange(() => { this.onDidChangeEmitter.fire(); @@ -162,16 +178,21 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { public readonly minReservedLineCount = 0; private readonly updateOperation = this._register(new MutableDisposable()); - private readonly cache = this._register(new MutableDisposable()); - private readonly updateSoon = this._register(new RunOnceScheduler(() => this.update(InlineCompletionTriggerKind.Automatic), 50)); - private readonly textModel = this.editor.getModel(); + private readonly updateSoon = this._register(new RunOnceScheduler(() => { + let triggerKind = this.initialTriggerKind; + // All subsequent triggers are automatic. + this.initialTriggerKind = InlineCompletionTriggerKind.Automatic; + return this.update(triggerKind); + }, 50)); constructor( editor: IActiveCodeEditor, private readonly triggerPosition: Position, private readonly shouldUpdate: () => boolean, private readonly commandService: ICommandService, + private readonly cache: SharedInlineCompletionCache, + private initialTriggerKind: InlineCompletionTriggerKind ) { super(editor); @@ -188,6 +209,10 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { } })); + this._register(toDisposable(() => { + this.cache.clear(); + })); + this._register(this.editor.onDidChangeCursorPosition((e) => { if (this.cache.value) { this.onDidChangeEmitter.fire(); @@ -195,24 +220,6 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { })); 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(); })); @@ -279,7 +286,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { this.onDidChangeEmitter.fire(); } - private async ensureUpdateWithExplicitContext(): Promise { + public async ensureUpdateWithExplicitContext(): Promise { if (this.updateOperation.value) { // Restart or wait for current update operation if (this.updateOperation.value.triggerKind === InlineCompletionTriggerKind.Explicit) { @@ -311,14 +318,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { 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, - }; + return completion.toLiveInlineCompletion(); } get isValid(): boolean { @@ -344,7 +344,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { try { result = await provideInlineCompletions(position, this.editor.getModel(), - { triggerKind }, + { triggerKind, selectedSuggestionInfo: undefined }, token ); } catch (e) { @@ -356,10 +356,9 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { return; } - this.cache.value = new SynchronizedInlineCompletionsCache( + this.cache.setValue( this.editor, result, - () => this.onDidChangeEmitter.fire(), triggerKind ); this.onDidChangeEmitter.fire(); @@ -414,7 +413,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { } } -class UpdateOperation implements IDisposable { +export class UpdateOperation implements IDisposable { constructor(public readonly promise: CancelablePromise, public readonly triggerKind: InlineCompletionTriggerKind) { } @@ -427,7 +426,7 @@ class UpdateOperation implements IDisposable { * 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 { +export class SynchronizedInlineCompletionsCache extends Disposable { public readonly completions: readonly CachedInlineCompletion[]; constructor( @@ -483,6 +482,7 @@ class CachedInlineCompletion { startColumn: this.inlineCompletion.range.startColumn, command: this.inlineCompletion.command }); + /** * The range, synchronized with text model changes. */ @@ -494,135 +494,19 @@ class CachedInlineCompletion { ) { this.synchronizedRange = inlineCompletion.range; } -} -export interface NormalizedInlineCompletion extends InlineCompletion { - range: Range; -} - -export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCompletion, textModel: ITextModel, mode: 'prefix' | 'subword' | 'subwordSmart', cursorPosition?: Position): GhostText | undefined { - if (inlineCompletion.range.startLineNumber !== inlineCompletion.range.endLineNumber) { - // Only single line replacements are supported. - return undefined; - } - - // This is a single line string - const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range); - - const changes = cachingDiff(valueToBeReplaced, inlineCompletion.text); - - const lineNumber = inlineCompletion.range.startLineNumber; - - const parts = new Array(); - - if (mode === 'prefix') { - const filteredChanges = changes.filter(c => c.originalLength === 0); - if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) { - // Prefixes only have a single change. - return undefined; - } - } - - for (const c of changes) { - const insertColumn = inlineCompletion.range.startColumn + c.originalStart + c.originalLength; - - if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === inlineCompletion.range.startLineNumber && insertColumn < cursorPosition.column) { - // No ghost text before cursor - return undefined; - } - - if (c.originalLength > 0) { - const originalText = valueToBeReplaced.substr(c.originalStart, c.originalLength); - const firstNonWsCol = textModel.getLineFirstNonWhitespaceColumn(lineNumber); - if (!(/^(\t| )*$/.test(originalText) && (firstNonWsCol === 0 || insertColumn <= firstNonWsCol))) { - return undefined; - } - } - - if (c.modifiedLength === 0) { - continue; - } - - const text = inlineCompletion.text.substr(c.modifiedStart, c.modifiedLength); - const lines = strings.splitLines(text); - parts.push(new GhostTextPart(insertColumn, lines)); - } - - return new GhostText(lineNumber, parts, 0); -} - -let lastRequest: { originalValue: string, newValue: string, changes: readonly IDiffChange[] } | undefined = undefined; -function cachingDiff(originalValue: string, newValue: string): readonly IDiffChange[] { - if (lastRequest?.originalValue === originalValue && lastRequest?.newValue === newValue) { - return lastRequest?.changes; - } else { - const changes = smartDiff(originalValue, newValue); - lastRequest = { - originalValue, - newValue, - changes + public toLiveInlineCompletion(): LiveInlineCompletion | undefined { + return { + text: this.inlineCompletion.text, + range: this.synchronizedRange, + command: this.inlineCompletion.command, + sourceProvider: this.inlineCompletion.sourceProvider, + sourceInlineCompletions: this.inlineCompletion.sourceInlineCompletions, + sourceInlineCompletion: this.inlineCompletion.sourceInlineCompletion, }; - return changes; } } -/** - * When matching `if ()` with `if (f() = 1) { g(); }`, - * align it like this: `if ( )` - * Not like this: `if ( )` - * Also not like this: `if ( )`. - * - * The parenthesis are preprocessed to ensure that they match correctly. - */ -function smartDiff(originalValue: string, newValue: string): readonly IDiffChange[] { - function getMaxCharCode(val: string): number { - let maxCharCode = 0; - for (let i = 0, len = val.length; i < len; i++) { - const charCode = val.charCodeAt(i); - if (charCode > maxCharCode) { - maxCharCode = charCode; - } - } - return maxCharCode; - } - const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue)); - function getUniqueCharCode(id: number): number { - if (id < 0) { - throw new Error('unexpected'); - } - return maxCharCode + id + 1; - } - - function getElements(source: string): Int32Array { - let level = 0; - let group = 0; - const characters = new Int32Array(source.length); - for (let i = 0, len = source.length; i < len; i++) { - const id = group * 100 + level; - - // TODO support more brackets - if (source[i] === '(') { - characters[i] = getUniqueCharCode(2 * id); - level++; - } else if (source[i] === ')') { - characters[i] = getUniqueCharCode(2 * id + 1); - if (level === 1) { - group++; - } - level = Math.max(level - 1, 0); - } else { - characters[i] = source.charCodeAt(i); - } - } - return characters; - } - - const elements1 = getElements(originalValue); - const elements2 = getElements(newValue); - - return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes; -} - export interface LiveInlineCompletion extends NormalizedInlineCompletion { sourceProvider: InlineCompletionsProvider; sourceInlineCompletion: InlineCompletion; @@ -646,7 +530,7 @@ function getDefaultRange(position: Position, model: ITextModel): Range { : Range.fromPositions(position, position.with(undefined, maxColumn)); } -async function provideInlineCompletions( +export async function provideInlineCompletions( position: Position, model: ITextModel, context: InlineCompletionContext, @@ -702,3 +586,29 @@ async function provideInlineCompletions( }, }; } + +/** + * Shrinks the range if the text has a suffix/prefix that agrees with the text buffer. + * E.g. text buffer: `ab[cdef]ghi`, [...] is the replace range, `cxyzf` is the new text. + * Then the minimized inline completion has range `abc[de]fghi` and text `xyz`. + */ +export function minimizeInlineCompletion(model: ITextModel, inlineCompletion: NormalizedInlineCompletion): NormalizedInlineCompletion; +export function minimizeInlineCompletion(model: ITextModel, inlineCompletion: NormalizedInlineCompletion | undefined): NormalizedInlineCompletion | undefined; +export function minimizeInlineCompletion(model: ITextModel, inlineCompletion: NormalizedInlineCompletion | undefined): NormalizedInlineCompletion | undefined { + if (!inlineCompletion) { + return inlineCompletion; + } + const valueToReplace = model.getValueInRange(inlineCompletion.range); + const commonPrefixLen = commonPrefixLength(valueToReplace, inlineCompletion.text); + const startOffset = model.getOffsetAt(inlineCompletion.range.getStartPosition()) + commonPrefixLen; + const start = model.getPositionAt(startOffset); + + const remainingValueToReplace = valueToReplace.substr(commonPrefixLen); + const commonSuffixLen = commonSuffixLength(remainingValueToReplace, inlineCompletion.text); + const end = model.getPositionAt(Math.max(startOffset, model.getOffsetAt(inlineCompletion.range.getEndPosition()) - commonSuffixLen)); + + return { + range: Range.fromPositions(start, end), + text: inlineCompletion.text.substr(commonPrefixLen, inlineCompletion.text.length - commonPrefixLen - commonSuffixLen), + }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts b/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts deleted file mode 100644 index 3a2da4ebe0..0000000000 --- a/src/vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel.ts +++ /dev/null @@ -1,231 +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 { 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/ghostText'; -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; - private isShiftKeyPressed = false; - private currentCompletion: NormalizedInlineCompletion | undefined; - - 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(this.editor.onDidChangeCursorPosition((e) => { - if (this.isSuggestionPreviewEnabled()) { - this.minReservedLineCount = 0; - this.update(); - } - })); - - this._register(toDisposable(() => { - const suggestController = SuggestController.get(this.editor); - if (suggestController) { - suggestController.stopForceRenderingAbove(); - } - })); - - // See the command acceptAlternativeSelectedSuggestion that is bound to shift+tab - this._register(editor.onKeyDown(e => { - if (e.shiftKey && !this.isShiftKeyPressed) { - this.isShiftKeyPressed = true; - this.updateFromSuggestion(); - } - })); - this._register(editor.onKeyUp(e => { - if (e.shiftKey && this.isShiftKeyPressed) { - this.isShiftKeyPressed = false; - this.updateFromSuggestion(); - } - })); - } - - 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, - this.isShiftKeyPressed - ) - ); - } - - private setCurrentInlineCompletion(completion: NormalizedInlineCompletion | undefined): void { - this.currentCompletion = completion; - this.update(); - } - - private update(): void { - const completion = this.currentCompletion; - const mode = this.editor.getOptions().get(EditorOption.suggest).previewMode; - - this.setGhostText( - completion - ? ( - inlineCompletionToGhostText(completion, this.editor.getModel(), mode, this.editor.getPosition()) || - // Show an invisible ghost text to reserve space - new GhostText(completion.range.endLineNumber, [], this.minReservedLineCount) - ) - : undefined - ); - } - - private setGhostText(newGhostText: GhostText | undefined): void { - if (GhostText.equals(this.currentGhostText, newGhostText)) { - return; - } - - this.currentGhostText = newGhostText; - - if (this.currentGhostText && this.expanded) { - function sum(arr: number[]): number { - return arr.reduce((a, b) => a + b, 0); - } - - this.minReservedLineCount = Math.max(this.minReservedLineCount, sum(this.currentGhostText.parts.map(p => p.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, toggleMode: boolean): 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, toggleMode); - 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/suggestWidgetInlineCompletionProvider.ts b/src/vs/editor/contrib/inlineCompletions/suggestWidgetInlineCompletionProvider.ts new file mode 100644 index 0000000000..76c5e0be50 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/suggestWidgetInlineCompletionProvider.ts @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { CompletionItemInsertTextRule } from 'vs/editor/common/modes'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; +import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession'; +import { CompletionItem } from 'vs/editor/contrib/suggest/suggest'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { minimizeInlineCompletion } from './inlineCompletionsModel'; +import { NormalizedInlineCompletion, normalizedInlineCompletionsEquals } from './inlineCompletionToGhostText'; +import { compareBy, compareByNumber, findMaxBy } from './utils'; + +export interface SuggestWidgetState { + /** + * Represents the currently selected item in the suggest widget as inline completion, if possible. + */ + selectedItemAsInlineCompletion: NormalizedInlineCompletion | undefined; +} + +export class SuggestWidgetInlineCompletionProvider extends Disposable { + private isSuggestWidgetVisible: boolean = false; + private isShiftKeyPressed = false; + private _isActive = false; + private _currentInlineCompletion: NormalizedInlineCompletion | undefined = undefined; + private readonly onDidChangeEmitter = new Emitter(); + + public readonly onDidChange = this.onDidChangeEmitter.event; + + // This delay fixes a suggest widget issue when typing "." immediately restarts the suggestion session. + private readonly setInactiveDelayed = this._register(new RunOnceScheduler(() => { + if (!this.isSuggestWidgetVisible) { + if (this._isActive) { + this._isActive = false; + this.onDidChangeEmitter.fire(); + } + } + }, 100)); + + /** + * Returns undefined if the suggest widget is not active. + */ + get state(): SuggestWidgetState | undefined { + if (!this._isActive) { + return undefined; + } + return { selectedItemAsInlineCompletion: this._currentInlineCompletion }; + } + + constructor( + private readonly editor: IActiveCodeEditor, + private readonly suggestControllerPreselector: () => NormalizedInlineCompletion | undefined + ) { + super(); + + // See the command acceptAlternativeSelectedSuggestion that is bound to shift+tab + this._register(editor.onKeyDown(e => { + if (e.shiftKey && !this.isShiftKeyPressed) { + this.isShiftKeyPressed = true; + this.update(this._isActive); + } + })); + this._register(editor.onKeyUp(e => { + if (e.shiftKey && this.isShiftKeyPressed) { + this.isShiftKeyPressed = false; + this.update(this._isActive); + } + })); + + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + this._register(suggestController.registerSelector({ + priority: 100, + select: (model, pos, suggestItems) => { + const textModel = this.editor.getModel(); + const normalizedItemToPreselect = minimizeInlineCompletion(textModel, this.suggestControllerPreselector()); + if (!normalizedItemToPreselect) { + return -1; + } + const position = Position.lift(pos); + + const candidates = suggestItems + .map((suggestItem, index) => { + const inlineSuggestItem = suggestionToInlineCompletion(suggestController, position, suggestItem, this.isShiftKeyPressed); + const normalizedSuggestItem = minimizeInlineCompletion(textModel, inlineSuggestItem); + if (!normalizedSuggestItem) { + return undefined; + } + const valid = + rangeStartsWith(normalizedItemToPreselect.range, normalizedSuggestItem.range) && + normalizedItemToPreselect.text.startsWith(normalizedSuggestItem.text); + return { index, valid, prefixLength: normalizedSuggestItem.text.length, suggestItem }; + }) + .filter(item => item && item.valid); + + const result = findMaxBy( + candidates, + compareBy(s => s!.prefixLength, compareByNumber()) + ); + return result ? result.index : - 1; + } + })); + + let isBoundToSuggestWidget = false; + const bindToSuggestWidget = () => { + if (isBoundToSuggestWidget) { + return; + } + isBoundToSuggestWidget = true; + + this._register(suggestController.widget.value.onDidShow(() => { + this.isSuggestWidgetVisible = true; + this.update(true); + })); + this._register(suggestController.widget.value.onDidHide(() => { + this.isSuggestWidgetVisible = false; + this.setInactiveDelayed.schedule(); + this.update(this._isActive); + })); + this._register(suggestController.widget.value.onDidFocus(() => { + this.isSuggestWidgetVisible = true; + this.update(true); + })); + }; + + this._register(Event.once(suggestController.model.onDidTrigger)(e => { + bindToSuggestWidget(); + })); + } + this.update(this._isActive); + } + + private update(newActive: boolean): void { + const newInlineCompletion = this.getInlineCompletion(); + let shouldFire = false; + if (!normalizedInlineCompletionsEquals(this._currentInlineCompletion, newInlineCompletion)) { + this._currentInlineCompletion = newInlineCompletion; + shouldFire = true; + } + if (this._isActive !== newActive) { + this._isActive = newActive; + shouldFire = true; + } + if (shouldFire) { + this.onDidChangeEmitter.fire(); + } + } + + private getInlineCompletion(): NormalizedInlineCompletion | undefined { + const suggestController = SuggestController.get(this.editor); + if (!suggestController) { + return undefined; + } + if (!this.isSuggestWidgetVisible) { + return undefined; + } + const focusedItem = suggestController.widget.value.getFocusedItem(); + if (!focusedItem) { + return undefined; + } + + // TODO: item.isResolved + return suggestionToInlineCompletion( + suggestController, + this.editor.getPosition(), + focusedItem.item, + this.isShiftKeyPressed + ); + } + + public stopForceRenderingAbove(): void { + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + suggestController.stopForceRenderingAbove(); + } + } + + public forceRenderingAbove(): void { + const suggestController = SuggestController.get(this.editor); + if (suggestController) { + suggestController.forceRenderingAbove(); + } + } +} + +function rangeStartsWith(rangeToTest: Range, prefix: Range): boolean { + return ( + rangeToTest.startLineNumber === prefix.startLineNumber && + rangeToTest.startColumn === prefix.startColumn && + (rangeToTest.endLineNumber < prefix.endLineNumber || + (rangeToTest.endLineNumber === prefix.endLineNumber && + rangeToTest.endColumn <= prefix.endColumn)) + ); +} + +function suggestionToInlineCompletion(suggestController: SuggestController, position: Position, item: CompletionItem, toggleMode: boolean): NormalizedInlineCompletion | undefined { + // additionalTextEdits might not be resolved here, this could be problematic. + if (Array.isArray(item.completion.additionalTextEdits) && item.completion.additionalTextEdits.length > 0) { + // 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()!; + + // Ignore snippets that are too large. + // Adjust whitespace is expensive for them. + if (snippet.children.length > 100) { + return undefined; + } + + SnippetSession.adjustWhitespace(model, position, snippet, true, true); + insertText = snippet.toString(); + } + + const info = suggestController.getOverwriteInfo(item, toggleMode); + 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/suggestWidgetPreviewModel.ts b/src/vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel.ts new file mode 100644 index 0000000000..34acdca425 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { InlineCompletionTriggerKind, SelectedSuggestionInfo } from 'vs/editor/common/modes'; +import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/ghostTextModel'; +import { BaseGhostTextWidgetModel, GhostText } from './ghostText'; +import { minimizeInlineCompletion, provideInlineCompletions, UpdateOperation } from './inlineCompletionsModel'; +import { inlineCompletionToGhostText, NormalizedInlineCompletion } from './inlineCompletionToGhostText'; +import { SuggestWidgetInlineCompletionProvider } from './suggestWidgetInlineCompletionProvider'; + +export class SuggestWidgetPreviewModel extends BaseGhostTextWidgetModel { + private readonly suggestionInlineCompletionSource = this._register( + new SuggestWidgetInlineCompletionProvider( + this.editor, + // Use the first cache item (if any) as preselection. + () => this.cache.value?.completions[0]?.toLiveInlineCompletion() + ) + ); + private readonly updateOperation = this._register(new MutableDisposable()); + private readonly updateCacheSoon = this._register(new RunOnceScheduler(() => this.updateCache(), 50)); + + public override minReservedLineCount: number = 0; + + public get isActive(): boolean { + return this.suggestionInlineCompletionSource.state !== undefined; + } + + constructor( + editor: IActiveCodeEditor, + private readonly cache: SharedInlineCompletionCache, + ) { + super(editor); + + this._register(this.suggestionInlineCompletionSource.onDidChange(() => { + this.updateCacheSoon.schedule(); + + const suggestWidgetState = this.suggestionInlineCompletionSource.state; + if (!suggestWidgetState) { + this.minReservedLineCount = 0; + } + + const newGhostText = this.ghostText; + if (newGhostText) { + this.minReservedLineCount = Math.max(this.minReservedLineCount, sum(newGhostText.parts.map(p => p.lines.length - 1))); + } + + if (this.minReservedLineCount >= 1 && this.isSuggestionPreviewEnabled()) { + this.suggestionInlineCompletionSource.forceRenderingAbove(); + } else { + this.suggestionInlineCompletionSource.stopForceRenderingAbove(); + } + this.onDidChangeEmitter.fire(); + })); + + this._register(this.cache.onDidChange(() => { + this.onDidChangeEmitter.fire(); + })); + + this._register(this.editor.onDidChangeCursorPosition((e) => { + if (this.isSuggestionPreviewEnabled()) { + this.minReservedLineCount = 0; + this.updateCacheSoon.schedule(); + this.onDidChangeEmitter.fire(); + } + })); + + this._register(toDisposable(() => this.suggestionInlineCompletionSource.stopForceRenderingAbove())); + } + + private isSuggestionPreviewEnabled(): boolean { + const suggestOptions = this.editor.getOption(EditorOption.suggest); + return suggestOptions.preview; + } + + private async updateCache() { + const state = this.suggestionInlineCompletionSource.state; + if (!state || !state.selectedItemAsInlineCompletion) { + return; + } + + const info: SelectedSuggestionInfo = { + text: state.selectedItemAsInlineCompletion.text, + range: state.selectedItemAsInlineCompletion.range, + }; + + const position = this.editor.getPosition(); + + const promise = createCancelablePromise(async token => { + let result; + try { + result = await provideInlineCompletions(position, + this.editor.getModel(), + { triggerKind: InlineCompletionTriggerKind.Automatic, selectedSuggestionInfo: info }, + token + ); + } catch (e) { + onUnexpectedError(e); + return; + } + if (token.isCancellationRequested) { + return; + } + this.cache.setValue( + this.editor, + result, + InlineCompletionTriggerKind.Automatic + ); + this.onDidChangeEmitter.fire(); + }); + const operation = new UpdateOperation(promise, InlineCompletionTriggerKind.Automatic); + this.updateOperation.value = operation; + await promise; + if (this.updateOperation.value === operation) { + this.updateOperation.clear(); + } + } + + public override get ghostText(): GhostText | undefined { + if (!this.isSuggestionPreviewEnabled()) { + return undefined; + } + + const suggestWidgetState = this.suggestionInlineCompletionSource.state; + + const originalInlineCompletion = minimizeInlineCompletion(this.editor.getModel()!, suggestWidgetState?.selectedItemAsInlineCompletion); + const augmentedCompletion = minimizeInlineCompletion(this.editor.getModel()!, this.cache.value?.completions[0]?.toLiveInlineCompletion()); + + const finalCompletion = + augmentedCompletion + && originalInlineCompletion + && augmentedCompletion.text.startsWith(originalInlineCompletion.text) + && augmentedCompletion.range.equalsRange(originalInlineCompletion.range) + ? augmentedCompletion : (originalInlineCompletion || augmentedCompletion); + + const inlineCompletionPreviewLength = originalInlineCompletion ? (finalCompletion?.text.length || 0) - (originalInlineCompletion.text.length) : 0; + + const toGhostText = (completion: NormalizedInlineCompletion | undefined): GhostText | undefined => { + const mode = this.editor.getOptions().get(EditorOption.suggest).previewMode; + return completion + ? ( + inlineCompletionToGhostText(completion, this.editor.getModel(), mode, this.editor.getPosition(), inlineCompletionPreviewLength) || + // Show an invisible ghost text to reserve space + new GhostText(completion.range.endLineNumber, [], this.minReservedLineCount) + ) + : undefined; + }; + + const newGhostText = toGhostText(finalCompletion); + + return newGhostText; + } +} + +function sum(arr: number[]): number { + return arr.reduce((a, b) => a + b, 0); +} diff --git a/src/vs/editor/contrib/inlineCompletions/test/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/inlineCompletionsProvider.test.ts index f5e694935a..79d42d4159 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/inlineCompletionsProvider.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/inlineCompletionsProvider.test.ts @@ -6,15 +6,17 @@ import * as assert from 'assert'; import { timeout } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Range } from 'vs/editor/common/core/range'; -import { InlineCompletionsProvider, InlineCompletionsProviderRegistry } from 'vs/editor/common/modes'; +import { InlineCompletionsProvider, InlineCompletionsProviderRegistry, InlineCompletionTriggerKind } from 'vs/editor/common/modes'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; -import { InlineCompletionsModel, inlineCompletionToGhostText } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; +import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/ghostTextModel'; +import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; import { GhostTextContext, MockInlineCompletionsProvider } from 'vs/editor/contrib/inlineCompletions/test/utils'; import { ITestCodeEditor, TestCodeEditorCreationOptions, withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { runWithFakedTimers } from 'vs/editor/contrib/inlineCompletions/test/timeTravelScheduler'; +import { inlineCompletionToGhostText } from '../inlineCompletionToGhostText'; suite('Inline Completions', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -58,9 +60,12 @@ suite('Inline Completions', () => { assert.deepStrictEqual(getOutput('[ foo]', 'foobar'), ' foo[bar]'); assert.deepStrictEqual(getOutput('[\tfoo]', 'foobar'), '\tfoo[bar]'); assert.deepStrictEqual(getOutput('[\t foo]', '\tfoobar'), ' foo[bar]'); + assert.deepStrictEqual(getOutput('[\tfoo]', '\t\tfoobar'), { prefix: undefined, subword: '\t[\t]foo[bar]' }); assert.deepStrictEqual(getOutput('[\t]', '\t\tfoobar'), '\t[\tfoobar]'); assert.deepStrictEqual(getOutput('\t[]', '\t'), '\t[\t]'); assert.deepStrictEqual(getOutput('\t[\t]', ''), '\t\t'); + + assert.deepStrictEqual(getOutput('[ ]', 'return 1'), ' [return 1]'); }); test('Whitespace (outside of indentation)', () => { @@ -84,7 +89,7 @@ suite('Inline Completions', () => { }); }); - test('Does trigger automatically if disabled', async function () { + test('Does not trigger automatically if disabled', async function () { const provider = new MockInlineCompletionsProvider(); await withAsyncTestCodeEditorAndInlineCompletionsModel('', { fakeClock: true, provider, inlineSuggest: { enabled: false } }, @@ -110,11 +115,11 @@ suite('Inline Completions', () => { context.keyboardType('foo'); provider.setReturnValue({ text: 'foobar', range: new Range(1, 1, 1, 4) }); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Explicit); await timeout(1000); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,4)', text: 'foo', triggerKind: 0, } + { position: '(1,4)', text: 'foo', triggerKind: 1, } ]); assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'foo[bar]']); } @@ -147,17 +152,18 @@ suite('Inline Completions', () => { async ({ editor, editorViewModel, model, context }) => { model.setActive(true); - context.keyboardType('foo'); provider.setReturnValue({ text: 'foobar', range: new Range(1, 1, 1, 4) }); - model.trigger(); + context.keyboardType('foo'); + model.trigger(InlineCompletionTriggerKind.Explicit); await timeout(1000); provider.setReturnValue({ text: 'foobizz', range: new Range(1, 1, 1, 6) }); - context.keyboardType('bi'); + context.keyboardType('b'); + context.keyboardType('i'); await timeout(1000); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,4)', text: 'foo', triggerKind: 0, }, + { position: '(1,4)', text: 'foo', triggerKind: 1, }, { position: '(1,6)', text: 'foobi', triggerKind: 0, } ]); assert.deepStrictEqual( @@ -177,7 +183,7 @@ suite('Inline Completions', () => { context.keyboardType(' '); provider.setReturnValue({ text: 'foo', range: new Range(1, 2, 1, 3) }); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Explicit); await timeout(1000); assert.deepStrictEqual(context.getAndClearViewStates(), ['', ' [foo]']); @@ -185,7 +191,7 @@ suite('Inline Completions', () => { model.commitCurrentSuggestion(); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,3)', text: ' ', triggerKind: 0, }, + { position: '(1,3)', text: ' ', triggerKind: 1, }, ]); assert.deepStrictEqual(context.getAndClearViewStates(), [' foo']); @@ -202,7 +208,7 @@ suite('Inline Completions', () => { context.keyboardType('\t\t'); provider.setReturnValue({ text: 'foo', range: new Range(1, 2, 1, 3) }); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Explicit); await timeout(1000); assert.deepStrictEqual(context.getAndClearViewStates(), ['', '\t\t[foo]']); @@ -210,7 +216,7 @@ suite('Inline Completions', () => { model.commitCurrentSuggestion(); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,3)', text: '\t\t', triggerKind: 0, }, + { position: '(1,3)', text: '\t\t', triggerKind: 1, }, ]); assert.deepStrictEqual(context.getAndClearViewStates(), ['\tfoo']); @@ -227,7 +233,7 @@ suite('Inline Completions', () => { context.keyboardType('buzz '); provider.setReturnValue({ text: 'foo', range: new Range(1, 6, 1, 7) }); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Explicit); await timeout(1000); assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'buzz ']); @@ -235,7 +241,7 @@ suite('Inline Completions', () => { model.commitCurrentSuggestion(); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,7)', text: 'buzz ', triggerKind: 0, }, + { position: '(1,7)', text: 'buzz ', triggerKind: 1, }, ]); assert.deepStrictEqual(context.getAndClearViewStates(), []); @@ -252,7 +258,7 @@ suite('Inline Completions', () => { context.keyboardType('foo'); provider.setReturnValue({ text: 'foobar1', range: new Range(1, 1, 1, 4) }); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Automatic); await timeout(1000); assert.deepStrictEqual( @@ -294,7 +300,6 @@ suite('Inline Completions', () => { { position: '(1,4)', text: 'foo', triggerKind: 0, }, { position: '(1,4)', text: 'foo', triggerKind: 1, }, ]); - } ); }); @@ -305,7 +310,7 @@ suite('Inline Completions', () => { { fakeClock: true, provider }, async ({ editor, editorViewModel, model, context }) => { model.setActive(true); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Automatic); context.keyboardType('f'); await timeout(40); @@ -365,7 +370,7 @@ suite('Inline Completions', () => { provider.setReturnValue({ text: 'foobar', range: new Range(1, 1, 1, 4) }); context.keyboardType('foo'); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Automatic); await timeout(1000); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ { position: '(1,4)', text: 'foo', triggerKind: 0, } @@ -403,10 +408,10 @@ suite('Inline Completions', () => { model.setActive(true); provider.setReturnValue({ text: 'foobar', range: new Range(1, 1, 1, 4) }); context.keyboardType('foo'); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Explicit); await timeout(100); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,4)', text: 'foo', triggerKind: 0, } + { position: '(1,4)', text: 'foo', triggerKind: 1, } ]); assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'foo[bar]']); @@ -435,10 +440,10 @@ suite('Inline Completions', () => { provider.setReturnValue({ text: 'foobar', range: new Range(1, 1, 1, 6) }); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Explicit); await timeout(1000); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,6)', text: 'fooba', triggerKind: 0, } + { position: '(1,6)', text: 'fooba', triggerKind: 1, } ]); assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'fooba[r]']); @@ -449,11 +454,6 @@ suite('Inline Completions', () => { { position: '(1,5)', text: 'foob', triggerKind: 0, } ]); assert.deepStrictEqual(context.getAndClearViewStates(), [ - /* - TODO: Remove this flickering. Fortunately, it is not visible. - It is caused by the text model updating before the cursor position. - */ - 'foob', 'foob[ar]', 'foob[az]' ]); @@ -470,7 +470,7 @@ suite('Inline Completions', () => { context.keyboardType('h'); provider.setReturnValue({ text: 'helloworld', range: new Range(1, 1, 1, 2) }, 1000); - model.trigger(); + model.trigger(InlineCompletionTriggerKind.Explicit); await timeout(1030); context.keyboardType('ello'); @@ -486,6 +486,53 @@ suite('Inline Completions', () => { ]); }); }); + + test('Do not reuse cache from previous session (#132516)', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider, inlineSuggest: { enabled: true } }, + async ({ editor, editorViewModel, model, context }) => { + model.setActive(true); + context.keyboardType('hello\n'); + context.cursorLeft(); + provider.setReturnValue({ text: 'helloworld', range: new Range(1, 1, 1, 6) }, 1000); + await timeout(2000); + + assert.deepStrictEqual(provider.getAndClearCallHistory(), [ + { + position: '(1,6)', + text: 'hello\n', + triggerKind: 0, + } + ]); + + provider.setReturnValue({ text: 'helloworld', range: new Range(2, 1, 2, 6) }, 1000); + + context.cursorDown(); + context.keyboardType('hello'); + await timeout(100); + + context.cursorLeft(); // Cause the ghost text to update + context.cursorRight(); + + await timeout(2000); + + assert.deepStrictEqual(provider.getAndClearCallHistory(), [ + { + position: '(2,6)', + text: 'hello\nhello', + triggerKind: 0, + } + ]); + + assert.deepStrictEqual(context.getAndClearViewStates(), [ + '', + 'hello[world]\n', + 'hello\n', + 'hello\nhello[world]', + ]); + }); + }); }); async function withAsyncTestCodeEditorAndInlineCompletionsModel( @@ -506,11 +553,15 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel( let result: T; await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => { - const model = instantiationService.createInstance(InlineCompletionsModel, editor); + const cache = disposableStore.add(new SharedInlineCompletionCache()); + const model = instantiationService.createInstance(InlineCompletionsModel, editor, cache); const context = new GhostTextContext(model, editor); - result = await callback({ editor, editorViewModel, model, context }); - context.dispose(); - model.dispose(); + try { + result = await callback({ editor, editorViewModel, model, context }); + } finally { + context.dispose(); + model.dispose(); + } }); if (options.provider instanceof MockInlineCompletionsProvider) { diff --git a/src/vs/editor/contrib/inlineCompletions/test/suggestWidgetModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/suggestWidgetModel.test.ts index c681919259..6e2eafc0ea 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/suggestWidgetModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/suggestWidgetModel.test.ts @@ -3,30 +3,35 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SuggestWidgetAdapterModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel'; -import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { timeout } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { mock } from 'vs/base/test/common/mock'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { Range } from 'vs/editor/common/core/range'; +import { CompletionItemKind, CompletionItemProvider, CompletionProviderRegistry } from 'vs/editor/common/modes'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; +import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/ghostTextModel'; +import { SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetPreviewModel'; +import { GhostTextContext } from 'vs/editor/contrib/inlineCompletions/test/utils'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; +import { ITestCodeEditor, TestCodeEditorCreationOptions, withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; -import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { mock } from 'vs/base/test/common/mock'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; -import { IMenuService, IMenu } from 'vs/platform/actions/common/actions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { timeout } from 'vs/base/common/async'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { CompletionItemKind, CompletionItemProvider, CompletionProviderRegistry } from 'vs/editor/common/modes'; -import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; -import { TestCodeEditorCreationOptions, ITestCodeEditor, withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { Event } from 'vs/base/common/event'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import assert = require('assert'); -import { GhostTextContext } from 'vs/editor/contrib/inlineCompletions/test/utils'; -import { Range } from 'vs/editor/common/core/range'; -import { runWithFakedTimers } from 'vs/editor/contrib/inlineCompletions/test/timeTravelScheduler'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { minimizeInlineCompletion } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel'; suite('Suggest Widget Model', () => { test('Active', async () => { @@ -69,11 +74,11 @@ suite('Suggest Widget Model', () => { const suggestController = (editor.getContribution(SuggestController.ID) as SuggestController); suggestController.triggerSuggest(); await timeout(1000); - assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'h[ello]']); + assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'h', 'h[ello]']); context.keyboardType('.'); await timeout(1000); - assert.deepStrictEqual(context.getAndClearViewStates(), ['hello', 'hello.[hello]']); + assert.deepStrictEqual(context.getAndClearViewStates(), ['hello', 'hello.', 'hello.[hello]']); suggestController.cancelSuggestWidget(); @@ -82,6 +87,21 @@ suite('Suggest Widget Model', () => { } ); }); + + test('minimizeInlineCompletion', async () => { + const model = createTextModel('fun'); + const result = minimizeInlineCompletion(model, { range: new Range(1, 1, 1, 4), text: 'function' })!; + + assert.deepStrictEqual({ + range: result.range.toString(), + text: result.text + }, { + range: '[1,4 -> 1,4]', + text: 'ction' + }); + + model.dispose(); + }); }); const provider: CompletionItemProvider = { @@ -107,7 +127,7 @@ const provider: CompletionItemProvider = { async function withAsyncTestCodeEditorAndInlineCompletionsModel( text: string, options: TestCodeEditorCreationOptions & { provider?: CompletionItemProvider, fakeClock?: boolean, serviceCollection?: never }, - callback: (args: { editor: ITestCodeEditor, editorViewModel: ViewModel, model: SuggestWidgetAdapterModel, context: GhostTextContext }) => Promise + callback: (args: { editor: ITestCodeEditor, editorViewModel: ViewModel, model: SuggestWidgetPreviewModel, context: GhostTextContext }) => Promise ): Promise { await runWithFakedTimers({ useFakeTimers: options.fakeClock }, async () => { const disposableStore = new DisposableStore(); @@ -134,7 +154,9 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel( override dispose() { } }; } - }] + }], + [ILabelService, new class extends mock() { }], + [IWorkspaceContextService, new class extends mock() { }], ); if (options.provider) { @@ -145,7 +167,8 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel( await withAsyncTestCodeEditor(text, { ...options, serviceCollection }, async (editor, editorViewModel, instantiationService) => { editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController); - const model = instantiationService.createInstance(SuggestWidgetAdapterModel, editor); + const cache = disposableStore.add(new SharedInlineCompletionCache()); + const model = instantiationService.createInstance(SuggestWidgetPreviewModel, editor, cache); const context = new GhostTextContext(model, editor); await callback({ editor, editorViewModel, model, context }); model.dispose(); diff --git a/src/vs/editor/contrib/inlineCompletions/test/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/utils.ts index 0a96f6f398..377099d0b6 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/utils.ts @@ -6,10 +6,10 @@ import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; -import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import { CoreEditingCommands, CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; import { Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; -import { InlineCompletionsProvider, InlineCompletion, InlineCompletionContext } from 'vs/editor/common/modes'; +import { InlineCompletion, InlineCompletionContext, InlineCompletionsProvider } from 'vs/editor/common/modes'; import { GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostText'; import { ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; @@ -112,6 +112,26 @@ export class GhostTextContext extends Disposable { this.editor.trigger('keyboard', 'type', { text }); } + public cursorUp(): void { + CoreNavigationCommands.CursorUp.runEditorCommand(null, this.editor, null); + } + + public cursorRight(): void { + CoreNavigationCommands.CursorRight.runEditorCommand(null, this.editor, null); + } + + public cursorLeft(): void { + CoreNavigationCommands.CursorLeft.runEditorCommand(null, this.editor, null); + } + + public cursorDown(): void { + CoreNavigationCommands.CursorDown.runEditorCommand(null, this.editor, null); + } + + public cursorLineEnd(): void { + CoreNavigationCommands.CursorLineEnd.runEditorCommand(null, this.editor, null); + } + public leftDelete(): void { CoreEditingCommands.DeleteLeft.runEditorCommand(null, this.editor, null); } diff --git a/src/vs/editor/contrib/inlineCompletions/utils.ts b/src/vs/editor/contrib/inlineCompletions/utils.ts index 7e3e800a31..8b166fc313 100644 --- a/src/vs/editor/contrib/inlineCompletions/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/utils.ts @@ -11,3 +11,23 @@ export function createDisposableRef(object: T, disposable?: IDisposable): IRe dispose: () => disposable?.dispose(), }; } + +export type Comparator = (a: T, b: T) => number; + +export function compareBy(selector: (item: TItem) => TCompareBy, comparator: Comparator): Comparator { + return (a, b) => comparator(selector(a), selector(b)); +} + +export function compareByNumber(): Comparator { + return (a, b) => a - b; +} + +export function findMaxBy(items: T[], comparator: Comparator): T | undefined { + let min: T | undefined = undefined; + for (const item of items) { + if (min === undefined || comparator(item, min) > 0) { + min = item; + } + } + return min; +} diff --git a/src/vs/editor/contrib/linesOperations/copyLinesCommand.ts b/src/vs/editor/contrib/linesOperations/copyLinesCommand.ts index 70c6a408ec..88502b74ba 100644 --- a/src/vs/editor/contrib/linesOperations/copyLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/copyLinesCommand.ts @@ -5,7 +5,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; -import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; +import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; export class CopyLinesCommand implements ICommand { diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index efd2bef628..5667308cb2 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; -import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, IActionOptions, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, IActionOptions, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandThatSelectsText } from 'vs/editor/common/commands/replaceCommand'; import { TrimTrailingWhitespaceCommand } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { TypeOperations } from 'vs/editor/common/controller/cursorTypeOperations'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; @@ -21,9 +21,9 @@ import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/mod import { CopyLinesCommand } from 'vs/editor/contrib/linesOperations/copyLinesCommand'; import { MoveLinesCommand } from 'vs/editor/contrib/linesOperations/moveLinesCommand'; import { SortLinesCommand } from 'vs/editor/contrib/linesOperations/sortLinesCommand'; +import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; // copy lines @@ -282,6 +282,74 @@ export class SortLinesDescendingAction extends AbstractSortLinesAction { } } +export class DeleteDuplicateLinesAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.removeDuplicateLines', + label: nls.localize('lines.deleteDuplicates', "Delete Duplicate Lines"), + alias: 'Delete Duplicate Lines', + precondition: EditorContextKeys.writable + }); + } + + public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { + if (!editor.hasModel()) { + return; + } + + let model: ITextModel = editor.getModel(); + if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { + return; + } + + let edits: IIdentifiedSingleEditOperation[] = []; + let endCursorState: Selection[] = []; + + let linesDeleted = 0; + + for (let selection of editor.getSelections()) { + let uniqueLines = new Set(); + let lines = []; + + for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) { + let line = model.getLineContent(i); + + if (uniqueLines.has(line)) { + continue; + } + + lines.push(line); + uniqueLines.add(line); + } + + + let selectionToReplace = new Selection( + selection.startLineNumber, + 1, + selection.endLineNumber, + model.getLineMaxColumn(selection.endLineNumber) + ); + + let adjustedSelectionStart = selection.startLineNumber - linesDeleted; + let finalSelection = new Selection( + adjustedSelectionStart, + 1, + adjustedSelectionStart + lines.length - 1, + lines[lines.length - 1].length + ); + + edits.push(EditOperation.replace(selectionToReplace, lines.join('\n'))); + endCursorState.push(finalSelection); + + linesDeleted += (selection.endLineNumber - selection.startLineNumber + 1) - lines.length; + } + + editor.pushUndoStop(); + editor.executeEdits(this.id, edits, endCursorState); + editor.pushUndoStop(); + } +} + export class TrimTrailingWhitespaceAction extends EditorAction { public static readonly ID = 'editor.action.trimTrailingWhitespace'; @@ -294,7 +362,7 @@ export class TrimTrailingWhitespaceAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX), weight: KeybindingWeight.EditorContrib } }); @@ -342,7 +410,7 @@ export class DeleteLinesAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_K, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyK, weight: KeybindingWeight.EditorContrib } }); @@ -444,7 +512,7 @@ export class IndentLinesAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.US_CLOSE_SQUARE_BRACKET, + primary: KeyMod.CtrlCmd | KeyCode.BracketRight, weight: KeybindingWeight.EditorContrib } }); @@ -470,7 +538,7 @@ class OutdentLinesAction extends EditorAction { precondition: EditorContextKeys.writable, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.CtrlCmd | KeyCode.US_OPEN_SQUARE_BRACKET, + primary: KeyMod.CtrlCmd | KeyCode.BracketLeft, weight: KeybindingWeight.EditorContrib } }); @@ -662,7 +730,7 @@ export class DeleteAllRightAction extends AbstractDeleteAllToBoundaryAction { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_K, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] }, + mac: { primary: KeyMod.WinCtrl | KeyCode.KeyK, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] }, weight: KeybindingWeight.EditorContrib } }); @@ -729,7 +797,7 @@ export class JoinLinesAction extends EditorAction { kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_J }, + mac: { primary: KeyMod.WinCtrl | KeyCode.KeyJ }, weight: KeybindingWeight.EditorContrib } }); @@ -1008,43 +1076,6 @@ export class LowerCaseAction extends AbstractCaseAction { } } -export class TitleCaseAction extends AbstractCaseAction { - constructor() { - super({ - id: 'editor.action.transformToTitlecase', - label: nls.localize('editor.transformToTitlecase', "Transform to Title Case"), - alias: 'Transform to Title Case', - precondition: EditorContextKeys.writable - }); - } - - protected _modifyText(text: string, wordSeparators: string): string { - const separators = '\r\n\t ' + wordSeparators; - const excludedChars = separators.split(''); - - let title = ''; - let startUpperCase = true; - - for (let i = 0; i < text.length; i++) { - let currentChar = text[i]; - - if (excludedChars.indexOf(currentChar) >= 0) { - startUpperCase = true; - - title += currentChar; - } else if (startUpperCase) { - startUpperCase = false; - - title += currentChar.toLocaleUpperCase(); - } else { - title += currentChar.toLocaleLowerCase(); - } - } - - return title; - } -} - class BackwardsCompatibleRegExp { private _actual: RegExp | null; @@ -1075,10 +1106,35 @@ class BackwardsCompatibleRegExp { } } +export class TitleCaseAction extends AbstractCaseAction { + + public static titleBoundary = new BackwardsCompatibleRegExp('(^|[^\\p{L}\\p{N}\']|((^|\\P{L})\'))\\p{L}', 'gmu'); + + constructor() { + super({ + id: 'editor.action.transformToTitlecase', + label: nls.localize('editor.transformToTitlecase', "Transform to Title Case"), + alias: 'Transform to Title Case', + precondition: EditorContextKeys.writable + }); + } + + protected _modifyText(text: string, wordSeparators: string): string { + const titleBoundary = TitleCaseAction.titleBoundary.get(); + if (!titleBoundary) { + // cannot support this + return text; + } + return text + .toLocaleLowerCase() + .replace(titleBoundary, (b) => b.toLocaleUpperCase()); + } +} + 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'); + public static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu'); + public static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu'); constructor() { super({ @@ -1090,15 +1146,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) { + const caseBoundary = SnakeCaseAction.caseBoundary.get(); + const singleLetters = SnakeCaseAction.singleLetters.get(); + if (!caseBoundary || !singleLetters) { // cannot support this return text; } return (text - .replace(regExp1, '$1_$2') - .replace(regExp2, '$1_$2$3') + .replace(caseBoundary, '$1_$2') + .replace(singleLetters, '$1_$2$3') .toLocaleLowerCase() ); } @@ -1111,6 +1167,7 @@ registerEditorAction(MoveLinesUpAction); registerEditorAction(MoveLinesDownAction); registerEditorAction(SortLinesAscendingAction); registerEditorAction(SortLinesDescendingAction); +registerEditorAction(DeleteDuplicateLinesAction); registerEditorAction(TrimTrailingWhitespaceAction); registerEditorAction(DeleteLinesAction); registerEditorAction(IndentLinesAction); @@ -1123,8 +1180,10 @@ registerEditorAction(JoinLinesAction); registerEditorAction(TransposeAction); registerEditorAction(UpperCaseAction); registerEditorAction(LowerCaseAction); -registerEditorAction(TitleCaseAction); -if (SnakeCaseAction.regExp1.isSupported() && SnakeCaseAction.regExp2.isSupported()) { +if (SnakeCaseAction.caseBoundary.isSupported() && SnakeCaseAction.singleLetters.isSupported()) { registerEditorAction(SnakeCaseAction); } +if (TitleCaseAction.titleBoundary.isSupported()) { + registerEditorAction(TitleCaseAction); +} diff --git a/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts index 85cb6314a1..6378aee2d9 100644 --- a/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts @@ -5,6 +5,7 @@ import * as strings from 'vs/base/common/strings'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; +import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; @@ -13,7 +14,6 @@ import { CompleteEnterAction, IndentAction } from 'vs/editor/common/modes/langua import { IIndentConverter, LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/modes/supports/indentRules'; import * as indentUtils from 'vs/editor/contrib/indentation/indentUtils'; -import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; export class MoveLinesCommand implements ICommand { @@ -60,8 +60,8 @@ export class MoveLinesCommand implements ICommand { getLineTokens: (lineNumber: number) => { return model.getLineTokens(lineNumber); }, - getLanguageIdentifier: () => { - return model.getLanguageIdentifier(); + getLanguageId: () => { + return model.getLanguageId(); }, getLanguageIdAtPosition: (lineNumber: number, column: number) => { return model.getLanguageIdAtPosition(lineNumber, column); diff --git a/src/vs/editor/contrib/linesOperations/sortLinesCommand.ts b/src/vs/editor/contrib/linesOperations/sortLinesCommand.ts index 0f80cf7aec..6fc0e15d55 100644 --- a/src/vs/editor/contrib/linesOperations/sortLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/sortLinesCommand.ts @@ -6,7 +6,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; +import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; export class SortLinesCommand implements ICommand { diff --git a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts index 04e00691a8..5c016b92c6 100644 --- a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts @@ -6,9 +6,9 @@ import * as assert from 'assert'; import { Selection } from 'vs/editor/common/core/selection'; import { CopyLinesCommand } from 'vs/editor/contrib/linesOperations/copyLinesCommand'; -import { testCommand } from 'vs/editor/test/browser/testCommand'; -import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { DuplicateSelectionAction } from 'vs/editor/contrib/linesOperations/linesOperations'; +import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { testCommand } from 'vs/editor/test/browser/testCommand'; function testCopyLinesDownCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { testCommand(lines, null, selection, (sel) => new CopyLinesCommand(sel, true), expectedLines, expectedSelection); diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index 2f13f5a6a5..f016ebb615 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { TitleCaseAction, DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction, SnakeCaseAction } from 'vs/editor/contrib/linesOperations/linesOperations'; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; +import { DeleteAllLeftAction, DeleteAllRightAction, DeleteDuplicateLinesAction, DeleteLinesAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SnakeCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TitleCaseAction, TransposeAction, UpperCaseAction } from 'vs/editor/contrib/linesOperations/linesOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction } from 'vs/editor/browser/editorExtensions'; -import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; function assertSelection(editor: ICodeEditor, expected: Selection | Selection[]): void { if (!Array.isArray(expected)) { @@ -143,6 +143,67 @@ suite('Editor Contrib - Line Operations', () => { }); }); + suite('DeleteDuplicateLinesAction', () => { + test('should remove duplicate lines', function () { + withTestCodeEditor( + [ + 'alpha', + 'beta', + 'beta', + 'beta', + 'alpha', + 'omicron', + ], {}, (editor) => { + let model = editor.getModel()!; + let deleteDuplicateLinesAction = new DeleteDuplicateLinesAction(); + + editor.setSelection(new Selection(1, 3, 6, 4)); + executeAction(deleteDuplicateLinesAction, editor); + assert.deepStrictEqual(model.getLinesContent(), [ + 'alpha', + 'beta', + 'omicron', + ]); + assertSelection(editor, new Selection(1, 1, 3, 7)); + }); + }); + + test('should remove duplicate lines in multiple selections', function () { + withTestCodeEditor( + [ + 'alpha', + 'beta', + 'beta', + 'omicron', + '', + 'alpha', + 'alpha', + 'beta' + ], {}, (editor) => { + let model = editor.getModel()!; + let deleteDuplicateLinesAction = new DeleteDuplicateLinesAction(); + + editor.setSelections([new Selection(1, 2, 4, 3), new Selection(6, 2, 8, 3)]); + executeAction(deleteDuplicateLinesAction, editor); + assert.deepStrictEqual(model.getLinesContent(), [ + 'alpha', + 'beta', + 'omicron', + '', + 'alpha', + 'beta' + ]); + let expectedSelections = [ + new Selection(1, 1, 3, 7), + new Selection(5, 1, 6, 4) + ]; + editor.getSelections()!.forEach((actualSelection, index) => { + assert.deepStrictEqual(actualSelection.toString(), expectedSelections[index].toString()); + }); + }); + }); + }); + suite('DeleteAllLeftAction', () => { test('should delete to the left of the cursor', function () { @@ -687,7 +748,8 @@ suite('Editor Contrib - Line Operations', () => { 'foO[baR]BaZ', 'foO`baR~BaZ', 'foO^baR%BaZ', - 'foO$baR!BaZ' + 'foO$baR!BaZ', + '\'physician\'s assistant\'' ], {}, (editor) => { let model = editor.getModel()!; let titlecaseAction = new TitleCaseAction(); @@ -698,7 +760,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(2, 1, 2, 12)); executeAction(titlecaseAction, editor); - assert.strictEqual(model.getLineContent(2), 'Foo\'Bar\'Baz'); + assert.strictEqual(model.getLineContent(2), 'Foo\'bar\'baz'); editor.setSelection(new Selection(3, 1, 3, 12)); executeAction(titlecaseAction, editor); @@ -715,6 +777,10 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(6, 1, 6, 12)); executeAction(titlecaseAction, editor); assert.strictEqual(model.getLineContent(6), 'Foo$Bar!Baz'); + + editor.setSelection(new Selection(7, 1, 7, 23)); + executeAction(titlecaseAction, editor); + assert.strictEqual(model.getLineContent(7), '\'Physician\'s Assistant\''); } ); diff --git a/src/vs/editor/contrib/linesOperations/test/moveLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/moveLinesCommand.test.ts index 079bc6aef8..0f47ebb440 100644 --- a/src/vs/editor/contrib/linesOperations/test/moveLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/moveLinesCommand.test.ts @@ -2,14 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; import { Selection } from 'vs/editor/common/core/selection'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; import { IndentationRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { MoveLinesCommand } from 'vs/editor/contrib/linesOperations/moveLinesCommand'; import { testCommand } from 'vs/editor/test/browser/testCommand'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; -import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; function testMoveLinesDownCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { testCommand(lines, null, selection, (sel) => new MoveLinesCommand(sel, true, EditorAutoIndentStrategy.Advanced), expectedLines, expectedSelection); @@ -19,11 +18,11 @@ function testMoveLinesUpCommand(lines: string[], selection: Selection, expectedL testCommand(lines, null, selection, (sel) => new MoveLinesCommand(sel, false, EditorAutoIndentStrategy.Advanced), expectedLines, expectedSelection); } -function testMoveLinesDownWithIndentCommand(languageId: LanguageIdentifier, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { +function testMoveLinesDownWithIndentCommand(languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { testCommand(lines, languageId, selection, (sel) => new MoveLinesCommand(sel, true, EditorAutoIndentStrategy.Full), expectedLines, expectedSelection); } -function testMoveLinesUpWithIndentCommand(languageId: LanguageIdentifier, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { +function testMoveLinesUpWithIndentCommand(languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { testCommand(lines, languageId, selection, (sel) => new MoveLinesCommand(sel, false, EditorAutoIndentStrategy.Full), expectedLines, expectedSelection); } @@ -260,10 +259,10 @@ suite('Editor Contrib - Move Lines Command', () => { }); class IndentRulesMode extends MockMode { - private static readonly _id = new LanguageIdentifier('moveLinesIndentMode', 7); + private static readonly _id = 'moveLinesIndentMode'; constructor(indentationRules: IndentationRule) { super(IndentRulesMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { indentationRules: indentationRules })); } @@ -282,7 +281,7 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { let mode = new IndentRulesMode(indentRules); testMoveLinesUpWithIndentCommand( - mode.getLanguageIdentifier(), + mode.languageId, [ 'class X {', '\tz = 2', @@ -305,7 +304,7 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { let mode = new IndentRulesMode(indentRules); testMoveLinesDownWithIndentCommand( - mode.getLanguageIdentifier(), + mode.languageId, [ 'const value = 2;', 'const standardLanguageDescriptions = [', @@ -354,10 +353,10 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { }); class EnterRulesMode extends MockMode { - private static readonly _id = new LanguageIdentifier('moveLinesEnterMode', 8); + private static readonly _id = 'moveLinesEnterMode'; constructor() { super(EnterRulesMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { indentationRules: { decreaseIndentPattern: /^\s*\[$/, increaseIndentPattern: /^\s*\]$/, @@ -375,7 +374,7 @@ suite('Editor - contrib - Move Lines Command honors onEnter Rules', () => { let mode = new EnterRulesMode(); testMoveLinesDownWithIndentCommand( - mode.getLanguageIdentifier(), + mode.languageId, [ 'if (true) {', diff --git a/src/vs/editor/contrib/linkedEditing/linkedEditing.ts b/src/vs/editor/contrib/linkedEditing/linkedEditing.ts index 3872f2dd0f..368b482035 100644 --- a/src/vs/editor/contrib/linkedEditing/linkedEditing.ts +++ b/src/vs/editor/contrib/linkedEditing/linkedEditing.ts @@ -3,32 +3,32 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { registerEditorContribution, registerModelAndPositionCommand, EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import * as arrays from 'vs/base/common/arrays'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; +import { CancelablePromise, createCancelablePromise, Delayer, first } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { LinkedEditingRangeProviderRegistry, LinkedEditingRanges } from 'vs/editor/common/modes'; -import { first, createCancelablePromise, CancelablePromise, Delayer } from 'vs/base/common/async'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { ContextKeyExpr, RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { URI } from 'vs/base/common/uri'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { Color } from 'vs/base/common/color'; import { isPromiseCanceledError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, registerModelAndPositionCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { IIdentifiedSingleEditOperation, IModelDeltaDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { LinkedEditingRangeProviderRegistry, LinkedEditingRanges } from 'vs/editor/common/modes'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import * as nls from 'vs/nls'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { Color } from 'vs/base/common/color'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey('LinkedEditingInputVisible', false); @@ -120,9 +120,9 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont return; } - this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageId()); this._localToDispose.add(model.onDidChangeLanguageConfiguration(() => { - this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageId()); })); const rangeUpdateScheduler = new Delayer(this._debounceDuration); diff --git a/src/vs/editor/contrib/linkedEditing/test/linkedEditing.test..ts b/src/vs/editor/contrib/linkedEditing/test/linkedEditing.test..ts index 0d76470c5e..46882f6454 100644 --- a/src/vs/editor/contrib/linkedEditing/test/linkedEditing.test..ts +++ b/src/vs/editor/contrib/linkedEditing/test/linkedEditing.test..ts @@ -6,17 +6,17 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Handler } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; import * as modes from 'vs/editor/common/modes'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LinkedEditingContribution } from 'vs/editor/contrib/linkedEditing/linkedEditing'; import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; -import { ITextModel } from 'vs/editor/common/model'; -import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; const mockFile = URI.parse('test:somefile.ttt'); const mockFileSelector = { scheme: 'test' }; @@ -30,16 +30,16 @@ interface TestEditor { redo(): void; } -const languageIdentifier = new modes.LanguageIdentifier('linkedEditingTestLangage', 74); -LanguageConfigurationRegistry.register(languageIdentifier, { - wordPattern: /[a-zA-Z]+/ -}); +const languageId = 'linkedEditingTestLangage'; suite('linked editing', () => { const disposables = new DisposableStore(); setup(() => { disposables.clear(); + disposables.add(LanguageConfigurationRegistry.register(languageId, { + wordPattern: /[a-zA-Z]+/ + })); }); teardown(() => { @@ -48,8 +48,8 @@ suite('linked editing', () => { function createMockEditor(text: string | string[]): ITestCodeEditor { const model = typeof text === 'string' - ? createTextModel(text, undefined, languageIdentifier, mockFile) - : createTextModel(text.join('\n'), undefined, languageIdentifier, mockFile); + ? createTextModel(text, undefined, languageId, mockFile) + : createTextModel(text.join('\n'), undefined, languageId, mockFile); const editor = createTestCodeEditor({ model }); disposables.add(model); diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index d9257ec654..444c1764bf 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -3,17 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { DisposableStore, isDisposable } from 'vs/base/common/lifecycle'; +import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { ILink, LinkProvider, LinkProviderRegistry, ILinksList } from 'vs/editor/common/modes'; +import { ILink, ILinksList, LinkProvider, LinkProviderRegistry } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { isDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { coalesce } from 'vs/base/common/arrays'; -import { assertType } from 'vs/base/common/types'; export class Link implements ILink { diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index a95977dfda..a86acfce77 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -3,31 +3,31 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./links'; -import * as nls from 'vs/nls'; import * as async from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import * as platform from 'vs/base/common/platform'; +import * as resources from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./links'; import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { LinkProviderRegistry } from 'vs/editor/common/modes'; import { ClickLinkGesture, ClickLinkKeyboardEvent, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/link/clickLinkGesture'; -import { Link, getLinks, LinksList } from 'vs/editor/contrib/links/getLinks'; +import { getLinks, Link, LinksList } from 'vs/editor/contrib/links/getLinks'; +import * as nls from 'vs/nls'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import * as resources from 'vs/base/common/resources'; function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString { const executeCmd = link.url && /^command:/i.test(link.url.toString()); @@ -57,7 +57,7 @@ function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString { nativeLabel = ` "${nativeLabelText}"`; } } - const hoverMessage = new MarkdownString('', true).appendMarkdown(`[${label}](${link.url.toString(true)}${nativeLabel}) (${kb})`); + const hoverMessage = new MarkdownString('', true).appendMarkdown(`[${label}](${link.url.toString(true).replace(/ /g, '%20')}${nativeLabel}) (${kb})`); return hoverMessage; } else { return new MarkdownString().appendText(`${label} (${kb})`); diff --git a/src/vs/editor/contrib/message/messageController.ts b/src/vs/editor/contrib/message/messageController.ts index 219119f580..99492f4767 100644 --- a/src/vs/editor/contrib/message/messageController.ts +++ b/src/vs/editor/contrib/message/messageController.ts @@ -3,22 +3,22 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./messageController'; -import * as nls from 'vs/nls'; +import { alert } from 'vs/base/browser/ui/aria/aria'; import { TimeoutTimer } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { alert } from 'vs/base/browser/ui/aria/aria'; +import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import 'vs/css!./messageController'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { EditorCommand, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; -import { registerEditorContribution, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditor, IContentWidget, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; -import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IPosition } from 'vs/editor/common/core/position'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground } from 'vs/platform/theme/common/colorRegistry'; +import * as nls from 'vs/nls'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground } from 'vs/platform/theme/common/colorRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; export class MessageController implements IEditorContribution { diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index 2e175191ac..f08323452f 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -3,32 +3,32 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { status } from 'vs/base/browser/ui/aria/aria'; import { RunOnceScheduler } from 'vs/base/common/async'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { status } from 'vs/base/browser/ui/aria/aria'; +import { Constants } from 'vs/base/common/uint'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { CursorState } from 'vs/editor/common/controller/cursorCommon'; import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { CursorMoveCommands } from 'vs/editor/common/controller/cursorMoveCommands'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { Constants } from 'vs/base/common/uint'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { FindMatch, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { FindMatch, ITextModel, OverviewRulerLane, TrackedRangeStickiness, MinimapPosition } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { DocumentHighlightProviderRegistry } from 'vs/editor/common/modes'; import { CommonFindController } from 'vs/editor/contrib/find/findController'; import { FindOptionOverride, INewFindReplaceState } from 'vs/editor/contrib/find/findState'; +import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { overviewRulerSelectionHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { CursorState } from 'vs/editor/common/controller/cursorCommon'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { overviewRulerSelectionHighlightForeground, minimapSelectionOccurrenceHighlight } from 'vs/platform/theme/common/colorRegistry'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; function announceCursorChange(previousCursorState: CursorState[], cursorState: CursorState[]): void { const cursorDiff = cursorState.filter(cs => !previousCursorState.find(pcs => pcs.equals(cs))); @@ -70,7 +70,10 @@ export class InsertCursorAbove extends EditorAction { return; } - const useLogicalLine = (args && args.logicalLine === true); + let useLogicalLine = true; + if (args && args.logicalLine === false) { + useLogicalLine = false; + } const viewModel = editor._getViewModel(); if (viewModel.cursorConfig.readOnly) { @@ -120,7 +123,10 @@ export class InsertCursorBelow extends EditorAction { return; } - const useLogicalLine = (args && args.logicalLine === true); + let useLogicalLine = true; + if (args && args.logicalLine === false) { + useLogicalLine = false; + } const viewModel = editor._getViewModel(); if (viewModel.cursorConfig.readOnly) { @@ -149,7 +155,7 @@ class InsertCursorAtEndOfEachLineSelected extends EditorAction { precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_I, + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyI, weight: KeybindingWeight.EditorContrib }, menuOpts: { @@ -693,7 +699,7 @@ export class AddSelectionToNextFindMatchAction extends MultiCursorSelectionContr precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_D, + primary: KeyMod.CtrlCmd | KeyCode.KeyD, weight: KeybindingWeight.EditorContrib }, menuOpts: { @@ -738,7 +744,7 @@ export class MoveSelectionToNextFindMatchAction extends MultiCursorSelectionCont precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_D), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyD), weight: KeybindingWeight.EditorContrib } }); @@ -771,7 +777,7 @@ export class SelectHighlightsAction extends MultiCursorSelectionControllerAction precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, weight: KeybindingWeight.EditorContrib }, menuOpts: { @@ -1050,6 +1056,10 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut description: 'selection-highlight-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'selectionHighlight', + minimap: { + color: themeColorFromId(minimapSelectionOccurrenceHighlight), + position: MinimapPosition.Inline + }, overviewRuler: { color: themeColorFromId(overviewRulerSelectionHighlightForeground), position: OverviewRulerLane.Center diff --git a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts index ebefb3cdb2..3bba1237c7 100644 --- a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts @@ -16,6 +16,31 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; suite('Multicursor', () => { + + test('issue #26393: Multiple cursors + Word wrap', () => { + withTestCodeEditor([ + 'a'.repeat(20), + 'a'.repeat(20), + ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 10 }, (editor, viewModel) => { + let addCursorDownAction = new InsertCursorBelow(); + addCursorDownAction.run(null!, editor, {}); + + assert.strictEqual(viewModel.getCursorStates().length, 2); + + assert.strictEqual(viewModel.getCursorStates()[0].viewState.position.lineNumber, 1); + assert.strictEqual(viewModel.getCursorStates()[1].viewState.position.lineNumber, 3); + + editor.setPosition({ lineNumber: 4, column: 1 }); + let addCursorUpAction = new InsertCursorAbove(); + addCursorUpAction.run(null!, editor, {}); + + assert.strictEqual(viewModel.getCursorStates().length, 2); + + assert.strictEqual(viewModel.getCursorStates()[0].viewState.position.lineNumber, 4); + assert.strictEqual(viewModel.getCursorStates()[1].viewState.position.lineNumber, 2); + }); + }); + test('issue #2205: Multi-cursor pastes in reverse order', () => { withTestCodeEditor([ 'abc', diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.css b/src/vs/editor/contrib/parameterHints/parameterHints.css index 320c82ca99..93c80fc55c 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/parameterHints.css @@ -102,7 +102,6 @@ .monaco-editor .parameter-hints-widget .signature .parameter.active { font-weight: bold; - text-decoration: underline; } .monaco-editor .parameter-hints-widget .documentation-parameter > .parameter { diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.ts b/src/vs/editor/contrib/parameterHints/parameterHints.ts index 3923373059..d87a0829f1 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHints.ts @@ -3,20 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ParameterHintsWidget } from './parameterHintsWidget'; -import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import * as modes from 'vs/editor/common/modes'; import { TriggerContext } from 'vs/editor/contrib/parameterHints/parameterHintsModel'; +import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; +import * as nls from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ParameterHintsWidget } from './parameterHintsWidget'; class ParameterHintsController extends Disposable implements IEditorContribution { @@ -105,7 +105,7 @@ registerEditorCommand(new ParameterHintsCommand({ kbExpr: EditorContextKeys.focus, primary: KeyCode.UpArrow, secondary: [KeyMod.Alt | KeyCode.UpArrow], - mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.Alt | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KEY_P] } + mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.Alt | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] } } })); registerEditorCommand(new ParameterHintsCommand({ @@ -117,6 +117,6 @@ registerEditorCommand(new ParameterHintsCommand({ kbExpr: EditorContextKeys.focus, primary: KeyCode.DownArrow, secondary: [KeyMod.Alt | KeyCode.DownArrow], - mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.Alt | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KEY_N] } + mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.Alt | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] } } })); diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts b/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts index 9c8f762931..864f750483 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts @@ -8,11 +8,11 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; import * as modes from 'vs/editor/common/modes'; import { provideSignatureHelp } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; export interface TriggerContext { readonly triggerKind: modes.SignatureHelpTriggerKind; diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 406792f8d3..cdfc7dfd7d 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -6,27 +6,27 @@ import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { Codicon } from 'vs/base/common/codicons'; import { Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { assertIsDefined } from 'vs/base/common/types'; import 'vs/css!./parameterHints'; +import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as modes from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { ParameterHintsModel, TriggerContext } from 'vs/editor/contrib/parameterHints/parameterHintsModel'; import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorHoverBackground, editorHoverBorder, textCodeBlockBackground, textLinkForeground, editorHoverForeground, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ParameterHintsModel, TriggerContext } from 'vs/editor/contrib/parameterHints/parameterHintsModel'; -import { escapeRegExpCharacters } from 'vs/base/common/strings'; -import { Codicon } from 'vs/base/common/codicons'; -import { assertIsDefined } from 'vs/base/common/types'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { editorHoverBackground, editorHoverBorder, editorHoverForeground, registerColor, textCodeBlockBackground, textLinkActiveForeground, textLinkForeground, listHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; const $ = dom.$; @@ -132,6 +132,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } const fontInfo = this.editor.getOption(EditorOption.fontInfo); this.domNodes.element.style.fontSize = `${fontInfo.fontSize}px`; + this.domNodes.element.style.lineHeight = `${fontInfo.lineHeight / fontInfo.fontSize}`; }; updateFont(); @@ -383,6 +384,8 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } } +export const editorHoverWidgetHighlightForeground = registerColor('editorHoverWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.')); + registerThemingParticipant((theme, collector) => { const border = theme.getColor(editorHoverBorder); if (border) { @@ -415,4 +418,10 @@ registerThemingParticipant((theme, collector) => { if (codeBackground) { collector.addRule(`.monaco-editor .parameter-hints-widget code { background-color: ${codeBackground}; }`); } + + const parameterHighlightColor = theme.getColor(editorHoverWidgetHighlightForeground); + if (parameterHighlightColor) { + collector.addRule(`.monaco-editor .parameter-hints-widget .parameter.active { color: ${parameterHighlightColor}}`); + } + }); diff --git a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts index 39298929a7..4dc0b52096 100644 --- a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts @@ -3,16 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { URI } from 'vs/base/common/uri'; -import { assertType } from 'vs/base/common/types'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const Context = { Visible: new RawContextKey('parameterHintsVisible', false), diff --git a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts index bf0dce5c23..cf3aca0136 100644 --- a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts @@ -10,14 +10,14 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import * as modes from 'vs/editor/common/modes'; +import { ParameterHintsModel } from 'vs/editor/contrib/parameterHints/parameterHintsModel'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { ParameterHintsModel } from 'vs/editor/contrib/parameterHints/parameterHintsModel'; const mockFile = URI.parse('test:somefile.ttt'); const mockFileSelector = { scheme: 'test' }; diff --git a/src/vs/editor/contrib/peekView/media/peekViewWidget.css b/src/vs/editor/contrib/peekView/media/peekViewWidget.css index f94f28eb28..642c690dcc 100644 --- a/src/vs/editor/contrib/peekView/media/peekViewWidget.css +++ b/src/vs/editor/contrib/peekView/media/peekViewWidget.css @@ -13,10 +13,13 @@ align-items: center; font-size: 13px; margin-left: 20px; - cursor: pointer; min-width: 0; } +.monaco-editor .peekview-widget .head .peekview-title.clickable { + cursor: pointer; +} + .monaco-editor .peekview-widget .head .peekview-title .dirname:not(:empty) { font-size: 0.9em; margin-left: 0.5em; diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index f1f498f8dd..e101fd33e1 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -3,29 +3,29 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/peekViewWidget'; import * as dom from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionBar, ActionsOrientation, IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; +import 'vs/css!./media/peekViewWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IOptions, IStyles, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import * as nls from 'vs/nls'; -import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ServicesAccessor, createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { registerColor, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { Codicon } from 'vs/base/common/codicons'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { activeContrastBorder, contrastBorder, editorInfoForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; export const IPeekViewService = createDecorator('IPeekViewService'); export interface IPeekViewService { @@ -92,7 +92,9 @@ export interface IPeekViewStyles extends IStyles { secondaryHeadingColor?: Color; } -export type IPeekViewOptions = IOptions & IPeekViewStyles; +export type IPeekViewOptions = IOptions & IPeekViewStyles & { + supportOnTitleClick?: boolean; +}; const defaultOptions: IPeekViewOptions = { headerBackgroundColor: Color.white, @@ -178,8 +180,11 @@ export abstract class PeekViewWidget extends ZoneWidget { protected _fillHead(container: HTMLElement, noCloseAction?: boolean): void { const titleElement = dom.$('.peekview-title'); + if ((this.options as IPeekViewOptions).supportOnTitleClick) { + titleElement.classList.add('clickable'); + dom.addStandardDisposableListener(titleElement, 'click', event => this._onTitleClick(event)); + } dom.append(this._headElement!, titleElement); - dom.addStandardDisposableListener(titleElement, 'click', event => this._onTitleClick(event)); this._fillTitleIcon(titleElement); this._primaryHeading = dom.$('span.filename'); @@ -213,7 +218,7 @@ export abstract class PeekViewWidget extends ZoneWidget { } protected _onTitleClick(event: IMouseEvent): void { - // implement me + // implement me if supportOnTitleClick option is set } setTitle(primaryHeading: string, secondaryHeading?: string): void { @@ -271,10 +276,10 @@ export abstract class PeekViewWidget extends ZoneWidget { } -export const peekViewTitleBackground = registerColor('peekViewTitle.background', { dark: '#1E1E1E', light: '#FFFFFF', hc: '#0C141F' }, nls.localize('peekViewTitleBackground', 'Background color of the peek view title area.')); -export const peekViewTitleForeground = registerColor('peekViewTitleLabel.foreground', { dark: '#FFFFFF', light: '#333333', hc: '#FFFFFF' }, nls.localize('peekViewTitleForeground', 'Color of the peek view title.')); -export const peekViewTitleInfoForeground = registerColor('peekViewTitleDescription.foreground', { dark: '#ccccccb3', light: '#616161e6', hc: '#FFFFFF99' }, nls.localize('peekViewTitleInfoForeground', 'Color of the peek view title info.')); -export const peekViewBorder = registerColor('peekView.border', { dark: '#007acc', light: '#007acc', hc: contrastBorder }, nls.localize('peekViewBorder', 'Color of the peek view borders and arrow.')); +export const peekViewTitleBackground = registerColor('peekViewTitle.background', { dark: transparent(editorInfoForeground, .1), light: transparent(editorInfoForeground, .1), hc: null }, nls.localize('peekViewTitleBackground', 'Background color of the peek view title area.')); +export const peekViewTitleForeground = registerColor('peekViewTitleLabel.foreground', { dark: Color.white, light: Color.black, hc: Color.white }, nls.localize('peekViewTitleForeground', 'Color of the peek view title.')); +export const peekViewTitleInfoForeground = registerColor('peekViewTitleDescription.foreground', { dark: '#ccccccb3', light: '#616161', hc: '#FFFFFF99' }, nls.localize('peekViewTitleInfoForeground', 'Color of the peek view title info.')); +export const peekViewBorder = registerColor('peekView.border', { dark: editorInfoForeground, light: editorInfoForeground, hc: contrastBorder }, nls.localize('peekViewBorder', 'Color of the peek view borders and arrow.')); export const peekViewResultsBackground = registerColor('peekViewResult.background', { dark: '#252526', light: '#F3F3F3', hc: Color.black }, nls.localize('peekViewResultsBackground', 'Background color of the peek view result list.')); export const peekViewResultsMatchForeground = registerColor('peekViewResult.lineForeground', { dark: '#bbbbbb', light: '#646465', hc: Color.white }, nls.localize('peekViewResultsMatchForeground', 'Foreground color for line nodes in the peek view result list.')); diff --git a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts index bc8f80f828..298839746b 100644 --- a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AbstractCommandsQuickAccessProvider, ICommandQuickPick, ICommandsQuickAccessOptions } from 'vs/platform/quickinput/browser/commandsQuickAccess'; +import { stripIcons } from 'vs/base/common/iconLabels'; import { IEditor } from 'vs/editor/common/editorCommon'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AbstractCommandsQuickAccessProvider, ICommandQuickPick, ICommandsQuickAccessOptions } from 'vs/platform/quickinput/browser/commandsQuickAccess'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { stripIcons } from 'vs/base/common/iconLabels'; export abstract class AbstractEditorCommandsQuickAccessProvider extends AbstractCommandsQuickAccessProvider { diff --git a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts index cf68b41d3a..e976e20108 100644 --- a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts @@ -3,19 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; -import { IEditor, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration, OverviewRulerLane, ITextModel } from 'vs/editor/common/model'; -import { IRange } from 'vs/editor/common/core/range'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry'; -import { IQuickPick, IQuickPickItem, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDisposable, DisposableStore, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; -import { isDiffEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { withNullAsUndefined } from 'vs/base/common/types'; import { once } from 'vs/base/common/functional'; +import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { getCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IRange } from 'vs/editor/common/core/range'; +import { IDiffEditor, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel, OverviewRulerLane } from 'vs/editor/common/model'; +import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry'; +import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IKeyMods, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; interface IEditorLineDecoration { rangeHighlightId: string; diff --git a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts index eb46723b9c..81048b41c3 100644 --- a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts @@ -3,16 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IEditor, ScrollType } from 'vs/editor/common/editorCommon'; -import { IRange } from 'vs/editor/common/core/range'; -import { AbstractEditorNavigationQuickAccessProvider, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; -import { IPosition } from 'vs/editor/common/core/position'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IRange } from 'vs/editor/common/core/range'; +import { IEditor, ScrollType } from 'vs/editor/common/editorCommon'; +import { AbstractEditorNavigationQuickAccessProvider, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; +import { localize } from 'vs/nls'; +import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; interface IGotoLineQuickPickItem extends IQuickPickItem, Partial { } diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index 0f57a5fafb..69d9f9793a 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -3,20 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { DisposableStore, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Codicon } from 'vs/base/common/codicons'; +import { IMatch } from 'vs/base/common/filters'; +import { IPreparedQuery, pieceToQuery, prepareQuery, scoreFuzzy2 } from 'vs/base/common/fuzzyScorer'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { format, trim } from 'vs/base/common/strings'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; -import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes'; +import { DocumentSymbol, DocumentSymbolProviderRegistry, SymbolKind, SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; -import { trim, format } from 'vs/base/common/strings'; -import { prepareQuery, IPreparedQuery, pieceToQuery, scoreFuzzy2 } from 'vs/base/common/fuzzyScorer'; -import { IMatch } from 'vs/base/common/filters'; -import { Codicon } from 'vs/base/common/codicons'; +import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; +import { localize } from 'vs/nls'; +import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; export interface IGotoSymbolQuickPickItem extends IQuickPickItem { kind: SymbolKind, diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 5757041bda..c50821729e 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.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 { onUnexpectedError } from 'vs/base/common/errors'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IEditorProgressService } from 'vs/platform/progress/common/progress'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand, registerModelAndPositionCommand } from 'vs/editor/browser/editorExtensions'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { RenameInputField, CONTEXT_RENAME_INPUT_VISIBLE } from './renameInputField'; -import { WorkspaceEdit, RenameProviderRegistry, RenameProvider, RenameLocation, Rejection } from 'vs/editor/common/modes'; -import { Position, IPosition } from 'vs/editor/common/core/position'; import { alert } from 'vs/base/browser/ui/aria/aria'; -import { Range } from 'vs/editor/common/core/range'; -import { MessageController } from 'vs/editor/contrib/message/messageController'; -import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/browser/core/editorState'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; -import { URI } from 'vs/base/common/uri'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { IdleValue, raceCancellation } from 'vs/base/common/async'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, ConfigurationScope, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/browser/core/editorState'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, registerModelAndPositionCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITextModel } from 'vs/editor/common/model'; +import { Rejection, RenameLocation, RenameProvider, RenameProviderRegistry, WorkspaceEdit } from 'vs/editor/common/modes'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { MessageController } from 'vs/editor/contrib/message/messageController'; +import * as nls from 'vs/nls'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { CONTEXT_RENAME_INPUT_VISIBLE, RenameInputField } from './renameInputField'; class RenameSkeleton { @@ -226,6 +226,9 @@ class RenameController implements IEditorContribution { return; } + // collapse selection to active end + this.editor.setSelection(Range.fromPositions(this.editor.getSelection().getPosition())); + this._bulkEditService.apply(ResourceEdit.convert(renameResult), { editor: this.editor, showPreview: inputFieldResult.wantsPreview, @@ -357,6 +360,15 @@ registerModelAndPositionCommand('_executeDocumentRenameProvider', function (mode return rename(model, position, newName); }); +registerModelAndPositionCommand('_executePrepareRename', async function (model, position) { + const skeleton = new RenameSkeleton(model, position); + const loc = await skeleton.resolveRenameLocation(CancellationToken.None); + if (loc?.rejectReason) { + throw new Error(loc.rejectReason); + } + return loc; +}); + //todo@jrieken use editor options world Registry.as(Extensions.Configuration).registerConfiguration({ diff --git a/src/vs/editor/contrib/rename/renameInputField.ts b/src/vs/editor/contrib/rename/renameInputField.ts index ee419deb01..acca2a0673 100644 --- a/src/vs/editor/contrib/rename/renameInputField.ts +++ b/src/vs/editor/contrib/rename/renameInputField.ts @@ -3,19 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./renameInputField'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import 'vs/css!./renameInputField'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { inputBackground, inputBorder, inputForeground, widgetShadow, editorWidgetBackground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { editorWidgetBackground, inputBackground, inputBorder, inputForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false, localize('renameInputVisible', "Whether the rename input widget is visible")); diff --git a/src/vs/editor/contrib/smartSelect/bracketSelections.ts b/src/vs/editor/contrib/smartSelect/bracketSelections.ts index 0f55b3fb4d..4d72d7fb41 100644 --- a/src/vs/editor/contrib/smartSelect/bracketSelections.ts +++ b/src/vs/editor/contrib/smartSelect/bracketSelections.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SelectionRangeProvider, SelectionRange } from 'vs/editor/common/modes'; -import { ITextModel } from 'vs/editor/common/model'; +import { LinkedList } from 'vs/base/common/linkedList'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { LinkedList } from 'vs/base/common/linkedList'; +import { ITextModel } from 'vs/editor/common/model'; +import { SelectionRange, SelectionRangeProvider } from 'vs/editor/common/modes'; export class BracketSelectionRangeProvider implements SelectionRangeProvider { @@ -41,7 +41,7 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { resolve(); break; } - let bracket = model.findNextBracket(pos); + let bracket = model.bracketPairs.findNextBracket(pos); if (!bracket) { resolve(); break; @@ -86,7 +86,7 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { resolve(); break; } - let bracket = model.findPrevBracket(pos); + let bracket = model.bracketPairs.findPrevBracket(pos); if (!bracket) { resolve(); break; diff --git a/src/vs/editor/contrib/smartSelect/smartSelect.ts b/src/vs/editor/contrib/smartSelect/smartSelect.ts index 391604da6d..ebb7284f23 100644 --- a/src/vs/editor/contrib/smartSelect/smartSelect.ts +++ b/src/vs/editor/contrib/smartSelect/smartSelect.ts @@ -5,9 +5,12 @@ import * as arrays from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor, registerModelCommand } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, IActionOptions, registerEditorAction, registerEditorContribution, registerModelCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +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 { Selection } from 'vs/editor/common/core/selection'; @@ -15,15 +18,12 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; +import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bracketSelections'; +import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections'; import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections'; -import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bracketSelections'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; class SelectionRanges { diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index a58c5f142d..9873b22542 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -3,34 +3,29 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { Range, IRange } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; -import { LanguageIdentifier, SelectionRangeProvider, SelectionRangeRegistry } from 'vs/editor/common/modes'; -import { MockMode, StaticLanguageSelector } from 'vs/editor/test/common/mocks/mockMode'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { SelectionRangeProvider, SelectionRangeRegistry } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; +import { IModelService } from 'vs/editor/common/services/modelService'; import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bracketSelections'; import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/smartSelect'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections'; -import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; -import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { createModelServices } from 'vs/editor/test/common/editorTestUtils'; +import { MockMode, StaticLanguageSelector } from 'vs/editor/test/common/mocks/mockMode'; +import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; class MockJSMode extends MockMode { - private static readonly _id = new LanguageIdentifier('mockJSMode', 3); + private static readonly _id = 'mockJSMode'; constructor() { super(MockJSMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { brackets: [ ['(', ')'], ['{', '}'], @@ -55,24 +50,25 @@ suite('SmartSelect', () => { BracketSelectionRangeProvider._maxDuration = OriginalBracketSelectionRangeProviderMaxDuration; }); - let modelService: ModelServiceImpl; + let disposables: DisposableStore; + let modelService: IModelService; let mode: MockJSMode; setup(() => { - const configurationService = new TestConfigurationService(); - const dialogService = new TestDialogService(); - modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService())); - mode = new MockJSMode(); + const [instantiationService, _disposables] = createModelServices(); + modelService = instantiationService.invokeFunction((accessor) => accessor.get(IModelService)); + disposables = _disposables; + mode = disposables.add(new MockJSMode()); }); teardown(() => { - modelService.dispose(); mode.dispose(); + disposables.dispose(); }); async function assertGetRangesToPosition(text: string[], lineNumber: number, column: number, ranges: Range[], selectLeadingAndTrailingWhitespace = true): Promise { let uri = URI.file('test.js'); - let model = modelService.createModel(text.join('\n'), new StaticLanguageSelector(mode.getLanguageIdentifier()), uri); + let model = modelService.createModel(text.join('\n'), new StaticLanguageSelector(mode.languageId), uri); let [actual] = await provideSelectionRanges(model, [new Position(lineNumber, column)], { selectLeadingAndTrailingWhitespace }, CancellationToken.None); let actualStr = actual!.map(r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn).toString()); let desiredStr = ranges.reverse().map(r => String(r)); @@ -219,7 +215,7 @@ suite('SmartSelect', () => { let index = value.indexOf('|'); value = value.replace('|', ''); - let model = modelService.createModel(value, new StaticLanguageSelector(mode.getLanguageIdentifier()), URI.parse('fake:lang')); + let model = modelService.createModel(value, new StaticLanguageSelector(mode.languageId), URI.parse('fake:lang')); let pos = model.getPositionAt(index); let all = await provider.provideSelectionRanges(model, [pos], CancellationToken.None); let ranges = all![0]; diff --git a/src/vs/editor/contrib/smartSelect/wordSelections.ts b/src/vs/editor/contrib/smartSelect/wordSelections.ts index 938a58e445..3f4f59887b 100644 --- a/src/vs/editor/contrib/smartSelect/wordSelections.ts +++ b/src/vs/editor/contrib/smartSelect/wordSelections.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SelectionRangeProvider, SelectionRange } from 'vs/editor/common/modes'; -import { ITextModel } from 'vs/editor/common/model'; +import { CharCode } from 'vs/base/common/charCode'; +import { isLowerAsciiLetter, isUpperAsciiLetter } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { CharCode } from 'vs/base/common/charCode'; -import { isUpperAsciiLetter, isLowerAsciiLetter } from 'vs/base/common/strings'; +import { ITextModel } from 'vs/editor/common/model'; +import { SelectionRange, SelectionRangeProvider } from 'vs/editor/common/modes'; export class WordSelectionRangeProvider implements SelectionRangeProvider { diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index fde537e9ba..b885a66c84 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -14,12 +14,12 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CompletionItem, CompletionItemKind } from 'vs/editor/common/modes'; import { Choice } from 'vs/editor/contrib/snippet/snippetParser'; import { showSimpleSuggestions } from 'vs/editor/contrib/suggest/suggest'; +import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; +import { localize } from 'vs/nls'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; import { SnippetSession } from './snippetSession'; -import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; -import { localize } from 'vs/nls'; export interface ISnippetInsertOptions { overwriteBefore: number; diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 0ec95eca75..44e09a98b0 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -4,27 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import { groupBy } from 'vs/base/common/arrays'; +import { CharCode } from 'vs/base/common/charCode'; import { dispose } from 'vs/base/common/lifecycle'; import { getLeadingWhitespace } from 'vs/base/common/strings'; +import { withNullAsUndefined } from 'vs/base/common/types'; import 'vs/css!./snippetSession'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; -import { Choice, Placeholder, SnippetParser, Text, TextmateSnippet, Marker } from './snippetParser'; -import { ClipboardBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, CommentBasedVariableResolver, WorkspaceBasedVariableResolver, RandomBasedVariableResolver } from './snippetVariables'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import * as colors from 'vs/platform/theme/common/colorRegistry'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; -import { CharCode } from 'vs/base/common/charCode'; +import { ILabelService } from 'vs/platform/label/common/label'; +import * as colors from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { Choice, Marker, Placeholder, SnippetParser, Text, TextmateSnippet } from './snippetParser'; +import { ClipboardBasedVariableResolver, CommentBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, RandomBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from './snippetVariables'; registerThemingParticipant((theme, collector) => { @@ -415,8 +414,8 @@ export class SnippetSession { } const model = editor.getModel(); - const workspaceService = editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService, optional)); - const modelBasedVariableResolver = editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService, optional), model)); + const workspaceService = editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService)); + const modelBasedVariableResolver = editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model)); const readClipboardText = () => clipboardText; let delta = 0; diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index 2dee52a7d2..f304941bab 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -3,20 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; import * as path from 'vs/base/common/path'; import { dirname } from 'vs/base/common/resources'; -import { ITextModel } from 'vs/editor/common/model'; -import { Selection } from 'vs/editor/common/core/selection'; -import { VariableResolver, Variable, Text } from 'vs/editor/contrib/snippet/snippetParser'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { getLeadingWhitespace, commonPrefixLength, isFalsyOrWhitespace, splitLines } from 'vs/base/common/strings'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { toWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; +import { commonPrefixLength, getLeadingWhitespace, isFalsyOrWhitespace, splitLines } from 'vs/base/common/strings'; import { generateUuid } from 'vs/base/common/uuid'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ITextModel } from 'vs/editor/common/model'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { Text, Variable, VariableResolver } from 'vs/editor/contrib/snippet/snippetParser'; +import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; +import * as nls from 'vs/nls'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze({ 'CURRENT_YEAR': true, @@ -149,7 +149,7 @@ export class SelectionBasedVariableResolver implements VariableResolver { export class ModelBasedVariableResolver implements VariableResolver { constructor( - private readonly _labelService: ILabelService | undefined, + private readonly _labelService: ILabelService, private readonly _model: ITextModel ) { // @@ -171,15 +171,15 @@ export class ModelBasedVariableResolver implements VariableResolver { return name.slice(0, idx); } - } else if (name === 'TM_DIRECTORY' && this._labelService) { + } else if (name === 'TM_DIRECTORY') { if (path.dirname(this._model.uri.fsPath) === '.') { return ''; } return this._labelService.getUriLabel(dirname(this._model.uri)); - } else if (name === 'TM_FILEPATH' && this._labelService) { + } else if (name === 'TM_FILEPATH') { return this._labelService.getUriLabel(this._model.uri); - } else if (name === 'RELATIVE_FILEPATH' && this._labelService) { + } else if (name === 'RELATIVE_FILEPATH') { return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true }); } diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts index eaac273466..1779c39763 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts @@ -3,13 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { mock } from 'vs/base/test/common/mock'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { ITestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILabelService } from 'vs/platform/label/common/label'; import { NullLogService } from 'vs/platform/log/common/log'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; class TestSnippetController extends SnippetController2 { @@ -40,7 +44,12 @@ suite('SnippetController', () => { ]; } - withTestCodeEditor(lines, {}, (editor) => { + const serviceCollection = new ServiceCollection( + [ILabelService, new class extends mock() { }], + [IWorkspaceContextService, new class extends mock() { }], + ); + + withTestCodeEditor(lines, { serviceCollection }, (editor) => { editor.getModel()!.updateOptions({ insertSpaces: false }); diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.test.ts index c301d17949..bc5b4f5694 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.test.ts @@ -3,16 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { mock } from 'vs/base/test/common/mock'; +import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; +import { Handler } from 'vs/editor/common/editorCommon'; +import { TextModel } from 'vs/editor/common/model/textModel'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { TextModel } from 'vs/editor/common/model/textModel'; -import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { Handler } from 'vs/editor/common/editorCommon'; -import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; suite('SnippetController2', function () { @@ -38,7 +42,11 @@ suite('SnippetController2', function () { setup(function () { contextKeys = new MockContextKeyService(); model = createTextModel('if\n $state\nfi'); - editor = createTestCodeEditor({ model: model }); + const serviceCollection = new ServiceCollection( + [ILabelService, new class extends mock() { }], + [IWorkspaceContextService, new class extends mock() { }], + ); + editor = createTestCodeEditor({ model, serviceCollection }); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); assert.strictEqual(model.getEOL(), '\n'); }); @@ -455,4 +463,32 @@ suite('SnippetController2', function () { ctrl.insert('\tHello World\n\tNew Line\n${1:\tmore}'); assert.strictEqual(model.getValue(), ' Hello World\n New Line\n more'); }); + + test.skip('Snippet transformation does not work after inserting variable using intellisense, #112362', function () { + + { + // HAPPY - no nested snippet + const ctrl = new SnippetController2(editor, logService, contextKeys); + model.setValue(''); + model.updateOptions({ insertSpaces: true, tabSize: 4 }); + ctrl.insert('$1\n\n${1/([A-Za-z0-9]+): ([A-Za-z]+).*/$1: \'$2\',/gm}'); + + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(3, 1, 3, 1)); + editor.trigger('test', 'type', { text: 'foo: number;' }); + ctrl.next(); + assert.strictEqual(model.getValue(), `foo: number;\n\nfoo: 'number',`); + } + + const ctrl = new SnippetController2(editor, logService, contextKeys); + model.setValue(''); + model.updateOptions({ insertSpaces: true, tabSize: 4 }); + ctrl.insert('$1\n\n${1/([A-Za-z0-9]+): ([A-Za-z]+).*/$1: \'$2\',/gm}'); + + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(3, 1, 3, 1)); + editor.trigger('test', 'type', { text: 'foo: ' }); + ctrl.insert('number;'); + ctrl.next(); + assert.strictEqual(model.getValue(), `foo: number;\n\nfoo: 'number',`); + // editor.trigger('test', 'type', { text: ';' }); + }); }); diff --git a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts index f8ae2972ab..8be9a1325e 100644 --- a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetParser.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 { Scanner, TokenType, SnippetParser, Text, Placeholder, Variable, Marker, TextmateSnippet, Choice, FormatString, Transform } from 'vs/editor/contrib/snippet/snippetParser'; +import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from 'vs/editor/contrib/snippet/snippetParser'; suite('SnippetParser', () => { diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index 4e42b3bbc5..3faa4af2d5 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { mock } from 'vs/base/test/common/mock'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -12,6 +13,9 @@ import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; suite('SnippetSession', function () { @@ -28,7 +32,11 @@ suite('SnippetSession', function () { setup(function () { model = createTextModel('function foo() {\n console.log(a);\n}'); - editor = createTestCodeEditor({ model: model }) as IActiveCodeEditor; + const serviceCollection = new ServiceCollection( + [ILabelService, new class extends mock() { }], + [IWorkspaceContextService, new class extends mock() { }], + ); + editor = createTestCodeEditor({ model, serviceCollection }) as IActiveCodeEditor; editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); assert.strictEqual(model.getEOL(), '\n'); }); diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index ea069229d5..dbb04ea113 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -3,21 +3,22 @@ * 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 { Selection } from 'vs/editor/common/core/selection'; -import { SelectionBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, ClipboardBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from 'vs/editor/contrib/snippet/snippetVariables'; -import { SnippetParser, Variable, VariableResolver } from 'vs/editor/contrib/snippet/snippetParser'; -import { TextModel } from 'vs/editor/common/model/textModel'; -import { IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { mock } from 'vs/base/test/common/mock'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; -import { sep } from 'vs/base/common/path'; -import { toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; import * as sinon from 'sinon'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { sep } from 'vs/base/common/path'; +import { isWindows } from 'vs/base/common/platform'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; +import { Selection } from 'vs/editor/common/core/selection'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { SnippetParser, Variable, VariableResolver } from 'vs/editor/contrib/snippet/snippetParser'; +import { ClipboardBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from 'vs/editor/contrib/snippet/snippetVariables'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; suite('Snippet Variables Resolver', function () { @@ -65,6 +66,8 @@ suite('Snippet Variables Resolver', function () { test('editor variables, file/dir', function () { + const disposables = new DisposableStore(); + assertVariableResolve(resolver, 'TM_FILENAME', 'text.txt'); if (!isWindows) { assertVariableResolve(resolver, 'TM_DIRECTORY', '/foo/files'); @@ -73,7 +76,7 @@ suite('Snippet Variables Resolver', function () { resolver = new ModelBasedVariableResolver( labelService, - createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')) + disposables.add(createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi'))) ); assertVariableResolve(resolver, 'TM_FILENAME', 'ghi'); if (!isWindows) { @@ -83,11 +86,12 @@ suite('Snippet Variables Resolver', function () { resolver = new ModelBasedVariableResolver( labelService, - createTextModel('', undefined, undefined, URI.parse('mem:fff.ts')) + disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:fff.ts'))) ); assertVariableResolve(resolver, 'TM_DIRECTORY', ''); assertVariableResolve(resolver, 'TM_FILEPATH', 'fff.ts'); + disposables.dispose(); }); test('Path delimiters in code snippet variables aren\'t specific to remote OS #76840', function () { @@ -103,6 +107,8 @@ suite('Snippet Variables Resolver', function () { const resolver = new CompositeSnippetVariableResolver([new ModelBasedVariableResolver(labelService, model)]); assertVariableResolve(resolver, 'TM_FILEPATH', '|foo|files|text.txt'); + + model.dispose(); }); test('editor variables, selection', function () { @@ -146,25 +152,29 @@ suite('Snippet Variables Resolver', function () { test('More useful environment variables for snippets, #32737', function () { + const disposables = new DisposableStore(); + assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'text'); resolver = new ModelBasedVariableResolver( labelService, - createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')) + disposables.add(createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi'))) ); assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'ghi'); resolver = new ModelBasedVariableResolver( labelService, - createTextModel('', undefined, undefined, URI.parse('mem:.git')) + disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:.git'))) ); assertVariableResolve(resolver, 'TM_FILENAME_BASE', '.git'); resolver = new ModelBasedVariableResolver( labelService, - createTextModel('', undefined, undefined, URI.parse('mem:foo.')) + disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:foo.'))) ); assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'foo'); + + disposables.dispose(); }); @@ -417,5 +427,7 @@ suite('Snippet Variables Resolver', function () { } else { assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files\\text.txt'); } + + model.dispose(); }); }); diff --git a/src/vs/editor/contrib/suggest/completionModel.ts b/src/vs/editor/contrib/suggest/completionModel.ts index 339d67eb88..1b83df02d7 100644 --- a/src/vs/editor/contrib/suggest/completionModel.ts +++ b/src/vs/editor/contrib/suggest/completionModel.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer, FuzzyScore, anyScore } from 'vs/base/common/filters'; -import { CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes'; -import { CompletionItem } from './suggest'; -import { InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; -import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; -import { CharCode } from 'vs/base/common/charCode'; -import { compareIgnoreCase } from 'vs/base/common/strings'; import { quickSelect } from 'vs/base/common/arrays'; +import { CharCode } from 'vs/base/common/charCode'; +import { anyScore, fuzzyScore, FuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer } from 'vs/base/common/filters'; +import { compareIgnoreCase } from 'vs/base/common/strings'; +import { InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; +import { CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/modes'; +import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; +import { CompletionItem } from './suggest'; type StrictCompletionItem = Required; diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 3074dd59fe..b2901d7a15 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -394,6 +394,10 @@ margin-bottom: 0; } +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs .monaco-tokenized-source { + white-space: pre; +} + .monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs .code { white-space: pre-wrap; word-wrap: break-word; diff --git a/src/vs/editor/contrib/suggest/resizable.ts b/src/vs/editor/contrib/suggest/resizable.ts index bdba480a8f..265cd9856a 100644 --- a/src/vs/editor/contrib/suggest/resizable.ts +++ b/src/vs/editor/contrib/suggest/resizable.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { Dimension } from 'vs/base/browser/dom'; import { Orientation, OrthogonalEdge, Sash, SashState } from 'vs/base/browser/ui/sash/sash'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export interface IResizeEvent { diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index a7e2984c23..cf08f8d32b 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -3,25 +3,25 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { onUnexpectedExternalError, canceled, isPromiseCanceledError } from 'vs/base/common/errors'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { canceled, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { DisposableStore, IDisposable, isDisposable } from 'vs/base/common/lifecycle'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Range } from 'vs/editor/common/core/range'; -import { FuzzyScore } from 'vs/base/common/filters'; -import { isDisposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; -import { StopWatch } from 'vs/base/common/stopwatch'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { assertType } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const Context = { Visible: new RawContextKey('suggestWidgetVisible', false, localize('suggestWidgetVisible', "Whether suggestion are visible")), @@ -412,3 +412,16 @@ export function showSimpleSuggestions(editor: ICodeEditor, suggestions: modes.Co editor.getContribution('editor.contrib.suggestController').triggerSuggest(new Set().add(_provider)); }, 0); } + +export interface ISuggestItemPreselector { + /** + * The preselector with highest priority is asked first. + */ + readonly priority: number; + + /** + * Is called to preselect a suggest item. + * When -1 is returned, item preselectors with lower priority are asked. + */ + select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number | -1; +} diff --git a/src/vs/editor/contrib/suggest/suggestCommitCharacters.ts b/src/vs/editor/contrib/suggest/suggestCommitCharacters.ts index dd75bf641f..ff0c9f593e 100644 --- a/src/vs/editor/contrib/suggest/suggestCommitCharacters.ts +++ b/src/vs/editor/contrib/suggest/suggestCommitCharacters.ts @@ -6,9 +6,9 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ISelectedSuggestion, SuggestWidget } from './suggestWidget'; -import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; +import { ISelectedSuggestion, SuggestWidget } from './suggestWidget'; export class CommitCharacterController { diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 390f753b46..39a1f9d165 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -5,43 +5,44 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { IdleValue } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { KeyCode, KeyMod, SimpleKeybinding } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable, DisposableStore, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { SimpleKeybinding } from 'vs/base/common/keybindings'; +import { DisposableStore, dispose, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import * as platform from 'vs/base/common/platform'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { assertType, isObject } from 'vs/base/common/types'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import { 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 { EditOperation } from 'vs/editor/common/core/editOperation'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { CompletionItemProvider, CompletionItemInsertTextRule } from 'vs/editor/common/modes'; +import { ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { CompletionItemInsertTextRule, CompletionItemProvider } from 'vs/editor/common/modes'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; +import { WordContextKey } from 'vs/editor/contrib/suggest/wordContextKey'; import * as nls from 'vs/nls'; -import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { Context as SuggestContext, CompletionItem, suggestWidgetStatusbarMenu } from './suggest'; -import { SuggestAlternatives } from './suggestAlternatives'; -import { State, SuggestModel } from './suggestModel'; -import { ISelectedSuggestion, SuggestWidget } from './suggestWidget'; -import { WordContextKey } from 'vs/editor/contrib/suggest/wordContextKey'; -import { Event } from 'vs/base/common/event'; -import { IdleValue } from 'vs/base/common/async'; -import { isObject, assertType } from 'vs/base/common/types'; -import { CommitCharacterController } from './suggestCommitCharacters'; -import { OvertypingCapturer } from './suggestOvertypingCapturer'; -import { IPosition, Position } from 'vs/editor/common/core/position'; -import { TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import * as platform from 'vs/base/common/platform'; -import { MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; -import { StopWatch } from 'vs/base/common/stopwatch'; +import { CompletionItem, Context as SuggestContext, ISuggestItemPreselector, suggestWidgetStatusbarMenu } from './suggest'; +import { SuggestAlternatives } from './suggestAlternatives'; +import { CommitCharacterController } from './suggestCommitCharacters'; +import { State, SuggestModel } from './suggestModel'; +import { OvertypingCapturer } from './suggestOvertypingCapturer'; +import { ISelectedSuggestion, SuggestWidget } from './suggestWidget'; // sticky suggest widget which doesn't disappear on focus out and such let _sticky = false; @@ -112,6 +113,7 @@ export class SuggestController implements IEditorContribution { private readonly _lineSuffix = new MutableDisposable(); private readonly _toDispose = new DisposableStore(); private readonly _overtypingCapturer: IdleValue; + private readonly _selectors = new PriorityRegistry(s => s.priority); constructor( editor: ICodeEditor, @@ -166,7 +168,6 @@ export class SuggestController implements IEditorContribution { if ( this.editor.getOption(EditorOption.acceptSuggestionOnEnter) === 'smart' && this.model.state === State.Auto - && !item.completion.command && !item.completion.additionalTextEdits && !(item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) && endColumn - startColumn === item.completion.insertText.length @@ -191,8 +192,8 @@ export class SuggestController implements IEditorContribution { this._toDispose.add(widget.onDetailsKeyDown(e => { // cmd + c on macOS, ctrl + c on Win / Linux if ( - e.toKeybinding().equals(new SimpleKeybinding(true, false, false, false, KeyCode.KEY_C)) || - (platform.isMacintosh && e.toKeybinding().equals(new SimpleKeybinding(false, false, false, true, KeyCode.KEY_C))) + e.toKeybinding().equals(new SimpleKeybinding(true, false, false, false, KeyCode.KeyC)) || + (platform.isMacintosh && e.toKeybinding().equals(new SimpleKeybinding(false, false, false, true, KeyCode.KeyC))) ) { e.stopPropagation(); return; @@ -223,7 +224,16 @@ export class SuggestController implements IEditorContribution { })); this._toDispose.add(this.model.onDidSuggest(e => { if (!e.shy) { - let index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items); + let index = -1; + for (const selector of this._selectors.itemsOrderedByPriorityDesc) { + index = selector.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items); + if (index !== -1) { + break; + } + } + if (index === -1) { + index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items); + } this.widget.value.showSuggestions(e.completionModel, index, e.isFrozen, e.auto); } })); @@ -447,10 +457,10 @@ export class SuggestController implements IEditorContribution { } } - triggerSuggest(onlyFrom?: Set): void { + triggerSuggest(onlyFrom?: Set, auto?: boolean): void { if (this.editor.hasModel()) { - this.model.trigger({ auto: false, shy: false }, false, onlyFrom); - this.editor.revealLine(this.editor.getPosition().lineNumber, ScrollType.Smooth); + this.model.trigger({ auto: auto ?? false, shy: false }, false, onlyFrom); + this.editor.revealPosition(this.editor.getPosition(), ScrollType.Smooth); this.editor.focus(); } } @@ -519,7 +529,7 @@ export class SuggestController implements IEditorContribution { }); this.model.trigger({ auto: false, shy: true }); - this.editor.revealLine(positionNow.lineNumber, ScrollType.Smooth); + this.editor.revealPosition(positionNow, ScrollType.Smooth); this.editor.focus(); } @@ -599,6 +609,37 @@ export class SuggestController implements IEditorContribution { } this.widget.value.stopForceRenderingAbove(); } + + registerSelector(selector: ISuggestItemPreselector): IDisposable { + return this._selectors.register(selector); + } +} + +class PriorityRegistry { + private readonly _items = new Array(); + + constructor(private readonly prioritySelector: (item: T) => number) { } + + register(value: T): IDisposable { + if (this._items.indexOf(value) !== -1) { + throw new Error('Value is already registered'); + } + this._items.push(value); + this._items.sort((s1, s2) => this.prioritySelector(s2) - this.prioritySelector(s1)); + + return { + dispose: () => { + const idx = this._items.indexOf(value); + if (idx >= 0) { + this._items.splice(idx, 1); + } + } + }; + } + + get itemsOrderedByPriorityDesc(): readonly T[] { + return this._items; + } } export class TriggerSuggestAction extends EditorAction { @@ -614,21 +655,29 @@ export class TriggerSuggestAction extends EditorAction { kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.Space, - secondary: [KeyMod.CtrlCmd | KeyCode.KEY_I], - mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.Alt | KeyCode.Escape, KeyMod.CtrlCmd | KeyCode.KEY_I] }, + secondary: [KeyMod.CtrlCmd | KeyCode.KeyI], + mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.Alt | KeyCode.Escape, KeyMod.CtrlCmd | KeyCode.KeyI] }, weight: KeybindingWeight.EditorContrib } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { const controller = SuggestController.get(editor); if (!controller) { return; } - controller.triggerSuggest(); + type TriggerArgs = { auto: boolean }; + let auto: boolean | undefined; + if (args && typeof args === 'object') { + if ((args).auto === true) { + auto = true; + } + } + + controller.triggerSuggest(undefined, auto); } } @@ -735,7 +784,7 @@ registerEditorCommand(new SuggestCommand({ kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], - mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KEY_N] } + mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] } } })); @@ -766,7 +815,7 @@ registerEditorCommand(new SuggestCommand({ kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], - mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KEY_P] } + mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] } } })); @@ -796,7 +845,8 @@ registerEditorCommand(new SuggestCommand({ weight: weight, kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.Space, - mac: { primary: KeyMod.WinCtrl | KeyCode.Space } + secondary: [KeyMod.CtrlCmd | KeyCode.KeyI], + mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.CtrlCmd | KeyCode.KeyI] } }, menuOpts: [{ menuId: suggestWidgetStatusbarMenu, @@ -819,7 +869,7 @@ registerEditorCommand(new SuggestCommand({ handler: x => x.toggleExplainMode(), kbOpts: { weight: KeybindingWeight.EditorContrib, - primary: KeyMod.CtrlCmd | KeyCode.US_SLASH, + primary: KeyMod.CtrlCmd | KeyCode.Slash, } })); diff --git a/src/vs/editor/contrib/suggest/suggestMemory.ts b/src/vs/editor/contrib/suggest/suggestMemory.ts index a54150894a..db3d63d29e 100644 --- a/src/vs/editor/contrib/suggest/suggestMemory.ts +++ b/src/vs/editor/contrib/suggest/suggestMemory.ts @@ -4,18 +4,17 @@ *--------------------------------------------------------------------------------------------*/ -import { LRUCache, TernarySearchTree } from 'vs/base/common/map'; -import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; -import { ITextModel } from 'vs/editor/common/model'; -import { IPosition } from 'vs/editor/common/core/position'; -import { CompletionItemKind, completionKindFromString } from 'vs/editor/common/modes'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { LRUCache, TernarySearchTree } from 'vs/base/common/map'; +import { IPosition } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { CompletionItemKind, completionKindFromString } from 'vs/editor/common/modes'; +import { CompletionItem } from 'vs/editor/contrib/suggest/suggest'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { CompletionItem } from 'vs/editor/contrib/suggest/suggest'; -import { IModeService } from 'vs/editor/common/services/modeService'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; export abstract class Memory { @@ -82,7 +81,7 @@ export class LRUMemory extends Memory { private _seq = 0; memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void { - const key = `${model.getLanguageIdentifier().language}/${item.textLabel}`; + const key = `${model.getLanguageId()}/${item.textLabel}`; this._cache.set(key, { touch: this._seq++, type: item.completion.kind, @@ -110,7 +109,7 @@ export class LRUMemory extends Memory { // consider only top items break; } - const key = `${model.getLanguageIdentifier().language}/${items[i].textLabel}`; + const key = `${model.getLanguageId()}/${items[i].textLabel}`; const item = this._cache.peek(key); if (item && item.touch > seq && item.type === items[i].completion.kind && item.insertText === items[i].completion.insertText) { seq = item.touch; @@ -158,7 +157,7 @@ export class PrefixMemory extends Memory { memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void { const { word } = model.getWordUntilPosition(pos); - const key = `${model.getLanguageIdentifier().language}/${word}`; + const key = `${model.getLanguageId()}/${word}`; this._trie.set(key, { type: item.completion.kind, insertText: item.completion.insertText, @@ -171,7 +170,7 @@ export class PrefixMemory extends Memory { if (!word) { return super.select(model, pos, items); } - let key = `${model.getLanguageIdentifier().language}/${word}`; + let key = `${model.getLanguageId()}/${word}`; let item = this._trie.get(key); if (!item) { item = this._trie.findSubstr(key); @@ -236,7 +235,6 @@ export class SuggestMemoryService implements ISuggestMemoryService { constructor( @IStorageService private readonly _storageService: IStorageService, - @IModeService private readonly _modeService: IModeService, @IConfigurationService private readonly _configService: IConfigurationService, ) { this._persistSoon = new RunOnceScheduler(() => this._saveState(), 500); @@ -264,7 +262,7 @@ export class SuggestMemoryService implements ISuggestMemoryService { private _withStrategy(model: ITextModel, pos: IPosition): Memory { const mode = this._configService.getValue('editor.suggestSelection', { - overrideIdentifier: this._modeService.getLanguageIdentifier(model.getLanguageIdAtPosition(pos.lineNumber, pos.column))?.language, + overrideIdentifier: model.getLanguageIdAtPosition(pos.lineNumber, pos.column), resource: model.uri }); diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index a6ef11d311..da4204aeb7 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -4,28 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import { TimeoutTimer } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { getLeadingWhitespace, isHighSurrogate, isLowSurrogate } from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; -import { Position, IPosition } from 'vs/editor/common/core/position'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel, IWordAtPosition } from 'vs/editor/common/model'; -import { CompletionItemProvider, StandardTokenType, CompletionContext, CompletionProviderRegistry, CompletionTriggerKind, CompletionItemKind } from 'vs/editor/common/modes'; -import { CompletionModel } from './completionModel'; -import { CompletionItem, getSuggestionComparator, provideSuggestionItems, getSnippetSuggestSupport, SnippetSortOrder, CompletionOptions, CompletionDurations } from './suggest'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CompletionContext, CompletionItemKind, CompletionItemProvider, CompletionProviderRegistry, CompletionTriggerKind, StandardTokenType } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { isLowSurrogate, isHighSurrogate, getLeadingWhitespace } from 'vs/base/common/strings'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { CompletionModel } from './completionModel'; +import { CompletionDurations, CompletionItem, CompletionOptions, getSnippetSuggestSupport, getSuggestionComparator, provideSuggestionItems, SnippetSortOrder } from './suggest'; export interface ICancelEvent { readonly retrigger: boolean; @@ -97,18 +97,42 @@ export const enum State { Auto = 2 } -function shouldPreventQuickSuggest(contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean { - return ( - Boolean(contextKeyService.getContextKeyValue('inlineSuggestionVisible')) - && !Boolean(configurationService.getValue('editor.inlineSuggest.allowQuickSuggestions')) - ); +function isSuggestPreviewEnabled(editor: ICodeEditor): boolean { + return editor.getOption(EditorOption.suggest).preview; } -function shouldPreventSuggestOnTriggerCharacters(contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean { - return ( - Boolean(contextKeyService.getContextKeyValue('inlineSuggestionVisible')) - && !Boolean(configurationService.getValue('editor.inlineSuggest.allowSuggestOnTriggerCharacters')) - ); +function canShowQuickSuggest(editor: ICodeEditor, contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean { + if (!Boolean(contextKeyService.getContextKeyValue('inlineSuggestionVisible'))) { + // Allow if there is no inline suggestion. + return true; + } + + const allowQuickSuggestions = configurationService.getValue('editor.inlineSuggest.allowQuickSuggestions'); + if (allowQuickSuggestions !== undefined) { + // Use setting if available. + return Boolean(allowQuickSuggestions); + } + + // Don't allow if inline suggestions are visible and no suggest preview is configured. + // TODO disabled for copilot + return false && isSuggestPreviewEnabled(editor); +} + +function canShowSuggestOnTriggerCharacters(editor: ICodeEditor, contextKeyService: IContextKeyService, configurationService: IConfigurationService): boolean { + if (!Boolean(contextKeyService.getContextKeyValue('inlineSuggestionVisible'))) { + // Allow if there is no inline suggestion. + return true; + } + + const allowQuickSuggestions = configurationService.getValue('editor.inlineSuggest.allowSuggestOnTriggerCharacters'); + if (allowQuickSuggestions !== undefined) { + // Use setting if available. + return Boolean(allowQuickSuggestions); + } + + // Don't allow if inline suggestions are visible and no suggest preview is configured. + // TODO disabled for copilot + return false && isSuggestPreviewEnabled(editor); } export class SuggestModel implements IDisposable { @@ -170,9 +194,8 @@ export class SuggestModel implements IDisposable { editorIsComposing = true; })); this._toDispose.add(this._editor.onDidCompositionEnd(() => { - // refilter when composition ends editorIsComposing = false; - this._refilterCompletionItems(); + this._onCompositionEnd(); })); this._toDispose.add(this._editor.onDidChangeModelContent(() => { // only filter completions when the editor isn't @@ -231,7 +254,7 @@ export class SuggestModel implements IDisposable { const checkTriggerCharacter = (text?: string) => { - if (shouldPreventSuggestOnTriggerCharacters(this._contextKeyService, this._configurationService)) { + if (!canShowSuggestOnTriggerCharacters(this._editor, this._contextKeyService, this._configurationService)) { return; } @@ -304,7 +327,6 @@ export class SuggestModel implements IDisposable { return; } - const model = this._editor.getModel(); const prevSelection = this._currentSelection; this._currentSelection = this._editor.getSelection(); @@ -318,72 +340,13 @@ export class SuggestModel implements IDisposable { return; } - if (!CompletionProviderRegistry.has(model)) { - return; - } if (this._state === State.Idle && e.reason === CursorChangeReason.NotSet) { - - if (this._editor.getOption(EditorOption.quickSuggestions) === false) { - // not enabled - return; + if (prevSelection.containsRange(this._currentSelection) || prevSelection.getEndPosition().isBeforeOrEqual(this._currentSelection.getPosition())) { + // cursor did move RIGHT due to typing -> trigger quick suggest + this._doTriggerQuickSuggest(); } - if (!prevSelection.containsRange(this._currentSelection) && !prevSelection.getEndPosition().isBeforeOrEqual(this._currentSelection.getPosition())) { - // cursor didn't move RIGHT - return; - } - - if (this._editor.getOption(EditorOption.suggest).snippetsPreventQuickSuggestions && SnippetController2.get(this._editor).isInSnippet()) { - // no quick suggestion when in snippet mode - return; - } - - this.cancel(); - - this._triggerQuickSuggest.cancelAndSet(() => { - if (this._state !== State.Idle) { - return; - } - if (!LineContext.shouldAutoTrigger(this._editor)) { - return; - } - if (!this._editor.hasModel()) { - return; - } - const model = this._editor.getModel(); - const pos = this._editor.getPosition(); - // validate enabled now - const quickSuggestions = this._editor.getOption(EditorOption.quickSuggestions); - if (quickSuggestions === false) { - return; - } else if (quickSuggestions === true) { - // all good - } else { - // Check the type of the token that triggered this - model.tokenizeIfCheap(pos.lineNumber); - const lineTokens = model.getLineTokens(pos.lineNumber); - const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0))); - const inValidScope = quickSuggestions.other && tokenType === StandardTokenType.Other - || quickSuggestions.comments && tokenType === StandardTokenType.Comment - || quickSuggestions.strings && tokenType === StandardTokenType.String; - - if (!inValidScope) { - return; - } - } - - if (shouldPreventQuickSuggest(this._contextKeyService, this._configurationService)) { - // do not trigger quick suggestions if inline suggestions are shown - return; - } - - // we made it till here -> trigger now - this.trigger({ auto: true, shy: false }); - - }, this._quickSuggestDelay); - - } else if (this._state !== State.Idle && e.reason === CursorChangeReason.Explicit) { // suggest is active and something like cursor keys are used to move // the cursor. this means we can refilter at the new position @@ -391,6 +354,76 @@ export class SuggestModel implements IDisposable { } } + private _onCompositionEnd(): void { + // trigger or refilter when composition ends + if (this._state === State.Idle) { + this._doTriggerQuickSuggest(); + } else { + this._refilterCompletionItems(); + } + } + + private _doTriggerQuickSuggest(): void { + + if (this._editor.getOption(EditorOption.quickSuggestions) === false) { + // not enabled + return; + } + + if (this._editor.getOption(EditorOption.suggest).snippetsPreventQuickSuggestions && SnippetController2.get(this._editor).isInSnippet()) { + // no quick suggestion when in snippet mode + return; + } + + this.cancel(); + + this._triggerQuickSuggest.cancelAndSet(() => { + if (this._state !== State.Idle) { + return; + } + if (!LineContext.shouldAutoTrigger(this._editor)) { + return; + } + if (!this._editor.hasModel()) { + return; + } + const model = this._editor.getModel(); + const pos = this._editor.getPosition(); + // validate enabled now + const quickSuggestions = this._editor.getOption(EditorOption.quickSuggestions); + if (quickSuggestions === false) { + return; + } else if (quickSuggestions === true) { + // all good + } else { + // Check the type of the token that triggered this + model.tokenizeIfCheap(pos.lineNumber); + const lineTokens = model.getLineTokens(pos.lineNumber); + const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0))); + const inValidScope = quickSuggestions.other && tokenType === StandardTokenType.Other + || quickSuggestions.comments && tokenType === StandardTokenType.Comment + || quickSuggestions.strings && tokenType === StandardTokenType.String; + + if (!inValidScope) { + return; + } + } + + if (!canShowQuickSuggest(this._editor, this._contextKeyService, this._configurationService)) { + // do not trigger quick suggestions if inline suggestions are shown + return; + } + + if (!CompletionProviderRegistry.has(model)) { + return; + } + + // we made it till here -> trigger now + this.trigger({ auto: true, shy: false }); + + }, this._quickSuggestDelay); + } + private _refilterCompletionItems(): void { // Re-filter suggestions. This MUST run async because filtering/scoring // uses the model content AND the cursor position. The latter is NOT diff --git a/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts b/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts index 796d20a425..5fd3b78b71 100644 --- a/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts +++ b/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { SuggestModel } from 'vs/editor/contrib/suggest/suggestModel'; diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index fac8060d6a..0b39836414 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -3,36 +3,36 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/suggest'; +import * as dom from 'vs/base/browser/dom'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded +import { IListEvent, IListGestureEvent, IListMouseEvent } from 'vs/base/browser/ui/list/list'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { CancelablePromise, createCancelablePromise, disposableTimeout, TimeoutTimer } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { clamp } from 'vs/base/common/numbers'; +import * as strings from 'vs/base/common/strings'; +import 'vs/css!./media/suggest'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IPosition } from 'vs/editor/common/core/position'; +import { SuggestWidgetStatus } from 'vs/editor/contrib/suggest/suggestWidgetStatus'; import 'vs/editor/contrib/symbolIcons/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors import * as nls from 'vs/nls'; -import * as strings from 'vs/base/common/strings'; -import * as dom from 'vs/base/browser/dom'; -import { Event, Emitter } from 'vs/base/common/event'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IListEvent, IListMouseEvent, IListGestureEvent } from 'vs/base/browser/ui/list/list'; -import { List } from 'vs/base/browser/ui/list/listWidget'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; -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, quickInputListFocusForeground, listFocusHighlightForeground, quickInputListFocusIconForeground, textLinkActiveForeground } 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'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { SuggestDetailsWidget, canExpandCompletionItem, SuggestDetailsOverlay } from './suggestWidgetDetails'; -import { SuggestWidgetStatus } from 'vs/editor/contrib/suggest/suggestWidgetStatus'; -import { getAriaId, ItemRenderer } from './suggestWidgetRenderer'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { activeContrastBorder, editorForeground, editorWidgetBackground, editorWidgetBorder, focusBorder, listFocusHighlightForeground, listHighlightForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, registerColor, textCodeBlockBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { attachListStyler } from 'vs/platform/theme/common/styler'; +import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { CompletionModel } from './completionModel'; import { ResizableHTMLElement } from './resizable'; -import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { IPosition } from 'vs/editor/common/core/position'; -import { clamp } from 'vs/base/common/numbers'; +import { CompletionItem, Context as SuggestContext } from './suggest'; +import { canExpandCompletionItem, SuggestDetailsOverlay, SuggestDetailsWidget } from './suggestWidgetDetails'; +import { getAriaId, ItemRenderer } from './suggestWidgetRenderer'; /** * Suggest widget colors @@ -339,13 +339,11 @@ export class SuggestWidget implements IDisposable { const backgroundColor = theme.getColor(editorSuggestWidgetBackground); if (backgroundColor) { this.element.domNode.style.backgroundColor = backgroundColor.toString(); - this._messageElement.style.backgroundColor = backgroundColor.toString(); this._details.widget.domNode.style.backgroundColor = backgroundColor.toString(); } const borderColor = theme.getColor(editorSuggestWidgetBorder); if (borderColor) { this.element.domNode.style.borderColor = borderColor.toString(); - this._messageElement.style.borderColor = borderColor.toString(); this._status.element.style.borderTopColor = borderColor.toString(); this._details.widget.domNode.style.borderColor = borderColor.toString(); this._detailsBorderColor = borderColor.toString(); diff --git a/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts b/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts index 5a60a0ffe0..a0ef2a72ad 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts @@ -3,19 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isSafari } from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ICodeEditor, IOverlayWidget } from 'vs/editor/browser/editorBrowser'; -import { CompletionItem } from './suggest'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; -import { MarkdownString } from 'vs/base/common/htmlContent'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { ICodeEditor, IOverlayWidget } from 'vs/editor/browser/editorBrowser'; +import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { ResizableHTMLElement } from 'vs/editor/contrib/suggest/resizable'; +import * as nls from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CompletionItem } from './suggest'; export function canExpandCompletionItem(item: CompletionItem | undefined): boolean { return !!item && Boolean(item.completion.documentation || item.completion.detail && item.completion.detail !== item.completion.label); @@ -83,7 +84,7 @@ export class SuggestDetailsWidget { private _configureFont(): void { const options = this._editor.getOptions(); const fontInfo = options.get(EditorOption.fontInfo); - const fontFamily = fontInfo.fontFamily; + const fontFamily = fontInfo.getMassagedFontFamily(isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null); const fontSize = options.get(EditorOption.suggestFontSize) || fontInfo.fontSize; const lineHeight = options.get(EditorOption.suggestLineHeight) || fontInfo.lineHeight; const fontWeight = fontInfo.fontWeight; @@ -91,7 +92,7 @@ export class SuggestDetailsWidget { const lineHeightPx = `${lineHeight}px`; this.domNode.style.fontSize = fontSizePx; - this.domNode.style.lineHeight = lineHeightPx; + this.domNode.style.lineHeight = `${lineHeight / fontSize}`; this.domNode.style.fontWeight = fontWeight; this.domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings; this._type.style.fontFamily = fontFamily; @@ -371,68 +372,64 @@ export class SuggestDetailsOverlay implements IOverlayWidget { const info = this.widget.getLayoutInfo(); - let maxSizeTop: dom.Dimension; - let maxSizeBottom: dom.Dimension; - let minSize = new dom.Dimension(220, 2 * info.lineHeight); + const defaultMinSize = new dom.Dimension(220, 2 * info.lineHeight); + const defaultTop = anchorBox.top; - let left = 0; - let top = anchorBox.top; - let bottom = anchorBox.top + anchorBox.height - info.borderHeight; + type Placement = { top: number, left: number, fit: number, maxSizeTop: dom.Dimension, maxSizeBottom: dom.Dimension, minSize: dom.Dimension }; - let alignAtTop: boolean; - let alignEast: boolean; + // EAST + const eastPlacement: Placement = (function () { + const width = bodyBox.width - (anchorBox.left + anchorBox.width + info.borderWidth + info.horizontalPadding); + const left = -info.borderWidth + anchorBox.left + anchorBox.width; + const maxSizeTop = new dom.Dimension(width, bodyBox.height - anchorBox.top - info.borderHeight - info.verticalPadding); + const maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height - info.borderHeight - info.verticalPadding); + return { top: defaultTop, left, fit: width - size.width, maxSizeTop, maxSizeBottom, minSize: defaultMinSize.with(Math.min(width, defaultMinSize.width)) }; + })(); - // position: EAST, west, south - let width = bodyBox.width - (anchorBox.left + anchorBox.width + info.borderWidth + info.horizontalPadding); - left = -info.borderWidth + anchorBox.left + anchorBox.width; - alignEast = true; - maxSizeTop = new dom.Dimension(width, bodyBox.height - anchorBox.top - info.borderHeight - info.verticalPadding); - maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height - info.borderHeight - info.verticalPadding); + // WEST + const westPlacement: Placement = (function () { + const width = anchorBox.left - info.borderWidth - info.horizontalPadding; + const left = Math.max(info.horizontalPadding, anchorBox.left - size.width - info.borderWidth); + const maxSizeTop = new dom.Dimension(width, bodyBox.height - anchorBox.top - info.borderHeight - info.verticalPadding); + const maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height - info.borderHeight - info.verticalPadding); + return { top: defaultTop, left, fit: width - size.width, maxSizeTop, maxSizeBottom, minSize: defaultMinSize.with(Math.min(width, defaultMinSize.width)) }; + })(); - // find a better place if the widget is wider than there is space available - if (size.width > width) { - // position: east, WEST, south - if (anchorBox.left > width) { - // pos = SuggestDetailsPosition.West; - width = anchorBox.left - info.borderWidth - info.horizontalPadding; - alignEast = false; - left = Math.max(info.horizontalPadding, anchorBox.left - size.width - info.borderWidth); - maxSizeTop = maxSizeTop.with(width); - maxSizeBottom = maxSizeTop.with(undefined, maxSizeBottom.height); - } + // SOUTH + const southPacement: Placement = (function () { + const left = anchorBox.left; + const top = -info.borderWidth + anchorBox.top + anchorBox.height; + const maxSizeBottom = new dom.Dimension(anchorBox.width - info.borderHeight, bodyBox.height - anchorBox.top - anchorBox.height - info.verticalPadding); + return { top, left, fit: maxSizeBottom.height - size.height, maxSizeBottom, maxSizeTop: maxSizeBottom, minSize: defaultMinSize.with(maxSizeBottom.width) }; + })(); - // position: east, west, SOUTH - if (anchorBox.width > width * 1.3 && bodyBox.height - (anchorBox.top + anchorBox.height) > anchorBox.height) { - width = anchorBox.width; - left = anchorBox.left; - top = -info.borderWidth + anchorBox.top + anchorBox.height; - maxSizeTop = new dom.Dimension(anchorBox.width - info.borderHeight, bodyBox.height - anchorBox.top - anchorBox.height - info.verticalPadding); - maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top - info.verticalPadding); - minSize = minSize.with(maxSizeTop.width); - } - } + // take first placement that fits or the first with "least bad" fit + const placements = [eastPlacement, westPlacement, southPacement]; + const placement = placements.find(p => p.fit >= 0) ?? placements.sort((a, b) => b.fit - a.fit)[0]; // top/bottom placement + const bottom = anchorBox.top + anchorBox.height - info.borderHeight; + let alignAtTop: boolean; let height = size.height; - let maxHeight = Math.max(maxSizeTop.height, maxSizeBottom.height); + const maxHeight = Math.max(placement.maxSizeTop.height, placement.maxSizeBottom.height); if (height > maxHeight) { height = maxHeight; } let maxSize: dom.Dimension; - if (height <= maxSizeTop.height) { + if (height <= placement.maxSizeTop.height) { alignAtTop = true; - maxSize = maxSizeTop; + maxSize = placement.maxSizeTop; } else { alignAtTop = false; - maxSize = maxSizeBottom; + maxSize = placement.maxSizeBottom; } - this._applyTopLeft({ left, top: alignAtTop ? top : bottom - height }); + this._applyTopLeft({ left: placement.left, top: alignAtTop ? placement.top : bottom - height }); this.getDomNode().style.position = 'fixed'; - this._resizable.enableSashes(!alignAtTop, alignEast, alignAtTop, !alignEast); + this._resizable.enableSashes(!alignAtTop, placement === eastPlacement, alignAtTop, placement !== eastPlacement); - this._resizable.minSize = minSize; + this._resizable.minSize = placement.minSize; this._resizable.maxSize = maxSize; this._resizable.layout(height, Math.min(maxSize.width, size.width)); this.widget.layout(this._resizable.size.width, this._resizable.size.height); diff --git a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index 3a5aa7b53c..04d550a4a8 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -3,27 +3,28 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { createMatches } from 'vs/base/common/filters'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { append, $, hide, show } from 'vs/base/browser/dom'; -import { IListRenderer } from 'vs/base/browser/ui/list/list'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { CompletionItem } from './suggest'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { CompletionItemKind, completionKindToCssClass, CompletionItemTag } from 'vs/editor/common/modes'; +import { isSafari } from 'vs/base/browser/browser'; +import { $, append, hide, show } from 'vs/base/browser/dom'; import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { URI } from 'vs/base/common/uri'; -import { FileKind } from 'vs/platform/files/common/files'; +import { IListRenderer } from 'vs/base/browser/ui/list/list'; import { flatten } from 'vs/base/common/arrays'; -import { canExpandCompletionItem } from './suggestWidgetDetails'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; +import { createMatches } from 'vs/base/common/filters'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; +import { CompletionItemKind, CompletionItemTag, completionKindToCssClass } from 'vs/editor/common/modes'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import * as nls from 'vs/nls'; +import { FileKind } from 'vs/platform/files/common/files'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { CompletionItem } from './suggest'; +import { canExpandCompletionItem } from './suggestWidgetDetails'; export function getAriaId(index: number): string { return `suggest-aria-id:${index}`; @@ -130,7 +131,7 @@ export class ItemRenderer implements IListRenderer { const options = this._editor.getOptions(); const fontInfo = options.get(EditorOption.fontInfo); - const fontFamily = fontInfo.fontFamily; + const fontFamily = fontInfo.getMassagedFontFamily(isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null); const fontFeatureSettings = fontInfo.fontFeatureSettings; const fontSize = options.get(EditorOption.suggestFontSize) || fontInfo.fontSize; const lineHeight = options.get(EditorOption.suggestLineHeight) || fontInfo.lineHeight; diff --git a/src/vs/editor/contrib/suggest/suggestWidgetStatus.ts b/src/vs/editor/contrib/suggest/suggestWidgetStatus.ts index 8deca02623..06ba30d08e 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetStatus.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetStatus.ts @@ -6,7 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction } from 'vs/base/common/actions'; -import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { suggestWidgetStatusbarMenu } from 'vs/editor/contrib/suggest/suggest'; import { localize } from 'vs/nls'; diff --git a/src/vs/editor/contrib/suggest/test/completionModel.test.ts b/src/vs/editor/contrib/suggest/test/completionModel.test.ts index 078b1c0275..1ef036694e 100644 --- a/src/vs/editor/contrib/suggest/test/completionModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/completionModel.test.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { EditorOptions, InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import * as modes from 'vs/editor/common/modes'; import { CompletionModel } from 'vs/editor/contrib/suggest/completionModel'; import { CompletionItem, getSuggestionComparator, SnippetSortOrder } from 'vs/editor/contrib/suggest/suggest'; import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; -import { EditorOptions, InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; export function createSuggestItem(label: string, overwriteBefore: number, kind = modes.CompletionItemKind.Property, incomplete: boolean = false, position: IPosition = { lineNumber: 1, column: 1 }, sortText?: string, filterText?: string): CompletionItem { const suggestion: modes.CompletionItem = { diff --git a/src/vs/editor/contrib/suggest/test/suggest.test.ts b/src/vs/editor/contrib/suggest/test/suggest.test.ts index 3c53b8097a..f466b8718d 100644 --- a/src/vs/editor/contrib/suggest/test/suggest.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggest.test.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { CompletionProviderRegistry, CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/modes'; -import { provideSuggestionItems, SnippetSortOrder, CompletionOptions } from 'vs/editor/contrib/suggest/suggest'; +import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; -import { TextModel } from 'vs/editor/common/model/textModel'; import { Range } from 'vs/editor/common/core/range'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { CompletionItemKind, CompletionItemProvider, CompletionProviderRegistry } from 'vs/editor/common/modes'; +import { CompletionOptions, provideSuggestionItems, SnippetSortOrder } from 'vs/editor/contrib/suggest/suggest'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts index c12fd1c685..e42164b5c1 100644 --- a/src/vs/editor/contrib/suggest/test/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestController.test.ts @@ -4,29 +4,31 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; -import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { TextModel } from 'vs/editor/common/model/textModel'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; +import { timeout } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { mock } from 'vs/base/test/common/mock'; -import { Selection } from 'vs/editor/common/core/selection'; -import { CompletionProviderRegistry, CompletionItemKind, CompletionItemInsertTextRule } from 'vs/editor/common/modes'; -import { Event } from 'vs/base/common/event'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; -import { IMenuService, IMenu } from 'vs/platform/actions/common/actions'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { Range } from 'vs/editor/common/core/range'; -import { timeout } from 'vs/base/common/async'; -import { NullLogService, ILogService } from 'vs/platform/log/common/log'; +import { Selection } from 'vs/editor/common/core/selection'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { CompletionItemInsertTextRule, CompletionItemKind, CompletionProviderRegistry } from 'vs/editor/common/modes'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; +import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; suite('SuggestController', function () { @@ -63,14 +65,16 @@ suite('SuggestController', function () { override dispose() { } }; } - }] + }], + [ILabelService, new class extends mock() { }], + [IWorkspaceContextService, new class extends mock() { }], ); - model = createTextModel('', undefined, undefined, URI.from({ scheme: 'test-ctrl', path: '/path.tst' })); - editor = createTestCodeEditor({ + model = disposables.add(createTextModel('', undefined, undefined, URI.from({ scheme: 'test-ctrl', path: '/path.tst' }))); + editor = disposables.add(createTestCodeEditor({ model, serviceCollection, - }); + })); editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); controller = editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController); diff --git a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts index 651f917bb3..d9bbf35b18 100644 --- a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { LRUMemory, NoMemory, PrefixMemory, Memory } from 'vs/editor/contrib/suggest/suggestMemory'; -import { ITextModel } from 'vs/editor/common/model'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { createSuggestItem } from 'vs/editor/contrib/suggest/test/completionModel.test'; import { IPosition } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; import { CompletionItem } from 'vs/editor/contrib/suggest/suggest'; +import { LRUMemory, Memory, NoMemory, PrefixMemory } from 'vs/editor/contrib/suggest/suggestMemory'; +import { createSuggestItem } from 'vs/editor/contrib/suggest/test/completionModel.test'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('SuggestMemories', function () { @@ -26,6 +26,10 @@ suite('SuggestMemories', function () { ]; }); + teardown(() => { + buffer.dispose(); + }); + test('AbstractMemory, select', function () { const mem = new class extends Memory { diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 51bf5203ef..f8594d6d08 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -4,39 +4,42 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; import { Handler } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { IState, CompletionList, CompletionItemProvider, LanguageIdentifier, MetadataConsts, CompletionProviderRegistry, CompletionTriggerKind, TokenizationRegistry, CompletionItemKind } from 'vs/editor/common/modes'; +import { CompletionItemKind, CompletionItemProvider, CompletionList, CompletionProviderRegistry, CompletionTriggerKind, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; import { LineContext, SuggestModel } from 'vs/editor/contrib/suggest/suggestModel'; import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget'; -import { ITestCodeEditor, createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createModelServices, createTextModel, createTextModel2 } from 'vs/editor/test/common/editorTestUtils'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; -import { ITextModel } from 'vs/editor/common/model'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { MockKeybindingService, MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { mock } from 'vs/base/test/common/mock'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; function createMockEditor(model: TextModel): ITestCodeEditor { @@ -54,6 +57,8 @@ function createMockEditor(model: TextModel): ITestCodeEditor { return -1; } }], + [ILabelService, new class extends mock() { }], + [IWorkspaceContextService, new class extends mock() { }], ), }); editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); @@ -61,25 +66,28 @@ function createMockEditor(model: TextModel): ITestCodeEditor { } suite('SuggestModel - Context', function () { - const OUTER_LANGUAGE_ID = new LanguageIdentifier('outerMode', 3); - const INNER_LANGUAGE_ID = new LanguageIdentifier('innerMode', 4); + const OUTER_LANGUAGE_ID = 'outerMode'; + const INNER_LANGUAGE_ID = 'innerMode'; class OuterMode extends MockMode { - constructor() { + constructor( + @IModeService modeService: IModeService + ) { super(OUTER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {})); + this._register(LanguageConfigurationRegistry.register(this.languageId, {})); - this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, { + this._register(TokenizationRegistry.register(this.languageId, { getInitialState: (): IState => NULL_STATE, tokenize: undefined!, tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { const tokensArr: number[] = []; - let prevLanguageId: LanguageIdentifier | undefined = undefined; + let prevLanguageId: string | undefined = undefined; for (let i = 0; i < line.length; i++) { const languageId = (line.charAt(i) === 'x' ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID); + const encodedLanguageId = modeService.languageIdCodec.encodeLanguageId(languageId); if (prevLanguageId !== languageId) { tokensArr.push(i); - tokensArr.push((languageId.id << MetadataConsts.LANGUAGEID_OFFSET)); + tokensArr.push((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET)); } prevLanguageId = languageId; } @@ -97,7 +105,7 @@ suite('SuggestModel - Context', function () { class InnerMode extends MockMode { constructor() { super(INNER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {})); + this._register(LanguageConfigurationRegistry.register(this.languageId, {})); } } @@ -109,42 +117,43 @@ suite('SuggestModel - Context', function () { editor.dispose(); }; - let disposables: Disposable[] = []; + let disposables: DisposableStore; setup(() => { - disposables = []; + disposables = new DisposableStore(); }); teardown(function () { - dispose(disposables); - disposables = []; + disposables.dispose(); }); test('Context - shouldAutoTrigger', function () { const model = createTextModel('Das Pferd frisst keinen Gurkensalat - Philipp Reis 1861.\nWer hat\'s erfunden?'); - disposables.push(model); + disposables.add(model); assertAutoTrigger(model, 3, true, 'end of word, Das|'); assertAutoTrigger(model, 4, false, 'no word Das |'); assertAutoTrigger(model, 1, false, 'middle of word D|as'); assertAutoTrigger(model, 55, false, 'number, 1861|'); + model.dispose(); }); test('shouldAutoTrigger at embedded language boundaries', () => { - const outerMode = new OuterMode(); - const innerMode = new InnerMode(); - disposables.push(outerMode, innerMode); + const [instantiationService, disposables] = createModelServices(); + const outerMode = disposables.add(instantiationService.createInstance(OuterMode)); + disposables.add(instantiationService.createInstance(InnerMode)); - const model = createTextModel('aa', undefined, outerMode.getLanguageIdentifier()); - disposables.push(model); + const model = disposables.add(createTextModel2(instantiationService, 'aa', undefined, outerMode.languageId)); - assertAutoTrigger(model, 1, true, 'a| — should trigger at boundary between languages'); + assertAutoTrigger(model, 4, true, 'a — should trigger at boundary between languages'); assertAutoTrigger(model, 5, false, 'a|a — should NOT trigger at start of word'); assertAutoTrigger(model, 6, true, 'aa|< — should trigger at end of word'); assertAutoTrigger(model, 8, true, 'aa — should trigger at end of word at boundary'); + + disposables.dispose(); }); }); @@ -179,13 +188,17 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { } }; - let disposables: IDisposable[] = []; + let disposables: DisposableStore; let model: TextModel; setup(function () { - disposables = dispose(disposables); + disposables = new DisposableStore(); model = createTextModel('abc def', undefined, undefined, URI.parse('test:somefile.ttt')); - disposables.push(model); + disposables.add(model); + }); + + teardown(() => { + disposables.dispose(); }); function withOracle(callback: (model: SuggestModel, editor: ITestCodeEditor) => any): Promise { @@ -209,7 +222,8 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { new MockContextKeyService(), new TestConfigurationService() ); - disposables.push(oracle, editor); + disposables.add(oracle); + disposables.add(editor); try { resolve(callback(oracle, editor)); @@ -273,7 +287,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('events - suggest/empty', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysEmptySupport)); + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysEmptySupport)); return withOracle(model => { return Promise.all([ @@ -295,7 +309,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('trigger - on type', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); return withOracle((model, editor) => { return assertEvent(model.onDidSuggest, () => { @@ -314,7 +328,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('#17400: Keep filtering suggestModel.ts after space', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos): CompletionList { return { incomplete: false, @@ -364,7 +378,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('#21484: Trigger character always force a new completion session', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos): CompletionList { return { incomplete: false, @@ -378,7 +392,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { } })); - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { triggerCharacters: ['.'], provideCompletionItems(doc, pos): CompletionList { return { @@ -426,7 +440,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [1/2]', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); return withOracle((model, editor) => { @@ -451,7 +465,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [2/2]', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); return withOracle((model, editor) => { @@ -476,7 +490,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (1/2)', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos): CompletionList { return { incomplete: true, @@ -513,7 +527,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (2/2)', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos): CompletionList { return { incomplete: true, @@ -556,7 +570,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('Trigger character is provided in suggest context', function () { let triggerCharacter = ''; - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { triggerCharacters: ['.'], provideCompletionItems(doc, pos, context): CompletionList { assert.strictEqual(context.triggerKind, CompletionTriggerKind.TriggerCharacter); @@ -589,7 +603,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }); test('Mac press and hold accent character insertion does not update suggestions, #35269', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos): CompletionList { return { incomplete: true, @@ -632,7 +646,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }); test('Backspace should not always cancel code completion, #36491', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); return withOracle(async (model, editor) => { await assertEvent(model.onDidSuggest, () => { @@ -661,7 +675,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }); test('Text changes for completion CodeAction are affected by the completion #39893', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos): CompletionList { return { incomplete: true, @@ -711,7 +725,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { test('Completion unexpectedly triggers on second keypress of an edit group in a snippet #43523', function () { - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport)); return withOracle((model, editor) => { return assertEvent(model.onDidSuggest, () => { @@ -735,7 +749,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { let disposeA = 0; let disposeB = 0; - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos) { return { incomplete: true, @@ -750,7 +764,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }; } })); - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos) { return { incomplete: false, @@ -804,7 +818,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { let countA = 0; let countB = 0; - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos) { countA += 1; return { @@ -818,7 +832,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }; } })); - disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test' }, { provideCompletionItems(doc, pos) { countB += 1; if (!doc.getWordUntilPosition(pos).word.startsWith('a')) { diff --git a/src/vs/editor/contrib/suggest/test/wordDistance.test.ts b/src/vs/editor/contrib/suggest/test/wordDistance.test.ts index bec6ba4b2d..b9af45ef03 100644 --- a/src/vs/editor/contrib/suggest/test/wordDistance.test.ts +++ b/src/vs/editor/contrib/suggest/test/wordDistance.test.ts @@ -4,35 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; -import { mock } from 'vs/base/test/common/mock'; -import { EditorWorkerHost, EditorWorkerServiceImpl } from 'vs/editor/common/services/editorWorkerServiceImpl'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; -import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { mock } from 'vs/base/test/common/mock'; +import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/model/wordHelper'; -import { Event } from 'vs/base/common/event'; -import { CompletionItem } from 'vs/editor/contrib/suggest/suggest'; -import { IPosition } from 'vs/editor/common/core/position'; import * as modes from 'vs/editor/common/modes'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; +import { EditorWorkerHost, EditorWorkerServiceImpl } from 'vs/editor/common/services/editorWorkerServiceImpl'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { CompletionItem } from 'vs/editor/contrib/suggest/suggest'; +import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; +import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +import { NullLogService } from 'vs/platform/log/common/log'; suite('suggest, word distance', function () { class BracketMode extends MockMode { - private static readonly _id = new modes.LanguageIdentifier('bracketMode', 3); + private static readonly _id = 'bracketMode'; constructor() { super(BracketMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { brackets: [ ['{', '}'], ['[', ']'], @@ -48,7 +48,7 @@ suite('suggest, word distance', function () { disposables.clear(); let mode = new BracketMode(); - let model = createTextModel('function abc(aa, ab){\na\n}', undefined, mode.getLanguageIdentifier(), URI.parse('test:///some.path')); + let model = createTextModel('function abc(aa, ab){\na\n}', undefined, mode.languageId, URI.parse('test:///some.path')); let editor = createTestCodeEditor({ model: model }); editor.updateOptions({ suggest: { localityBonus: true } }); editor.setPosition({ lineNumber: 2, column: 2 }); diff --git a/src/vs/editor/contrib/suggest/wordContextKey.ts b/src/vs/editor/contrib/suggest/wordContextKey.ts index b514feb4c0..b11fbb1cdf 100644 --- a/src/vs/editor/contrib/suggest/wordContextKey.ts +++ b/src/vs/editor/contrib/suggest/wordContextKey.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export class WordContextKey { diff --git a/src/vs/editor/contrib/suggest/wordDistance.ts b/src/vs/editor/contrib/suggest/wordDistance.ts index 5051a43b38..2e1186c97a 100644 --- a/src/vs/editor/contrib/suggest/wordDistance.ts +++ b/src/vs/editor/contrib/suggest/wordDistance.ts @@ -5,12 +5,12 @@ import { binarySearch, isFalsyOrEmpty } from 'vs/base/common/arrays'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { CompletionItem, CompletionItemKind } from 'vs/editor/common/modes'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bracketSelections'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; export abstract class WordDistance { diff --git a/src/vs/editor/contrib/symbolIcons/symbolIcons.ts b/src/vs/editor/contrib/symbolIcons/symbolIcons.ts index 9f9b74f947..926af56cfc 100644 --- a/src/vs/editor/contrib/symbolIcons/symbolIcons.ts +++ b/src/vs/editor/contrib/symbolIcons/symbolIcons.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { registerColor, foreground } from 'vs/platform/theme/common/colorRegistry'; import { Codicon } from 'vs/base/common/codicons'; +import { localize } from 'vs/nls'; +import { foreground, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', { dark: foreground, diff --git a/src/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.ts b/src/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.ts index e9727d2694..35af9930c7 100644 --- a/src/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.ts +++ b/src/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; +import * as nls from 'vs/nls'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export class ToggleTabFocusModeAction extends EditorAction { @@ -23,8 +23,8 @@ export class ToggleTabFocusModeAction extends EditorAction { precondition: undefined, kbOpts: { kbExpr: null, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M), // {{SQL CARBON EDIT}} We use Ctrl+M already so move this to an unused binding - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_M }, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyM), // {{SQL CARBON EDIT}} We use Ctrl+M already so move this to an unused binding + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyM }, weight: KeybindingWeight.EditorContrib } }); diff --git a/src/vs/editor/contrib/tokenization/tokenization.ts b/src/vs/editor/contrib/tokenization/tokenization.ts index a8c0f19f16..d5be61fb3a 100644 --- a/src/vs/editor/contrib/tokenization/tokenization.ts +++ b/src/vs/editor/contrib/tokenization/tokenization.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 { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import * as nls from 'vs/nls'; class ForceRetokenizeAction extends EditorAction { constructor() { diff --git a/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts b/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts index d69ba46459..3a6cb6a1b4 100644 --- a/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts +++ b/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts @@ -3,16 +3,16 @@ * 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 { basename } from 'vs/base/common/resources'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; +import * as nls from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { basename } from 'vs/base/common/resources'; const ignoreUnusualLineTerminators = 'ignoreUnusualLineTerminators'; diff --git a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts index 2fbc13ed4c..237fb4f84f 100644 --- a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts +++ b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts @@ -3,20 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler, createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { DocumentRangeSemanticTokensProviderRegistry, DocumentRangeSemanticTokensProvider, SemanticTokens } from 'vs/editor/common/modes'; +import { DocumentRangeSemanticTokensProviderRegistry } from 'vs/editor/common/modes'; +import { getDocumentRangeSemanticTokens, hasDocumentRangeSemanticTokensProvider } from 'vs/editor/common/services/getSemanticTokens'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { toMultilineTokens2, SemanticTokensProviderStyling } from 'vs/editor/common/services/semanticTokensProviderStyling'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isSemanticColoringEnabled, SEMANTIC_HIGHLIGHTING_SETTING_ID } from 'vs/editor/common/services/modelServiceImpl'; -import { getDocumentRangeSemanticTokensProvider } from 'vs/editor/common/services/getSemanticTokens'; +import { toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution { @@ -28,7 +28,7 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo private readonly _editor: ICodeEditor; private readonly _tokenizeViewport: RunOnceScheduler; - private _outstandingRequests: CancelablePromise[]; + private _outstandingRequests: CancelablePromise[]; constructor( editor: ICodeEditor, @@ -74,7 +74,7 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo this._outstandingRequests = []; } - private _removeOutstandingRequest(req: CancelablePromise): void { + private _removeOutstandingRequest(req: CancelablePromise): void { for (let i = 0, len = this._outstandingRequests.length; i < len; i++) { if (this._outstandingRequests[i] === req) { this._outstandingRequests.splice(i, 1); @@ -97,27 +97,27 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo } return; } - const provider = getDocumentRangeSemanticTokensProvider(model); - if (!provider) { + if (!hasDocumentRangeSemanticTokensProvider(model)) { if (model.hasSomeSemanticTokens()) { model.setSemanticTokens(null, false); } return; } - const styling = this._modelService.getSemanticTokensProviderStyling(provider); const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); - this._outstandingRequests = this._outstandingRequests.concat(visibleRanges.map(range => this._requestRange(model, range, provider, styling))); + this._outstandingRequests = this._outstandingRequests.concat(visibleRanges.map(range => this._requestRange(model, range))); } - private _requestRange(model: ITextModel, range: Range, provider: DocumentRangeSemanticTokensProvider, styling: SemanticTokensProviderStyling): CancelablePromise { + private _requestRange(model: ITextModel, range: Range): CancelablePromise { const requestVersionId = model.getVersionId(); - const request = createCancelablePromise(token => Promise.resolve(provider.provideDocumentRangeSemanticTokens(model, range, token))); + const request = createCancelablePromise(token => Promise.resolve(getDocumentRangeSemanticTokens(model, range, token))); request.then((r) => { - if (!r || model.isDisposed() || model.getVersionId() !== requestVersionId) { + if (!r || !r.tokens || model.isDisposed() || model.getVersionId() !== requestVersionId) { return; } - model.setPartialSemanticTokens(range, toMultilineTokens2(r, styling, model.getLanguageIdentifier())); + const { provider, tokens: result } = r; + const styling = this._modelService.getSemanticTokensProviderStyling(provider); + model.setPartialSemanticTokens(range, toMultilineTokens2(result, styling, model.getLanguageId())); }).then(() => this._removeOutstandingRequest(request), () => this._removeOutstandingRequest(request)); return request; } diff --git a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts index 80b979d60a..25ad30bf0f 100644 --- a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { alert } from 'vs/base/browser/ui/aria/aria'; import * as arrays from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, first, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -12,22 +12,22 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, IActionOptions, registerEditorAction, registerEditorContribution, registerModelAndPositionCommand } from 'vs/editor/browser/editorExtensions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { IModelDeltaDecoration, ITextModel, OverviewRulerLane, TrackedRangeStickiness, IWordAtPosition } from 'vs/editor/common/model'; +import { IModelDeltaDecoration, ITextModel, IWordAtPosition, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProviderRegistry } from 'vs/editor/common/modes'; +import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { activeContrastBorder, editorSelectionHighlight, editorSelectionHighlightBorder, overviewRulerSelectionHighlightForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, editorSelectionHighlight, editorSelectionHighlightBorder, minimapSelectionOccurrenceHighlight, overviewRulerSelectionHighlightForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { alert } from 'vs/base/browser/ui/aria/aria'; const editorWordHighlight = registerColor('editor.wordHighlightBackground', { dark: '#575757B8', light: '#57575740', hc: null }, nls.localize('wordHighlight', 'Background color of a symbol during read-access, like reading a variable. The color must not be opaque so as not to hide underlying decorations.'), true); const editorWordHighlightStrong = registerColor('editor.wordHighlightStrongBackground', { dark: '#004972B8', light: '#0e639c40', hc: null }, nls.localize('wordHighlightStrong', 'Background color of a symbol during write-access, like writing to a variable. The color must not be opaque so as not to hide underlying decorations.'), true); @@ -449,7 +449,11 @@ class WordHighlighter { overviewRuler: { color: themeColorFromId(overviewRulerWordHighlightStrongForeground), position: OverviewRulerLane.Center - } + }, + minimap: { + color: themeColorFromId(minimapSelectionOccurrenceHighlight), + position: MinimapPosition.Inline + }, }); private static readonly _TEXT_OPTIONS = ModelDecorationOptions.register({ @@ -459,7 +463,11 @@ class WordHighlighter { overviewRuler: { color: themeColorFromId(overviewRulerSelectionHighlightForeground), position: OverviewRulerLane.Center - } + }, + minimap: { + color: themeColorFromId(minimapSelectionOccurrenceHighlight), + position: MinimapPosition.Inline + }, }); private static readonly _REGULAR_OPTIONS = ModelDecorationOptions.register({ @@ -469,7 +477,11 @@ class WordHighlighter { overviewRuler: { color: themeColorFromId(overviewRulerWordHighlightForeground), position: OverviewRulerLane.Center - } + }, + minimap: { + color: themeColorFromId(minimapSelectionOccurrenceHighlight), + position: MinimapPosition.Inline + }, }); public dispose(): void { diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index e55576544e..6032bb54a1 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorCommand } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; -import { deserializePipePositions, serializePipePositions, testRepeatedActionAndExtractPositions } from 'vs/editor/contrib/wordOperations/test/wordTestUtils'; -import { CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorWordEndRightSelect, CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect, CursorWordStartLeft, CursorWordStartLeftSelect, CursorWordStartRight, CursorWordStartRightSelect, DeleteWordEndLeft, DeleteWordEndRight, DeleteWordLeft, DeleteWordRight, DeleteWordStartLeft, DeleteWordStartRight, CursorWordAccessibilityLeft, CursorWordAccessibilityLeftSelect, CursorWordAccessibilityRight, CursorWordAccessibilityRightSelect, DeleteInsideWord } from 'vs/editor/contrib/wordOperations/wordOperations'; -import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; -import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; +import { deserializePipePositions, serializePipePositions, testRepeatedActionAndExtractPositions } from 'vs/editor/contrib/wordOperations/test/wordTestUtils'; +import { CursorWordAccessibilityLeft, CursorWordAccessibilityLeftSelect, CursorWordAccessibilityRight, CursorWordAccessibilityRightSelect, CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorWordEndRightSelect, CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect, CursorWordStartLeft, CursorWordStartLeftSelect, CursorWordStartRight, CursorWordStartRightSelect, DeleteInsideWord, DeleteWordEndLeft, DeleteWordEndRight, DeleteWordLeft, DeleteWordRight, DeleteWordStartLeft, DeleteWordStartRight } from 'vs/editor/contrib/wordOperations/wordOperations'; +import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; suite('WordOperations', () => { @@ -731,11 +730,11 @@ suite('WordOperations', () => { }); test('deleteWordLeft - issue #91855: Matching (quote, bracket, paren) doesn\'t get deleted when hitting Ctrl+Backspace', () => { - const languageId = new LanguageIdentifier('myTestMode', 5); + const languageId = 'myTestMode'; class TestMode extends MockMode { constructor() { super(languageId); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { autoClosingPairs: [ { open: '\"', close: '\"' } ] diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index 58d31fa8a1..a8ad046bfb 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -3,27 +3,27 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorCommand, ICommandOptions, ServicesAccessor, registerEditorCommand, EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorCommand, ICommandOptions, registerEditorAction, registerEditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand'; +import { EditorOption, EditorOptions } from 'vs/editor/common/config/editorOptions'; import { CursorState } from 'vs/editor/common/controller/cursorCommon'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { DeleteWordContext, WordNavigationType, WordOperations } from 'vs/editor/common/controller/cursorWordOperations'; -import { WordCharacterClassifier, getMapForWordSeparators } from 'vs/editor/common/controller/wordCharacterClassifier'; +import { getMapForWordSeparators, WordCharacterClassifier } from 'vs/editor/common/controller/wordCharacterClassifier'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import * as nls from 'vs/nls'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { EditorOption, EditorOptions } from 'vs/editor/common/config/editorOptions'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IsWindowsContext } from 'vs/platform/contextkey/common/contextkeys'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export interface MoveWordOptions extends ICommandOptions { inSelectionMode: boolean; @@ -339,7 +339,7 @@ export abstract class DeleteWordCommand extends EditorCommand { const selections = editor.getSelections(); const autoClosingBrackets = editor.getOption(EditorOption.autoClosingBrackets); const autoClosingQuotes = editor.getOption(EditorOption.autoClosingQuotes); - const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(model.getLanguageIdentifier().id); + const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(model.getLanguageId()); const viewModel = editor._getViewModel(); const commands = selections.map((sel) => { diff --git a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts index 9d533b73d4..2f07113b1e 100644 --- a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./zoneWidget'; import * as dom from 'vs/base/browser/dom'; import { IHorizontalSashLayoutProvider, ISashEvent, Orientation, Sash, SashState } from 'vs/base/browser/ui/sash/sash'; import { Color, RGBA } from 'vs/base/common/color'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { DisposableStore } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; +import 'vs/css!./zoneWidget'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts index bff39cc85f..828362a70c 100644 --- a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts +++ b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts @@ -141,7 +141,7 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { return; } - if (e.equals(KeyMod.CtrlCmd | KeyCode.KEY_E)) { + if (e.equals(KeyMod.CtrlCmd | KeyCode.KeyE)) { alert(AccessibilityHelpNLS.emergencyConfOn); this._editor.updateOptions({ @@ -156,7 +156,7 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { e.stopPropagation(); } - if (e.equals(KeyMod.CtrlCmd | KeyCode.KEY_H)) { + if (e.equals(KeyMod.CtrlCmd | KeyCode.KeyH)) { alert(AccessibilityHelpNLS.openingDocs); let url = (this._editor.getRawOptions()).accessibilityHelpUrl; diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index 2e83a3e931..6f76a8224f 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { IViewLineTokens, LineTokens } from 'vs/editor/common/core/lineTokens'; import { ITextModel } from 'vs/editor/common/model'; -import { ColorId, FontStyle, ITokenizationSupport, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/modes'; +import { ColorId, FontStyle, ILanguageIdCodec, ITokenizationSupport, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; import { RenderLineInput, renderViewLine2 as renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; @@ -49,6 +49,7 @@ export class Colorizer { } public static colorize(modeService: IModeService, text: string, mimeType: string, options: IColorizerOptions | null | undefined): Promise { + const languageIdCodec = modeService.languageIdCodec; let tabSize = 4; if (options && typeof options.tabSize === 'number') { tabSize = options.tabSize; @@ -60,7 +61,7 @@ export class Colorizer { let lines = strings.splitLines(text); let language = modeService.getModeId(mimeType); if (!language) { - return Promise.resolve(_fakeColorize(lines, tabSize)); + return Promise.resolve(_fakeColorize(lines, tabSize, languageIdCodec)); } // Send out the event to create the mode @@ -68,7 +69,7 @@ export class Colorizer { const tokenizationSupport = TokenizationRegistry.get(language); if (tokenizationSupport) { - return _colorize(lines, tabSize, tokenizationSupport); + return _colorize(lines, tabSize, tokenizationSupport, languageIdCodec); } const tokenizationSupportPromise = TokenizationRegistry.getPromise(language); @@ -76,7 +77,7 @@ export class Colorizer { // A tokenizer will be registered soon return new Promise((resolve, reject) => { tokenizationSupportPromise.then(tokenizationSupport => { - _colorize(lines, tabSize, tokenizationSupport).then(resolve, reject); + _colorize(lines, tabSize, tokenizationSupport, languageIdCodec).then(resolve, reject); }, reject); }); } @@ -96,10 +97,10 @@ export class Colorizer { } const tokenizationSupport = TokenizationRegistry.get(language!); if (tokenizationSupport) { - _colorize(lines, tabSize, tokenizationSupport).then(resolve, reject); + _colorize(lines, tabSize, tokenizationSupport, languageIdCodec).then(resolve, reject); return; } - resolve(_fakeColorize(lines, tabSize)); + resolve(_fakeColorize(lines, tabSize, languageIdCodec)); }; // wait 500ms for mode to load, then give up @@ -149,10 +150,10 @@ export class Colorizer { } } -function _colorize(lines: string[], tabSize: number, tokenizationSupport: ITokenizationSupport): Promise { +function _colorize(lines: string[], tabSize: number, tokenizationSupport: ITokenizationSupport, languageIdCodec: ILanguageIdCodec): Promise { return new Promise((c, e) => { const execute = () => { - const result = _actualColorize(lines, tabSize, tokenizationSupport); + const result = _actualColorize(lines, tabSize, tokenizationSupport, languageIdCodec); if (tokenizationSupport instanceof MonarchTokenizer) { const status = tokenizationSupport.getLoadStatus(); if (status.loaded === false) { @@ -166,7 +167,7 @@ function _colorize(lines: string[], tabSize: number, tokenizationSupport: IToken }); } -function _fakeColorize(lines: string[], tabSize: number): string { +function _fakeColorize(lines: string[], tabSize: number, languageIdCodec: ILanguageIdCodec): string { let html: string[] = []; const defaultMetadata = ( @@ -183,7 +184,7 @@ function _fakeColorize(lines: string[], tabSize: number): string { let line = lines[i]; tokens[0] = line.length; - const lineTokens = new LineTokens(tokens, line); + const lineTokens = new LineTokens(tokens, line, languageIdCodec); const isBasicASCII = ViewLineRenderingData.isBasicASCII(line, /* check for basic ASCII */true); const containsRTL = ViewLineRenderingData.containsRTL(line, isBasicASCII, /* check for RTL */true); @@ -216,7 +217,7 @@ function _fakeColorize(lines: string[], tabSize: number): string { return html.join(''); } -function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: ITokenizationSupport): string { +function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: ITokenizationSupport, languageIdCodec: ILanguageIdCodec): string { let html: string[] = []; let state = tokenizationSupport.getInitialState(); @@ -224,7 +225,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: let line = lines[i]; let tokenizeResult = tokenizationSupport.tokenize2(line, true, state, 0); LineTokens.convertToEndOffset(tokenizeResult.tokens, line.length); - let lineTokens = new LineTokens(tokenizeResult.tokens, line); + let lineTokens = new LineTokens(tokenizeResult.tokens, line, languageIdCodec); const isBasicASCII = ViewLineRenderingData.isBasicASCII(line, /* check for basic ASCII */true); const containsRTL = ViewLineRenderingData.containsRTL(line, isBasicASCII, /* check for RTL */true); let renderResult = renderViewLine(new RenderLineInput( diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 5427a9eb58..6bba4d233b 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Token } from 'vs/editor/common/core/token'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { FontStyle, IState, ITokenizationSupport, LanguageIdentifier, StandardTokenType, TokenMetadata, TokenizationRegistry } from 'vs/editor/common/modes'; +import { FontStyle, IState, ITokenizationSupport, StandardTokenType, TokenMetadata, TokenizationRegistry, ILanguageIdCodec } from 'vs/editor/common/modes'; import { NULL_STATE, nullTokenize, nullTokenize2 } from 'vs/editor/common/modes/nullMode'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; @@ -103,7 +103,7 @@ interface ICompleteLineTokenization { } interface IDecodedMetadata { - languageIdentifier: LanguageIdentifier; + languageId: string; tokenType: StandardTokenType; fontStyle: FontStyle; foreground: Color; @@ -130,15 +130,16 @@ function renderTokenText(tokenText: string): string { return result; } -function getSafeTokenizationSupport(languageIdentifier: LanguageIdentifier): ITokenizationSupport { - let tokenizationSupport = TokenizationRegistry.get(languageIdentifier.language); +function getSafeTokenizationSupport(languageIdCodec: ILanguageIdCodec, languageId: string): ITokenizationSupport { + const tokenizationSupport = TokenizationRegistry.get(languageId); if (tokenizationSupport) { return tokenizationSupport; } + const encodedLanguageId = languageIdCodec.encodeLanguageId(languageId); return { getInitialState: () => NULL_STATE, - tokenize: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize(languageIdentifier.language, line, state, deltaOffset), - tokenize2: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(languageIdentifier.id, line, state, deltaOffset) + tokenize: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize(languageId, line, state, deltaOffset), + tokenize2: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(encodedLanguageId, line, state, deltaOffset) }; } @@ -165,7 +166,7 @@ class InspectTokensWidget extends Disposable implements IContentWidget { this._model = this._editor.getModel(); this._domNode = document.createElement('div'); this._domNode.className = 'tokens-inspect-widget'; - this._tokenizationSupport = getSafeTokenizationSupport(this._model.getLanguageIdentifier()); + this._tokenizationSupport = getSafeTokenizationSupport(this._modeService.languageIdCodec, this._model.getLanguageId()); this._compute(this._editor.getPosition()); this._register(this._editor.onDidChangeCursorPosition((e) => this._compute(this._editor.getPosition()))); this._editor.addContentWidget(this); @@ -218,7 +219,7 @@ class InspectTokensWidget extends Disposable implements IContentWidget { $('tbody', undefined, $('tr', undefined, $('td.tm-metadata-key', undefined, 'language'), - $('td.tm-metadata-value', undefined, `${metadata ? metadata.languageIdentifier.language : '-?-'}`) + $('td.tm-metadata-value', undefined, `${metadata ? metadata.languageId : '-?-'}`) ), $('tr', undefined, $('td.tm-metadata-key', undefined, 'token type' as string), @@ -255,7 +256,7 @@ class InspectTokensWidget extends Disposable implements IContentWidget { let foreground = TokenMetadata.getForeground(metadata); let background = TokenMetadata.getBackground(metadata); return { - languageIdentifier: this._modeService.getLanguageIdentifier(languageId)!, + languageId: this._modeService.languageIdCodec.decodeLanguageId(languageId), tokenType: tokenType, fontStyle: fontStyle, foreground: colorMap[foreground], diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts index 5c4e420d78..2fb4d036b5 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts @@ -45,8 +45,8 @@ export class GotoLineAction extends EditorAction { precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_G, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_G }, + primary: KeyMod.CtrlCmd | KeyCode.KeyG, + mac: { primary: KeyMod.WinCtrl | KeyCode.KeyG }, weight: KeybindingWeight.EditorContrib } }); diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts index 4b11d57a9e..fd527726a5 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts @@ -51,7 +51,7 @@ export class GotoLineAction extends EditorAction { precondition: EditorContextKeys.hasDocumentSymbolProvider, kbOpts: { kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO, weight: KeybindingWeight.EditorContrib }, contextMenuOpts: { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 23ffd0d5cb..75ad2589c0 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -7,7 +7,7 @@ import * as strings from 'vs/base/common/strings'; import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Emitter, Event } from 'vs/base/common/event'; -import { Keybinding, ResolvedKeybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes'; +import { Keybinding, ResolvedKeybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keybindings'; import { IDisposable, IReference, ImmortalReference, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { OS, isLinux, isMacintosh } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; @@ -38,7 +38,7 @@ import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayo import { ILabelService, ResourceLabelFormatter, IFormatterChangeEvent } from 'vs/platform/label/common/label'; import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions, NotificationsFilter } from 'vs/platform/notification/common/notification'; import { IProgressRunner, IEditorProgressService } from 'vs/platform/progress/common/progress'; -import { ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryInfo, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, IWorkspaceFoldersWillChangeEvent, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; @@ -94,7 +94,7 @@ export class SimpleModel implements IResolvedTextEditorModel { } public getMode(): string | undefined { - return this.model.getModeId(); + return this.model.getLanguageId(); } } @@ -341,7 +341,7 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { if (keybinding) { this._dynamicKeybindings.push({ - keybinding: keybinding, + keybinding: keybinding.parts, command: commandId, when: when, weight1: 1000, @@ -397,7 +397,7 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { // This might be a removal keybinding item in user settings => accept it result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, null, false); } else { - const resolvedKeybindings = this.resolveKeybinding(keybinding); + const resolvedKeybindings = USLayoutResolvedKeybinding.resolveUserBinding(keybinding, OS); for (const resolvedKeybinding of resolvedKeybindings) { result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, null, false); } @@ -574,7 +574,7 @@ export class SimpleResourcePropertiesService implements ITextResourcePropertiesS export class StandaloneTelemetryService implements ITelemetryService { declare readonly _serviceBrand: undefined; - public isOptedIn = false; + public telemetryLevel = TelemetryLevel.NONE; public sendErrorTelemetry = false; public setEnabled(value: boolean): void { diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index ec348fd00e..50ae9b2f6d 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -5,7 +5,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ICodeEditor, IDiffEditor, IEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions, IEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; @@ -165,12 +165,12 @@ export interface IStandaloneEditorConstructionOptions extends IEditorConstructio model?: ITextModel | null; /** * The initial value of the auto created model in the editor. - * To not create automatically a model, use `model: null`. + * To not automatically create a model, use `model: null`. */ value?: string; /** * The initial language of the auto created model in the editor. - * To not create automatically a model, use `model: null`. + * To not automatically create a model, use `model: null`. */ language?: string; /** @@ -193,12 +193,17 @@ export interface IStandaloneEditorConstructionOptions extends IEditorConstructio * Defaults to "https://go.microsoft.com/fwlink/?linkid=852450" */ accessibilityHelpUrl?: string; + /** + * Container element to use for ARIA messages. + * Defaults to document.body. + */ + ariaContainerElement?: HTMLElement; } /** * The options to create a diff editor. */ -export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { +export interface IStandaloneDiffEditorConstructionOptions extends IDiffEditorConstructionOptions { /** * Initial theme to be used for rendering. * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. @@ -233,12 +238,19 @@ export interface IStandaloneDiffEditor extends IDiffEditor { let LAST_GENERATED_COMMAND_ID = 0; let ariaDomNodeCreated = false; -function createAriaDomNode() { - if (ariaDomNodeCreated) { - return; +/** + * Create ARIA dom node inside parent, + * or only for the first editor instantiation inside document.body. + * @param parent container element for ARIA dom node + */ +function createAriaDomNode(parent: HTMLElement | undefined) { + if (!parent) { + if (ariaDomNodeCreated) { + return; + } + ariaDomNodeCreated = true; } - ariaDomNodeCreated = true; - aria.setARIAContainer(document.body); + aria.setARIAContainer(parent || document.body); } /** @@ -271,8 +283,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon this._standaloneKeybindingService = null; } - // Create the ARIA dom node as soon as the first editor is instantiated - createAriaDomNode(); + createAriaDomNode(options.ariaContainerElement); } public addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null { @@ -482,7 +493,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon constructor( domElement: HTMLElement, - _options: Readonly | undefined, + _options: Readonly | undefined, toDispose: IDisposable, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 7d50642dba..8aee7114e9 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -24,7 +24,7 @@ import { IWebWorkerOptions, MonacoWebWorker, createWebWorker as actualCreateWebW import * as standaloneEnums from 'vs/editor/common/standalone/standaloneEnums'; import { Colorizer, IColorizerElementOptions, IColorizerOptions } from 'vs/editor/standalone/browser/colorizer'; import { SimpleEditorModelResolverService } from 'vs/editor/standalone/browser/simpleServices'; -import { IDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneDiffEditor, StandaloneEditor, createTextModel } from 'vs/editor/standalone/browser/standaloneCodeEditor'; +import { IStandaloneEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneDiffEditor, StandaloneEditor, createTextModel, IStandaloneDiffEditorConstructionOptions } from 'vs/editor/standalone/browser/standaloneCodeEditor'; import { DynamicStandaloneServices, IEditorOverrideServices, StaticServices } from 'vs/editor/standalone/browser/standaloneServices'; import { IStandaloneThemeData, IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -111,7 +111,7 @@ export function onDidCreateEditor(listener: (codeEditor: ICodeEditor) => void): * `domElement` should be empty (not contain other dom nodes). * The editor will read the size of `domElement`. */ -export function createDiffEditor(domElement: HTMLElement, options?: IDiffEditorConstructionOptions, override?: IEditorOverrideServices): IStandaloneDiffEditor { +export function createDiffEditor(domElement: HTMLElement, options?: IStandaloneDiffEditorConstructionOptions, override?: IEditorOverrideServices): IStandaloneDiffEditor { return withAllStandaloneServices(domElement, override || {}, (services) => { return new StandaloneDiffEditor( domElement, diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index de35983c8e..f92bd7c19f 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -14,7 +14,7 @@ import * as modes from 'vs/editor/common/modes'; import { LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; -import { ILanguageExtensionPoint } from 'vs/editor/common/services/modeService'; +import { ILanguageExtensionPoint, IModeService } from 'vs/editor/common/services/modeService'; import * as standaloneEnums from 'vs/editor/common/standalone/standaloneEnums'; import { StaticServices } from 'vs/editor/standalone/browser/standaloneServices'; import { compile } from 'vs/editor/standalone/common/monarch/monarchCompile'; @@ -40,8 +40,8 @@ export function getLanguages(): ILanguageExtensionPoint[] { } export function getEncodedLanguageId(languageId: string): number { - let lid = StaticServices.modeService.get().getLanguageIdentifier(languageId); - return lid ? lid.id : 0; + const modeService = StaticServices.modeService.get(); + return modeService.languageIdCodec.encodeLanguageId(languageId); } /** @@ -49,8 +49,8 @@ export function getEncodedLanguageId(languageId: string): number { * @event */ export function onLanguage(languageId: string, callback: () => void): IDisposable { - let disposable = StaticServices.modeService.get().onDidCreateMode((mode) => { - if (mode.getId() === languageId) { + let disposable = StaticServices.modeService.get().onDidEncounterLanguage((languageId) => { + if (languageId === languageId) { // stop listening disposable.dispose(); // invoke actual listener @@ -64,11 +64,11 @@ export function onLanguage(languageId: string, callback: () => void): IDisposabl * Set the editing configuration for a language. */ export function setLanguageConfiguration(languageId: string, configuration: LanguageConfiguration): IDisposable { - let languageIdentifier = StaticServices.modeService.get().getLanguageIdentifier(languageId); - if (!languageIdentifier) { + const validLanguageId = StaticServices.modeService.get().validateLanguageId(languageId); + if (!validLanguageId) { throw new Error(`Cannot set configuration for unknown language ${languageId}`); } - return LanguageConfigurationRegistry.register(languageIdentifier, configuration, 100); + return LanguageConfigurationRegistry.register(validLanguageId, configuration, 100); } /** @@ -76,11 +76,11 @@ export function setLanguageConfiguration(languageId: string, configuration: Lang */ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSupport { - private readonly _languageIdentifier: modes.LanguageIdentifier; + private readonly _languageId: string; private readonly _actual: EncodedTokensProvider; - constructor(languageIdentifier: modes.LanguageIdentifier, actual: EncodedTokensProvider) { - this._languageIdentifier = languageIdentifier; + constructor(languageId: string, actual: EncodedTokensProvider) { + this._languageId = languageId; this._actual = actual; } @@ -90,7 +90,7 @@ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSu public tokenize(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult { if (typeof this._actual.tokenize === 'function') { - return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, <{ tokenize(line: string, state: modes.IState): ILineTokens; }>this._actual, line, state, offsetDelta); + return TokenizationSupport2Adapter.adaptTokenize(this._languageId, <{ tokenize(line: string, state: modes.IState): ILineTokens; }>this._actual, line, state, offsetDelta); } throw new Error('Not supported!'); } @@ -106,14 +106,12 @@ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSu */ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { - private readonly _standaloneThemeService: IStandaloneThemeService; - private readonly _languageIdentifier: modes.LanguageIdentifier; - private readonly _actual: TokensProvider; - - constructor(standaloneThemeService: IStandaloneThemeService, languageIdentifier: modes.LanguageIdentifier, actual: TokensProvider) { - this._standaloneThemeService = standaloneThemeService; - this._languageIdentifier = languageIdentifier; - this._actual = actual; + constructor( + private readonly _languageId: string, + private readonly _actual: TokensProvider, + private readonly _modeService: IModeService, + private readonly _standaloneThemeService: IStandaloneThemeService, + ) { } public getInitialState(): modes.IState { @@ -159,11 +157,11 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { } public tokenize(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult { - return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, this._actual, line, state, offsetDelta); + return TokenizationSupport2Adapter.adaptTokenize(this._languageId, this._actual, line, state, offsetDelta); } - private _toBinaryTokens(tokens: IToken[], offsetDelta: number): Uint32Array { - const languageId = this._languageIdentifier.id; + private _toBinaryTokens(languageIdCodec: modes.ILanguageIdCodec, tokens: IToken[], offsetDelta: number): Uint32Array { + const languageId = languageIdCodec.encodeLanguageId(this._languageId); const tokenTheme = this._standaloneThemeService.getColorTheme().tokenTheme; let result: number[] = [], resultLen = 0; @@ -202,7 +200,7 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { public tokenize2(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult2 { let actualResult = this._actual.tokenize(line, state); - let tokens = this._toBinaryTokens(actualResult.tokens, offsetDelta); + let tokens = this._toBinaryTokens(this._modeService.languageIdCodec, actualResult.tokens, offsetDelta); let endState: modes.IState; // try to save an object if possible @@ -331,15 +329,20 @@ export function setColorMap(colorMap: string[] | null): void { * Set the tokens provider for a language (manual implementation). */ export function setTokensProvider(languageId: string, provider: TokensProvider | EncodedTokensProvider | Thenable): IDisposable { - let languageIdentifier = StaticServices.modeService.get().getLanguageIdentifier(languageId); - if (!languageIdentifier) { + const validLanguageId = StaticServices.modeService.get().validateLanguageId(languageId); + if (!validLanguageId) { throw new Error(`Cannot set tokens provider for unknown language ${languageId}`); } const create = (provider: TokensProvider | EncodedTokensProvider) => { if (isEncodedTokensProvider(provider)) { - return new EncodedTokenizationSupport2Adapter(languageIdentifier!, provider); + return new EncodedTokenizationSupport2Adapter(validLanguageId, provider); } else { - return new TokenizationSupport2Adapter(StaticServices.standaloneThemeService.get(), languageIdentifier!, provider); + return new TokenizationSupport2Adapter( + validLanguageId, + provider, + StaticServices.modeService.get(), + StaticServices.standaloneThemeService.get(), + ); } }; if (isThenable(provider)) { @@ -459,14 +462,16 @@ export function registerCodeLensProvider(languageId: string, provider: modes.Cod /** * Register a code action provider (used by e.g. quick fix). */ -export function registerCodeActionProvider(languageId: string, provider: CodeActionProvider): IDisposable { +export function registerCodeActionProvider(languageId: string, provider: CodeActionProvider, metadata?: CodeActionProviderMetadata): IDisposable { return modes.CodeActionProviderRegistry.register(languageId, { + providedCodeActionKinds: metadata?.providedCodeActionKinds, provideCodeActions: (model: model.ITextModel, range: Range, context: modes.CodeActionContext, token: CancellationToken): modes.ProviderResult => { let markers = StaticServices.markerService.get().read({ resource: model.uri }).filter(m => { return Range.areIntersectingOrTouching(m, range); }); return provider.provideCodeActions(model, range, { markers, only: context.only }, token); - } + }, + resolveCodeAction: provider.resolveCodeAction }); } @@ -587,6 +592,28 @@ export interface CodeActionProvider { * Provide commands for the given document and range. */ provideCodeActions(model: model.ITextModel, range: Range, context: CodeActionContext, token: CancellationToken): modes.ProviderResult; + + /** + * Given a code action fill in the edit. Will only invoked when missing. + */ + resolveCodeAction?(codeAction: modes.CodeAction, token: CancellationToken): modes.ProviderResult; +} + + + +/** + * Metadata about the type of code actions that a {@link CodeActionProvider} provides. + */ +export interface CodeActionProviderMetadata { + /** + * List of code action kinds 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 `["quickfix", "refactor", "source"]`, or list out every kind provided, + * such as `["quickfix.removeLine", "source.fixAll" ...]`. + */ + readonly providedCodeActionKinds?: readonly string[]; } /** diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index d38503bd9f..28f39cb58b 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -47,13 +47,14 @@ import { MarkerDecorationsService } from 'vs/editor/common/services/markerDecora import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; -import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService'; +import { AccessibilityService } from 'vs/platform/accessibility/browser/accessibilityService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { BrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { StandaloneQuickInputServiceImpl } from 'vs/editor/standalone/browser/quickInput/standaloneQuickInputServiceImpl'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { ILanguageConfigurationService, LanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; export interface IEditorOverrideServices { [index: string]: any; @@ -156,7 +157,21 @@ export module StaticServices { export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o))); - export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o))); + export const languageConfigurationService = define(ILanguageConfigurationService, (o) => new LanguageConfigurationService(configurationService.get(o), modeService.get(o))); + + export const modelService = define( + IModelService, + (o) => + new ModelServiceImpl( + configurationService.get(o), + resourcePropertiesService.get(o), + standaloneThemeService.get(o), + logService.get(o), + undoRedoService.get(o), + modeService.get(o), + languageConfigurationService.get(o) + ) + ); export const markerDecorationsService = define(IMarkerDecorationsService, (o) => new MarkerDecorationsService(modelService.get(o), markerService.get(o))); diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index 92d61afeab..a1d12960ab 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -125,17 +125,17 @@ class MonarchStackElement { } class EmbeddedModeData { - public readonly modeId: string; + public readonly languageId: string; public readonly state: modes.IState; - constructor(modeId: string, state: modes.IState) { - this.modeId = modeId; + constructor(languageId: string, state: modes.IState) { + this.languageId = languageId; this.state = state; } public equals(other: EmbeddedModeData): boolean { return ( - this.modeId === other.modeId + this.languageId === other.languageId && this.state.equals(other.state) ); } @@ -146,7 +146,7 @@ class EmbeddedModeData { if (stateClone === this.state) { return this; } - return new EmbeddedModeData(this.modeId, this.state); + return new EmbeddedModeData(this.languageId, this.state); } } @@ -229,7 +229,7 @@ class MonarchLineState implements modes.IState { } interface IMonarchTokensCollector { - enterMode(startOffset: number, modeId: string): void; + enterMode(startOffset: number, languageId: string): void; emit(startOffset: number, type: string): void; nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState; } @@ -237,32 +237,32 @@ interface IMonarchTokensCollector { class MonarchClassicTokensCollector implements IMonarchTokensCollector { private _tokens: Token[]; - private _language: string | null; + private _languageId: string | null; private _lastTokenType: string | null; private _lastTokenLanguage: string | null; constructor() { this._tokens = []; - this._language = null; + this._languageId = null; this._lastTokenType = null; this._lastTokenLanguage = null; } - public enterMode(startOffset: number, modeId: string): void { - this._language = modeId; + public enterMode(startOffset: number, languageId: string): void { + this._languageId = languageId; } public emit(startOffset: number, type: string): void { - if (this._lastTokenType === type && this._lastTokenLanguage === this._language) { + if (this._lastTokenType === type && this._lastTokenLanguage === this._languageId) { return; } this._lastTokenType = type; - this._lastTokenLanguage = this._language; - this._tokens.push(new Token(startOffset, type, this._language!)); + this._lastTokenLanguage = this._languageId; + this._tokens.push(new Token(startOffset, type, this._languageId!)); } public nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { - const nestedModeId = embeddedModeData.modeId; + const nestedModeId = embeddedModeData.languageId; const embeddedModeState = embeddedModeData.state; const nestedModeTokenizationSupport = modes.TokenizationRegistry.get(nestedModeId); @@ -276,7 +276,7 @@ class MonarchClassicTokensCollector implements IMonarchTokensCollector { this._tokens = this._tokens.concat(nestedResult.tokens); this._lastTokenType = null; this._lastTokenLanguage = null; - this._language = null; + this._languageId = null; return nestedResult.endState; } @@ -303,8 +303,8 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { this._lastTokenMetadata = 0; } - public enterMode(startOffset: number, modeId: string): void { - this._currentLanguageId = this._modeService.getLanguageIdentifier(modeId)!.id; + public enterMode(startOffset: number, languageId: string): void { + this._currentLanguageId = this._modeService.languageIdCodec.encodeLanguageId(languageId); } public emit(startOffset: number, type: string): void { @@ -346,7 +346,7 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { } public nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { - const nestedModeId = embeddedModeData.modeId; + const nestedModeId = embeddedModeData.languageId; const embeddedModeState = embeddedModeData.state; const nestedModeTokenizationSupport = modes.TokenizationRegistry.get(nestedModeId); @@ -378,16 +378,16 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { private readonly _modeService: IModeService; private readonly _standaloneThemeService: IStandaloneThemeService; - private readonly _modeId: string; + private readonly _languageId: string; private readonly _lexer: monarchCommon.ILexer; - private readonly _embeddedModes: { [modeId: string]: boolean; }; + private readonly _embeddedModes: { [languageId: string]: boolean; }; public embeddedLoaded: Promise; private readonly _tokenizationRegistryListener: IDisposable; - constructor(modeService: IModeService, standaloneThemeService: IStandaloneThemeService, modeId: string, lexer: monarchCommon.ILexer) { + constructor(modeService: IModeService, standaloneThemeService: IStandaloneThemeService, languageId: string, lexer: monarchCommon.ILexer) { this._modeService = modeService; this._standaloneThemeService = standaloneThemeService; - this._modeId = modeId; + this._languageId = languageId; this._lexer = lexer; this._embeddedModes = Object.create(null); this.embeddedLoaded = Promise.resolve(undefined); @@ -408,7 +408,7 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { } if (isOneOfMyEmbeddedModes) { emitting = true; - modes.TokenizationRegistry.fire([this._modeId]); + modes.TokenizationRegistry.fire([this._languageId]); emitting = false; } }); @@ -525,7 +525,7 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { if (popOffset === -1) { // tokenization will not leave nested mode let nestedEndState = tokensCollector.nestedModeTokenize(line, hasEOL, lineState.embeddedModeData!, offsetDelta); - return MonarchLineStateFactory.create(lineState.stack, new EmbeddedModeData(lineState.embeddedModeData!.modeId, nestedEndState)); + return MonarchLineStateFactory.create(lineState.stack, new EmbeddedModeData(lineState.embeddedModeData!.languageId, nestedEndState)); } let nestedModeLine = line.substring(0, popOffset); @@ -546,7 +546,7 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { } private _myTokenize(lineWithoutLF: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { - tokensCollector.enterMode(offsetDelta, this._modeId); + tokensCollector.enterMode(offsetDelta, this._languageId); const lineWithoutLFLength = lineWithoutLF.length; const line = (hasEOL && this._lexer.includeLF ? lineWithoutLF + '\n' : lineWithoutLF); @@ -863,20 +863,20 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return null; } - if (mimetypeOrModeId === this._modeId) { + if (mimetypeOrModeId === this._languageId) { // embedding myself... return mimetypeOrModeId; } - let modeId = this._modeService.getModeId(mimetypeOrModeId); + const languageId = this._modeService.getModeId(mimetypeOrModeId); - if (modeId) { + if (languageId) { // Fire mode loading event - this._modeService.triggerMode(modeId); - this._embeddedModes[modeId] = true; + this._modeService.triggerMode(languageId); + this._embeddedModes[languageId] = true; } - return modeId; + return languageId; } } @@ -902,6 +902,6 @@ function findBracket(lexer: monarchCommon.ILexer, matched: string) { return null; } -export function createTokenizationSupport(modeService: IModeService, standaloneThemeService: IStandaloneThemeService, modeId: string, lexer: monarchCommon.ILexer): modes.ITokenizationSupport { - return new MonarchTokenizer(modeService, standaloneThemeService, modeId, lexer); +export function createTokenizationSupport(modeService: IModeService, standaloneThemeService: IStandaloneThemeService, languageId: string, lexer: monarchCommon.ILexer): modes.ITokenizationSupport { + return new MonarchTokenizer(modeService, standaloneThemeService, languageId, lexer); } diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 676809ff4f..4c85d9a7de 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -6,9 +6,12 @@ import * as assert from 'assert'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Token } from 'vs/editor/common/core/token'; -import { IState, LanguageId, LanguageIdentifier, MetadataConsts } from 'vs/editor/common/modes'; +import { IState, LanguageId, MetadataConsts } from 'vs/editor/common/modes'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { TokenTheme } from 'vs/editor/common/modes/supports/tokenization'; +import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ILineTokens, IToken, TokenizationSupport2Adapter, TokensProvider } from 'vs/editor/standalone/browser/standaloneLanguages'; import { IStandaloneTheme, IStandaloneThemeData, IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; @@ -17,8 +20,8 @@ import { IFileIconTheme, IColorTheme, ITokenStyle } from 'vs/platform/theme/comm suite('TokenizationSupport2Adapter', () => { - const languageIdentifier = new LanguageIdentifier('tttt', LanguageId.PlainText); - const tokenMetadata = (languageIdentifier.id << MetadataConsts.LANGUAGEID_OFFSET); + const languageId = 'tttt'; + // const tokenMetadata = (LanguageId.PlainText << MetadataConsts.LANGUAGEID_OFFSET); class MockTokenTheme extends TokenTheme { private counter = 0; @@ -109,7 +112,15 @@ suite('TokenizationSupport2Adapter', () => { } } - const adapter = new TokenizationSupport2Adapter(new MockThemeService(), languageIdentifier, new BadTokensProvider()); + const disposables = new DisposableStore(); + const modeService = disposables.add(new ModeServiceImpl()); + disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + const adapter = new TokenizationSupport2Adapter( + languageId, + new BadTokensProvider(), + modeService, + new MockThemeService() + ); const actualClassicTokens = adapter.tokenize('whatever', true, MockState.INSTANCE, offsetDelta); assert.deepStrictEqual(actualClassicTokens.tokens, expectedClassicTokens); @@ -119,10 +130,19 @@ suite('TokenizationSupport2Adapter', () => { for (let i = 0; i < actualModernTokens.tokens.length; i++) { modernTokens[i] = actualModernTokens.tokens[i]; } + + // Add the encoded language id to the expected tokens + const encodedLanguageId = modeService.languageIdCodec.encodeLanguageId(languageId); + const tokenLanguageMetadata = (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET); + for (let i = 1; i < expectedModernTokens.length; i += 2) { + expectedModernTokens[i] |= tokenLanguageMetadata; + } assert.deepStrictEqual(modernTokens, expectedModernTokens); + + disposables.dispose(); } - test('tokens always start at index 0 (no offset delta)', () => { + test(' (no offset delta)', () => { testBadTokensProvider( [ { startIndex: 7, scopes: 'foo' }, @@ -130,12 +150,12 @@ suite('TokenizationSupport2Adapter', () => { ], 0, [ - new Token(0, 'foo', languageIdentifier.language), - new Token(0, 'bar', languageIdentifier.language), + new Token(0, 'foo', languageId), + new Token(0, 'bar', languageId), ], [ - 0, tokenMetadata | (0 << MetadataConsts.FOREGROUND_OFFSET), - 0, tokenMetadata | (1 << MetadataConsts.FOREGROUND_OFFSET) + 0, (0 << MetadataConsts.FOREGROUND_OFFSET), + 0, (1 << MetadataConsts.FOREGROUND_OFFSET) ] ); }); @@ -149,14 +169,14 @@ suite('TokenizationSupport2Adapter', () => { ], 0, [ - new Token(0, 'foo', languageIdentifier.language), - new Token(5, 'bar', languageIdentifier.language), - new Token(5, 'foo', languageIdentifier.language), + new Token(0, 'foo', languageId), + new Token(5, 'bar', languageId), + new Token(5, 'foo', languageId), ], [ - 0, tokenMetadata | (0 << MetadataConsts.FOREGROUND_OFFSET), - 5, tokenMetadata | (1 << MetadataConsts.FOREGROUND_OFFSET), - 5, tokenMetadata | (2 << MetadataConsts.FOREGROUND_OFFSET) + 0, (0 << MetadataConsts.FOREGROUND_OFFSET), + 5, (1 << MetadataConsts.FOREGROUND_OFFSET), + 5, (2 << MetadataConsts.FOREGROUND_OFFSET) ] ); }); @@ -169,12 +189,12 @@ suite('TokenizationSupport2Adapter', () => { ], 7, [ - new Token(7, 'foo', languageIdentifier.language), - new Token(7, 'bar', languageIdentifier.language), + new Token(7, 'foo', languageId), + new Token(7, 'bar', languageId), ], [ - 7, tokenMetadata | (0 << MetadataConsts.FOREGROUND_OFFSET), - 7, tokenMetadata | (1 << MetadataConsts.FOREGROUND_OFFSET) + 7, (0 << MetadataConsts.FOREGROUND_OFFSET), + 7, (1 << MetadataConsts.FOREGROUND_OFFSET) ] ); }); @@ -188,14 +208,14 @@ suite('TokenizationSupport2Adapter', () => { ], 7, [ - new Token(7, 'foo', languageIdentifier.language), - new Token(12, 'bar', languageIdentifier.language), - new Token(12, 'foo', languageIdentifier.language), + new Token(7, 'foo', languageId), + new Token(12, 'bar', languageId), + new Token(12, 'foo', languageId), ], [ - 7, tokenMetadata | (0 << MetadataConsts.FOREGROUND_OFFSET), - 12, tokenMetadata | (1 << MetadataConsts.FOREGROUND_OFFSET), - 12, tokenMetadata | (2 << MetadataConsts.FOREGROUND_OFFSET) + 7, (0 << MetadataConsts.FOREGROUND_OFFSET), + 12, (1 << MetadataConsts.FOREGROUND_OFFSET), + 12, (2 << MetadataConsts.FOREGROUND_OFFSET) ] ); }); diff --git a/src/vs/editor/standalone/test/monarch/monarch.test.ts b/src/vs/editor/standalone/test/monarch/monarch.test.ts index 5dc54173a9..79edcbd052 100644 --- a/src/vs/editor/standalone/test/monarch/monarch.test.ts +++ b/src/vs/editor/standalone/test/monarch/monarch.test.ts @@ -106,6 +106,7 @@ suite('Monarch', () => { ]); innerModeTokenizationRegistration.dispose(); innerModeRegistration.dispose(); + modeService.dispose(); }); test('microsoft/monaco-editor#1235: Empty Line Handling', () => { @@ -161,6 +162,7 @@ suite('Monarch', () => { [], [new Token(0, 'source.test', 'test')] ]); + modeService.dispose(); }); @@ -209,6 +211,7 @@ suite('Monarch', () => { new Token(18, 'number.test', 'test'), ] ]); + modeService.dispose(); }); test('issue #115662: monarchCompile function need an extra option which can control replacement', () => { @@ -262,6 +265,7 @@ suite('Monarch', () => { new Token(0, 'ham.test', 'test'), ] ]); + modeService.dispose(); }); test('microsoft/monaco-editor#2424: Allow to target @@', () => { @@ -289,6 +293,7 @@ suite('Monarch', () => { new Token(0, 'ham.test', 'test'), ] ]); + modeService.dispose(); }); }); diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index de54c9ea2e..b5f5b13bd3 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -8,7 +8,6 @@ import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { getEditOperation, testCommand } from 'vs/editor/test/browser/testCommand'; import { withEditorModel } from 'vs/editor/test/common/editorTestUtils'; @@ -29,11 +28,11 @@ export function createSingleEditOp(text: string, positionLineNumber: number, pos class DocBlockCommentMode extends MockMode { - private static readonly _id = new LanguageIdentifier('commentMode', 3); + private static readonly _id = 'commentMode'; constructor() { super(DocBlockCommentMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { brackets: [ ['(', ')'], ['{', '}'], @@ -45,8 +44,8 @@ class DocBlockCommentMode extends MockMode { } } -function testShiftCommand(lines: string[], languageIdentifier: LanguageIdentifier | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, languageIdentifier, selection, (sel) => new ShiftCommand(sel, { +function testShiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection): void { + testCommand(lines, languageId, selection, (sel) => new ShiftCommand(sel, { isUnshift: false, tabSize: 4, indentSize: 4, @@ -56,8 +55,8 @@ function testShiftCommand(lines: string[], languageIdentifier: LanguageIdentifie }), expectedLines, expectedSelection); } -function testUnshiftCommand(lines: string[], languageIdentifier: LanguageIdentifier | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, languageIdentifier, selection, (sel) => new ShiftCommand(sel, { +function testUnshiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection): void { + testCommand(lines, languageId, selection, (sel) => new ShiftCommand(sel, { isUnshift: true, tabSize: 4, indentSize: 4, @@ -565,7 +564,7 @@ suite('Editor Commands - ShiftCommand', () => { ' */', 'function hello() {}' ], - mode.getLanguageIdentifier(), + mode.languageId, true, new Selection(1, 1, 5, 20), [ @@ -586,7 +585,7 @@ suite('Editor Commands - ShiftCommand', () => { ' */', 'function hello() {}' ], - mode.getLanguageIdentifier(), + mode.languageId, true, new Selection(1, 1, 5, 20), [ @@ -607,7 +606,7 @@ suite('Editor Commands - ShiftCommand', () => { '\t */', '\tfunction hello() {}' ], - mode.getLanguageIdentifier(), + mode.languageId, true, new Selection(1, 1, 5, 21), [ @@ -635,7 +634,7 @@ suite('Editor Commands - ShiftCommand', () => { ' */', 'var foo = 0;' ], - mode.getLanguageIdentifier(), + mode.languageId, true, new Selection(1, 1, 7, 13), [ diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 4e50a64d98..392ab34557 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -14,7 +14,7 @@ import { TokenizationResult2 } from 'vs/editor/common/core/token'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, EndOfLineSequence, ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { IState, ITokenizationSupport, LanguageIdentifier, TokenizationRegistry } from 'vs/editor/common/modes'; +import { IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/modes'; import { IndentAction, IndentationRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; @@ -1272,22 +1272,22 @@ suite('Editor Controller - Cursor', () => { class SurroundingMode extends MockMode { - private static readonly _id = new LanguageIdentifier('surroundingMode', 3); + private static readonly _id = 'surroundingMode'; constructor() { super(SurroundingMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { autoClosingPairs: [{ open: '(', close: ')' }] })); } } class OnEnterMode extends MockMode { - private static readonly _id = new LanguageIdentifier('onEnterMode', 3); + private static readonly _id = 'onEnterMode'; constructor(indentAction: IndentAction) { super(OnEnterMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { onEnterRules: [{ beforeText: /.*/, action: { @@ -1299,10 +1299,10 @@ class OnEnterMode extends MockMode { } class IndentRulesMode extends MockMode { - private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); + private static readonly _id = 'indentRulesMode'; constructor(indentationRules: IndentationRule) { super(IndentRulesMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { indentationRules: indentationRules })); } @@ -1413,11 +1413,11 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #47733: Undo mangles unicode characters', () => { - const languageId = new LanguageIdentifier('myMode', 3); + const languageId = 'myMode'; class MyMode extends MockMode { constructor() { super(languageId); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { surroundingPairs: [{ open: '%', close: '%' }] })); } @@ -1505,7 +1505,7 @@ suite('Editor Controller - Regression tests', () => { ' function baz() {' ].join('\n'), undefined, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { @@ -1628,13 +1628,33 @@ suite('Editor Controller - Regression tests', () => { }); }); + test('issue #128602: When cutting multiple lines (ctrl x), the last line will not be erased', () => { + withTestCodeEditor([ + 'a1', + 'a2', + 'a3' + ], {}, (editor, viewModel) => { + const model = editor.getModel()!; + + viewModel.setSelections('test', [ + new Selection(1, 1, 1, 1), + new Selection(2, 1, 2, 1), + new Selection(3, 1, 3, 1), + ]); + + viewModel.cut('keyboard'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), ''); + }); + }); + test('Bug #11476: Double bracket surrounding + undo is broken', () => { let mode = new SurroundingMode(); usingCursor({ text: [ 'hello' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 3, false); moveTo(editor, viewModel, 1, 5, true); @@ -1920,7 +1940,7 @@ suite('Editor Controller - Regression tests', () => { 'and more lines', 'just some text', ], - languageIdentifier: null + languageId: null }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 1, false); @@ -2428,7 +2448,7 @@ suite('Editor Controller - Regression tests', () => { const LANGUAGE_ID = 'modelModeTest1'; const languageRegistration = TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); - let model = createTextModel('Just text', undefined, new LanguageIdentifier(LANGUAGE_ID, 0)); + let model = createTextModel('Just text', undefined, LANGUAGE_ID); withTestCodeEditor(null, { model: model }, (editor1, cursor1) => { withTestCodeEditor(null, { model: model }, (editor2, cursor2) => { @@ -2727,6 +2747,8 @@ suite('Editor Controller - Regression tests', () => { new Selection(1, 32, 1, 33) ]); }); + + model.dispose(); }); test('issue #105730: move right should always skip wrap point', () => { @@ -2759,6 +2781,8 @@ suite('Editor Controller - Regression tests', () => { ]); } ); + + model.dispose(); }); test('issue #123178: sticky tab in consecutive wrapped lines', () => { @@ -2787,6 +2811,8 @@ suite('Editor Controller - Regression tests', () => { ]); } ); + + model.dispose(); }); }); @@ -2889,7 +2915,7 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ '\thello' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 7, false); assertCursor(viewModel, new Selection(1, 7, 1, 7)); @@ -2906,7 +2932,7 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ '\thello' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 7, false); assertCursor(viewModel, new Selection(1, 7, 1, 7)); @@ -2923,7 +2949,7 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ '\thell()' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 7, false); assertCursor(viewModel, new Selection(1, 7, 1, 7)); @@ -2979,8 +3005,8 @@ suite('Editor Controller - Cursor Configuration', () => { test('issue #115033: indent and appendText', () => { const mode = new class extends MockMode { constructor() { - super(new LanguageIdentifier('onEnterMode', 3)); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + super('onEnterMode'); + this._register(LanguageConfigurationRegistry.register(this.languageId, { onEnterRules: [{ beforeText: /.*/, action: { @@ -2995,7 +3021,7 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ 'text' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 5); @@ -3013,7 +3039,7 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ 'function foo (params: string) {}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 32); @@ -3405,7 +3431,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\tif (true) {' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -3430,7 +3456,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\t' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 2, false); @@ -3448,7 +3474,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\t\t\treturn true' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -3468,7 +3494,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true)', '\t\t\t\treturn true' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -3496,7 +3522,7 @@ suite('Editor Controller - Indentation Rules', () => { { insertSpaces: false, }, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, viewModel) => { @@ -3523,7 +3549,7 @@ suite('Editor Controller - Indentation Rules', () => { 'return true;', '}}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 13, false); @@ -3543,7 +3569,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\treturn true;', '\t}a}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 3, false); @@ -3562,7 +3588,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\tif (true) {' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 12, false); @@ -3583,7 +3609,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\tif (true) {' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 12, false); assertCursor(viewModel, new Selection(1, 12, 1, 12)); @@ -3607,7 +3633,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', ' if (true) {' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 12, false); assertCursor(viewModel, new Selection(1, 12, 1, 12)); @@ -3631,7 +3657,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', ' if (true) {' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 12, false); @@ -3660,7 +3686,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t}', '\t}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -3681,7 +3707,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\treturn true;', '\t}a}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 9, false); @@ -3701,7 +3727,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\treturn true;', '\t}a}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3, false); @@ -3721,7 +3747,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return true;', ' }a}' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 11, false); assertCursor(viewModel, new Selection(3, 11, 3, 11)); @@ -3740,7 +3766,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\treturn true;', '\t}a}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 2, false); @@ -3767,7 +3793,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t \treturn true;', '\t\t}a}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 4, false); @@ -3794,7 +3820,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return true;', '}a}' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 2, false); assertCursor(viewModel, new Selection(3, 2, 3, 2)); @@ -3823,7 +3849,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t return true;', '}a}' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { tabSize: 2, indentSize: 2 @@ -3852,7 +3878,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return true;', '' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, modelOpts: { tabSize: 2 } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 5, false); @@ -3876,7 +3902,7 @@ suite('Editor Controller - Indentation Rules', () => { modelOpts: { insertSpaces: false, }, - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 8, false); moveTo(editor, viewModel, 2, 12, true); @@ -3899,7 +3925,7 @@ suite('Editor Controller - Indentation Rules', () => { modelOpts: { insertSpaces: false, }, - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 12, false); moveTo(editor, viewModel, 3, 8, true); @@ -3964,7 +3990,7 @@ suite('Editor Controller - Indentation Rules', () => { { insertSpaces: false, }, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { @@ -3992,7 +4018,7 @@ suite('Editor Controller - Indentation Rules', () => { { insertSpaces: false, }, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { @@ -4020,7 +4046,7 @@ suite('Editor Controller - Indentation Rules', () => { { insertSpaces: false, }, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { @@ -4047,7 +4073,7 @@ suite('Editor Controller - Indentation Rules', () => { { insertSpaces: false, }, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { @@ -4074,7 +4100,7 @@ suite('Editor Controller - Indentation Rules', () => { { insertSpaces: false, }, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { @@ -4099,7 +4125,7 @@ suite('Editor Controller - Indentation Rules', () => { ' }' ].join('\n'), undefined, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { @@ -4129,7 +4155,7 @@ suite('Editor Controller - Indentation Rules', () => { ' en' ].join('\n'), undefined, - rubyMode.getLanguageIdentifier() + rubyMode.languageId ); withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, viewModel) => { @@ -4153,7 +4179,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\tconsole.log()', '\t}' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 5, 3, false); assertCursor(viewModel, new Selection(5, 3, 5, 3)); @@ -4174,7 +4200,7 @@ suite('Editor Controller - Indentation Rules', () => { ') {', '}' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3, false); assertCursor(viewModel, new Selection(2, 3, 2, 3)); @@ -4192,7 +4218,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t// {', '\t\t' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3, false); @@ -4206,10 +4232,10 @@ suite('Editor Controller - Indentation Rules', () => { test('issue #36090: JS: editor.autoIndent seems to be broken', () => { class JSMode extends MockMode { - private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); + private static readonly _id = 'indentRulesMode'; constructor() { super(JSMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { brackets: [ ['{', '}'], ['[', ']'], @@ -4239,7 +4265,7 @@ suite('Editor Controller - Indentation Rules', () => { '}', ].join('\n'), undefined, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, viewModel) => { @@ -4269,10 +4295,10 @@ suite('Editor Controller - Indentation Rules', () => { test('issue #115304: OnEnter broken for TS', () => { class JSMode extends MockMode { - private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); + private static readonly _id = 'indentRulesMode'; constructor() { super(JSMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { onEnterRules: javascriptOnEnterRules })); } @@ -4285,7 +4311,7 @@ suite('Editor Controller - Indentation Rules', () => { 'function f() {}', ].join('\n'), undefined, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, viewModel) => { @@ -4310,10 +4336,10 @@ suite('Editor Controller - Indentation Rules', () => { test('issue #38261: TAB key results in bizarre indentation in C++ mode ', () => { class CppMode extends MockMode { - private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); + private static readonly _id = 'indentRulesMode'; constructor() { super(CppMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { brackets: [ ['{', '}'], ['[', ']'], @@ -4344,7 +4370,7 @@ suite('Editor Controller - Indentation Rules', () => { tabSize: 2, indentSize: 2 }, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, viewModel) => { @@ -4377,10 +4403,10 @@ suite('Editor Controller - Indentation Rules', () => { text: [ 'Project:', ], - languageIdentifier: (new IndentRulesMode({ + languageId: (new IndentRulesMode({ decreaseIndentPattern: /^\s*}$/gm, increaseIndentPattern: /^(?![^\S\n]*(?!--|––|——)(?:[-❍❑■⬜□☐▪▫–—≡→›✘xX✔✓☑+]|\[[ xX+-]?\])\s[^\n]*)[^\S\n]*(.+:)[^\S\n]*(?:(?=@[^\s*~(]+(?::\/\/[^\s*~(:]+)?(?:\([^)]*\))?)|$)/gm, - })).getLanguageIdentifier(), + })).languageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -4401,10 +4427,10 @@ suite('Editor Controller - Indentation Rules', () => { test('', () => { class JSONMode extends MockMode { - private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); + private static readonly _id = 'indentRulesMode'; constructor() { super(JSONMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { brackets: [ ['{', '}'], ['[', ']'], @@ -4434,7 +4460,7 @@ suite('Editor Controller - Indentation Rules', () => { tabSize: 2, indentSize: 2 }, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, viewModel) => { @@ -4469,7 +4495,7 @@ suite('Editor Controller - Indentation Rules', () => { }); test('issue #111128: Multicursor `Enter` issue with indentation', () => { - const model = createTextModel(' let a, b, c;', { detectIndentation: false, insertSpaces: false, tabSize: 4 }, mode.getLanguageIdentifier()); + const model = createTextModel(' let a, b, c;', { detectIndentation: false, insertSpaces: false, tabSize: 4 }, mode.languageId); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { editor.setSelections([ new Selection(1, 11, 1, 11), @@ -4478,6 +4504,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), ' let a,\n\t b,\n\t c;'); }); + model.dispose(); }); test('issue #122714: tabSize=1 prevent typing a string matching decreaseIndentPattern in an empty file', () => { @@ -4488,7 +4515,7 @@ suite('Editor Controller - Indentation Rules', () => { let model = createTextModel( '\\end', { tabSize: 1 }, - latexMode.getLanguageIdentifier() + latexMode.languageId ); withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, viewModel) => { @@ -4506,27 +4533,28 @@ suite('Editor Controller - Indentation Rules', () => { interface ICursorOpts { text: string[]; - languageIdentifier?: LanguageIdentifier | null; + languageId?: string | null; modelOpts?: IRelaxedTextModelCreationOptions; editorOpts?: IEditorOptions; } function usingCursor(opts: ICursorOpts, callback: (editor: ITestCodeEditor, model: TextModel, viewModel: ViewModel) => void): void { - const model = createTextModel(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier); + const model = createTextModel(opts.text.join('\n'), opts.modelOpts, opts.languageId); const editorOptions: TestCodeEditorCreationOptions = opts.editorOpts || {}; editorOptions.model = model; withTestCodeEditor(null, editorOptions, (editor, viewModel) => { callback(editor, model, viewModel); }); + model.dispose(); } class ElectricCharMode extends MockMode { - private static readonly _id = new LanguageIdentifier('electricCharMode', 3); + private static readonly _id = 'electricCharMode'; constructor() { super(ElectricCharMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { __electricCharacterSupport: { docComment: { open: '/**', close: ' */' } }, @@ -4547,7 +4575,7 @@ suite('ElectricCharacter', () => { ' if (a) {', '' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('*', 'keyboard'); @@ -4563,7 +4591,7 @@ suite('ElectricCharacter', () => { ' if (a) {', '' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); @@ -4579,7 +4607,7 @@ suite('ElectricCharacter', () => { ' if (a) {', ' ' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('}', 'keyboard'); @@ -4597,7 +4625,7 @@ suite('ElectricCharacter', () => { ' }', ' ' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 1); viewModel.type('}', 'keyboard'); @@ -4615,7 +4643,7 @@ suite('ElectricCharacter', () => { ' }', ' } ' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 6); viewModel.type('}', 'keyboard'); @@ -4631,7 +4659,7 @@ suite('ElectricCharacter', () => { ' if (a) {', '// hello' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); @@ -4647,7 +4675,7 @@ suite('ElectricCharacter', () => { ' if (a) {', ' ' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('}', 'keyboard'); @@ -4663,7 +4691,7 @@ suite('ElectricCharacter', () => { ' if (a) {', 'a' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 2); viewModel.type('}', 'keyboard'); @@ -4680,7 +4708,7 @@ suite('ElectricCharacter', () => { ' ( 1 + 2 ) ', '})' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 13); viewModel.type('*', 'keyboard'); @@ -4695,7 +4723,7 @@ suite('ElectricCharacter', () => { text: [ '(div', ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 5); let changeText: string | null = null; @@ -4717,7 +4745,7 @@ suite('ElectricCharacter', () => { '\t2', '\t3' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3); viewModel.type(')', 'keyboard'); @@ -4733,7 +4761,7 @@ suite('ElectricCharacter', () => { ' if (a) {', '/*' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('*', 'keyboard'); @@ -4749,7 +4777,7 @@ suite('ElectricCharacter', () => { ' if (a) {', ' /*' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('*', 'keyboard'); @@ -4765,7 +4793,7 @@ suite('ElectricCharacter', () => { '{', 'word' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); moveTo(editor, viewModel, 2, 1, true); @@ -4780,11 +4808,11 @@ suite('autoClosingPairs', () => { class AutoClosingMode extends MockMode { - private static readonly _id = new LanguageIdentifier('autoClosingMode', 5); + private static readonly _id = 'autoClosingMode'; constructor() { super(AutoClosingMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { autoClosingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, @@ -4802,7 +4830,7 @@ suite('autoClosingPairs', () => { } public setAutocloseEnabledSet(chars: string) { - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { autoCloseBefore: chars, autoClosingPairs: [ { open: '{', close: '}' }, @@ -4850,6 +4878,18 @@ suite('autoClosingPairs', () => { model.undo(); } + test('issue #61070: backtick (`) should auto-close after a word character', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: ['const markup = highlight'], + languageId: mode.languageId + }, (editor, model, viewModel) => { + model.forceTokenization(1); + assertType(editor, model, viewModel, 1, 25, '`', '``', `auto closes \` @ (1, 25)`); + }); + mode.dispose(); + }); + test('open parens: default', () => { let mode = new AutoClosingMode(); usingCursor({ @@ -4863,7 +4903,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { let autoClosePositions = [ @@ -4906,7 +4946,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoClosingBrackets: 'beforeWhitespace' } @@ -4945,7 +4985,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = [];', ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoClosingBrackets: 'beforeWhitespace', autoClosingQuotes: 'never' @@ -4975,7 +5015,7 @@ suite('autoClosingPairs', () => { text: [ 'var b = [];', ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoClosingBrackets: 'never', autoClosingQuotes: 'beforeWhitespace' @@ -5017,7 +5057,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoClosingBrackets: 'languageDefined' } @@ -5063,7 +5103,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoClosingBrackets: 'never', autoClosingQuotes: 'never' @@ -5105,7 +5145,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = asd' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [ @@ -5128,7 +5168,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = asd' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoSurround: 'never' } @@ -5148,7 +5188,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = asd' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoSurround: 'quotes' } @@ -5171,7 +5211,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = asd' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoSurround: 'brackets' } @@ -5205,7 +5245,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { let autoClosePositions = [ @@ -5243,7 +5283,7 @@ suite('autoClosingPairs', () => { text: [ '', ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { model.setValue('begi'); @@ -5260,11 +5300,11 @@ suite('autoClosingPairs', () => { }); test('issue #72177: multi-character autoclose with conflicting patterns', () => { - const languageId = new LanguageIdentifier('autoClosingModeMultiChar', 5); + const languageId = 'autoClosingModeMultiChar'; class AutoClosingModeMultiChar extends MockMode { constructor() { super(languageId); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { autoClosingPairs: [ { open: '(', close: ')' }, { open: '(*', close: '*)' }, @@ -5281,7 +5321,7 @@ suite('autoClosingPairs', () => { text: [ '', ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { viewModel.type('(', 'keyboard'); assert.strictEqual(model.getLineContent(1), '()'); @@ -5305,11 +5345,11 @@ suite('autoClosingPairs', () => { }); test('issue #55314: Do not auto-close when ending with open', () => { - const languageId = new LanguageIdentifier('myElectricMode', 5); + const languageId = 'myElectricMode'; class ElectricMode extends MockMode { constructor() { super(languageId); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { autoClosingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, @@ -5333,7 +5373,7 @@ suite('autoClosingPairs', () => { 'little sheep', 'Big LAMB' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { model.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 1, 4, '"', '"', `does not double quote when ending with open`); @@ -5355,7 +5395,7 @@ suite('autoClosingPairs', () => { text: [ 'var arr = ["b", "c"];' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertType(editor, model, viewModel, 1, 12, '"', '"', `does not over type and will not auto close`); }); @@ -5368,7 +5408,7 @@ suite('autoClosingPairs', () => { text: [ '', ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { function typeCharacters(viewModel: ViewModel, chars: string): void { @@ -5435,7 +5475,7 @@ suite('autoClosingPairs', () => { '', 'y=();' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5465,7 +5505,7 @@ suite('autoClosingPairs', () => { '', 'y=();' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5486,7 +5526,7 @@ suite('autoClosingPairs', () => { '', 'y=();' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5510,7 +5550,7 @@ suite('autoClosingPairs', () => { '', 'y=();' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5536,7 +5576,7 @@ suite('autoClosingPairs', () => { '', 'y=();' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5570,7 +5610,7 @@ suite('autoClosingPairs', () => { text: [ 'std::cout << \'"\' << entryMap' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 29, 1, 29)]); @@ -5593,11 +5633,11 @@ suite('autoClosingPairs', () => { }); test('issue #85983 - editor.autoClosingBrackets: beforeWhitespace is incorrect for Python', () => { - const languageId = new LanguageIdentifier('pythonMode', 5); + const languageId = 'pythonMode'; class PythonMode extends MockMode { constructor() { super(languageId); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { autoClosingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, @@ -5630,7 +5670,7 @@ suite('autoClosingPairs', () => { text: [ 'foo\'hello\'' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertType(editor, model, viewModel, 1, 4, '(', '(', `does not auto close @ (1, 4)`); }); @@ -5643,7 +5683,7 @@ suite('autoClosingPairs', () => { text: [ '
{ viewModel.setSelections('test', [new Selection(1, 8, 1, 8)]); @@ -5666,7 +5706,7 @@ suite('autoClosingPairs', () => { '', 'y=();' ], - languageIdentifier: mode.getLanguageIdentifier(), + languageId: mode.languageId, editorOpts: { autoClosingOvertype: 'always' } @@ -5695,7 +5735,7 @@ suite('autoClosingPairs', () => { usingCursor({ text: [ ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5716,7 +5756,7 @@ suite('autoClosingPairs', () => { text: [ 'test' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 1, 1, 5)]); @@ -5738,7 +5778,7 @@ suite('autoClosingPairs', () => { text: [ 'console.log();' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 13, 1, 13)]); @@ -5764,7 +5804,7 @@ suite('autoClosingPairs', () => { text: [ '' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); @@ -5794,7 +5834,7 @@ suite('autoClosingPairs', () => { 'hello', 'world' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5819,7 +5859,7 @@ suite('autoClosingPairs', () => { text: [ '' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5886,7 +5926,7 @@ suite('autoClosingPairs', () => { text: [ '{}' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 2, 1, 2)]); @@ -5906,7 +5946,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = asd' ], - languageIdentifier: mode.getLanguageIdentifier() + languageId: mode.languageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [ @@ -5923,11 +5963,11 @@ suite('autoClosingPairs', () => { }); test('issue #41825: Special handling of quotes in surrounding pairs', () => { - const languageId = new LanguageIdentifier('myMode', 3); + const languageId = 'myMode'; class MyMode extends MockMode { constructor() { super(languageId); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { surroundingPairs: [ { open: '"', close: '"' }, { open: '\'', close: '\'' }, @@ -5966,7 +6006,7 @@ suite('autoClosingPairs', () => { 'var a = ()' ].join('\n'), TextModel.DEFAULT_CREATION_OPTIONS, - mode.getLanguageIdentifier() + mode.languageId ); withTestCodeEditor(null, { model: model }, (editor, viewModel) => { @@ -6009,6 +6049,8 @@ suite('autoClosingPairs', () => { }); assertCursor(viewModel, new Selection(3, 7, 4, 7)); }); + + model.dispose(); }); }); @@ -6041,6 +6083,8 @@ suite('Undo stops', () => { assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); + + model.dispose(); }); test('there is an undo stop between typing and deleting right', () => { @@ -6070,6 +6114,8 @@ suite('Undo stops', () => { assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); + + model.dispose(); }); test('there is an undo stop between deleting left and typing', () => { @@ -6104,6 +6150,8 @@ suite('Undo stops', () => { assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); + + model.dispose(); }); test('there is an undo stop between deleting left and deleting right', () => { @@ -6142,6 +6190,8 @@ suite('Undo stops', () => { assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); + + model.dispose(); }); test('there is an undo stop between deleting right and typing', () => { @@ -6173,6 +6223,8 @@ suite('Undo stops', () => { assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); + + model.dispose(); }); test('there is an undo stop between deleting right and deleting left', () => { @@ -6209,6 +6261,8 @@ suite('Undo stops', () => { assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); + + model.dispose(); }); test('inserts undo stop when typing space', () => { @@ -6237,6 +6291,8 @@ suite('Undo stops', () => { assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); + + model.dispose(); }); test('can undo typing and EOL change in one undo stop', () => { @@ -6261,6 +6317,8 @@ suite('Undo stops', () => { assert.strictEqual(model.getValue(), 'A line\nAnother line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); + + model.dispose(); }); test('issue #93585: Undo multi cursor edit corrupts document', () => { @@ -6282,6 +6340,8 @@ suite('Undo stops', () => { CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(), 'hello world\nhello world'); }); + + model.dispose(); }); test('there is a single undo stop for consecutive whitespaces', () => { @@ -6313,6 +6373,8 @@ suite('Undo stops', () => { CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), '', 'assert4'); }); + + model.dispose(); }); test('there is no undo stop after a single whitespace', () => { @@ -6340,5 +6402,7 @@ suite('Undo stops', () => { CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), '', 'assert4'); }); + + model.dispose(); }); }); diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 94671bfae6..ac9edde087 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, IActiveCodeEditor, IEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { IEditorContributionCtor } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -11,28 +11,44 @@ import { View } from 'vs/editor/browser/view/viewImpl'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { IConfiguration, IEditorContribution } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { TestCodeEditorService, TestCommandService } from 'vs/editor/test/browser/editorTestServices'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { createTextModel2 } from 'vs/editor/test/common/editorTestUtils'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; -import { BrandedService, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { BrandedService, IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { NullTelemetryServiceShape } from 'vs/platform/telemetry/common/telemetryUtils'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; export interface ITestCodeEditor extends IActiveCodeEditor { getViewModel(): ViewModel | undefined; registerAndInstantiateContribution(id: string, ctor: new (editor: ICodeEditor, ...services: Services) => T): T; + registerDisposable(disposable: IDisposable): void; } export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { @@ -63,6 +79,9 @@ export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { this._contributions[id] = r; return r; } + public registerDisposable(disposable: IDisposable): void { + this._register(disposable); + } } class TestCodeEditorWithAutoModelDisposal extends TestCodeEditor { @@ -97,79 +116,97 @@ export interface TestCodeEditorCreationOptions extends editorOptions.IEditorOpti hasTextFocus?: boolean; } -export function withTestCodeEditor(text: string | string[] | null, options: TestCodeEditorCreationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { - // create a model if necessary and remember it in order to dispose it. +export function withTestCodeEditor(text: string | string[] | ITextBufferFactory | null, options: TestCodeEditorCreationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { + const [instantiationService, disposables] = createTestCodeEditorServices(options.serviceCollection); + delete options.serviceCollection; + + // create a model if necessary if (!options.model) { - if (typeof text === 'string') { - options.model = createTextModel(text); - } else if (text) { - options.model = createTextModel(text.join('\n')); + if (Array.isArray(text)) { + options.model = disposables.add(createTextModel2(instantiationService, text.join('\n'))); + } else if (text !== null) { + options.model = disposables.add(createTextModel2(instantiationService, text)); } } - const editor = createTestCodeEditor(options); + const editor = disposables.add(doCreateTestCodeEditor(instantiationService, options)); const viewModel = editor.getViewModel()!; viewModel.setHasFocus(true); callback(editor, editor.getViewModel()!); - editor.dispose(); + disposables.dispose(); } -export async function withAsyncTestCodeEditor(text: string | string[] | null, options: TestCodeEditorCreationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: IInstantiationService) => Promise): Promise { - // create a model if necessary and remember it in order to dispose it. - let model: TextModel | undefined; +export async function withAsyncTestCodeEditor(text: string | string[] | ITextBufferFactory | null, options: TestCodeEditorCreationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel, instantiationService: IInstantiationService) => Promise): Promise { + const [instantiationService, disposables] = createTestCodeEditorServices(options.serviceCollection); + delete options.serviceCollection; + + // create a model if necessary if (!options.model) { - if (typeof text === 'string') { - model = options.model = createTextModel(text); - } else if (text) { - model = options.model = createTextModel(text.join('\n')); + if (Array.isArray(text)) { + options.model = disposables.add(createTextModel2(instantiationService, text.join('\n'))); + } else if (text !== null) { + options.model = disposables.add(createTextModel2(instantiationService, text)); } } - const [instantiationService, editor, disposable] = doCreateTestCodeEditor(options); + const editor = disposables.add(doCreateTestCodeEditor(instantiationService, options)); const viewModel = editor.getViewModel()!; viewModel.setHasFocus(true); await callback(editor, editor.getViewModel()!, instantiationService); - editor.dispose(); - model?.dispose(); - disposable.dispose(); + disposables.dispose(); } export function createTestCodeEditor(options: TestCodeEditorCreationOptions): ITestCodeEditor { - const [, editor] = doCreateTestCodeEditor(options); + const [instantiationService, disposables] = createTestCodeEditorServices(options.serviceCollection); + delete options.serviceCollection; + + const editor = doCreateTestCodeEditor(instantiationService, options); + editor.registerDisposable(disposables); return editor; } -function doCreateTestCodeEditor(options: TestCodeEditorCreationOptions): [IInstantiationService, ITestCodeEditor, IDisposable] { - const store = new DisposableStore(); +export function createTestCodeEditorServices(services: ServiceCollection = new ServiceCollection()): [IInstantiationService, DisposableStore] { + const serviceIdentifiers: ServiceIdentifier[] = []; + const define = (id: ServiceIdentifier, ctor: new (...args: any[]) => T) => { + if (!services.has(id)) { + services.set(id, new SyncDescriptor(ctor)); + } + serviceIdentifiers.push(id); + }; - const model = options.model; - delete options.model; - - const services: ServiceCollection = options.serviceCollection || new ServiceCollection(); - delete options.serviceCollection; + define(INotificationService, TestNotificationService); + define(IDialogService, TestDialogService); + define(IUndoRedoService, UndoRedoService); + define(IModeService, ModeServiceImpl); + define(ILanguageConfigurationService, TestLanguageConfigurationService); + define(IConfigurationService, TestConfigurationService); + define(ITextResourcePropertiesService, TestTextResourcePropertiesService); + define(IThemeService, TestThemeService); + define(ILogService, NullLogService); + define(IModelService, ModelServiceImpl); + define(ICodeEditorService, TestCodeEditorService); + define(IContextKeyService, MockContextKeyService); + define(ICommandService, TestCommandService); + define(ITelemetryService, NullTelemetryServiceShape); const instantiationService: IInstantiationService = new InstantiationService(services); + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => { + for (const id of serviceIdentifiers) { + const instanceOrDescriptor = services.get(id); + if (typeof instanceOrDescriptor.dispose === 'function') { + instanceOrDescriptor.dispose(); + } + } + })); + return [instantiationService, disposables]; +} - if (!services.has(ICodeEditorService)) { - services.set(ICodeEditorService, store.add(new TestCodeEditorService())); - } - if (!services.has(IContextKeyService)) { - services.set(IContextKeyService, store.add(new MockContextKeyService())); - } - if (!services.has(INotificationService)) { - services.set(INotificationService, new TestNotificationService()); - } - if (!services.has(ICommandService)) { - services.set(ICommandService, new TestCommandService(instantiationService)); - } - if (!services.has(IThemeService)) { - services.set(IThemeService, new TestThemeService()); - } - if (!services.has(ITelemetryService)) { - services.set(ITelemetryService, NullTelemetryService); - } +function doCreateTestCodeEditor(instantiationService: IInstantiationService, options: TestCodeEditorCreationOptions): ITestCodeEditor { + const model = options.model; + delete options.model; const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { contributions: [] @@ -185,5 +222,32 @@ function doCreateTestCodeEditor(options: TestCodeEditorCreationOptions): [IInsta } editor.setHasTextFocus(options.hasTextFocus); editor.setModel(model); - return [instantiationService, editor, store]; + return editor; +} + +export interface TestCodeEditorCreationOptions2 extends editorOptions.IEditorOptions { + /** + * If the editor has text focus. + * Defaults to true. + */ + hasTextFocus?: boolean; +} + +export function createTestCodeEditor2(instantiationService: IInstantiationService, model: ITextModel, options: TestCodeEditorCreationOptions2): TestCodeEditor { + const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: [] + }; + const editor = instantiationService.createInstance( + TestCodeEditor, + new TestEditorDomElement(), + options, + codeEditorWidgetOptions + ); + if (typeof options.hasTextFocus === 'undefined') { + options.hasTextFocus = true; + } + editor.setHasTextFocus(options.hasTextFocus); + editor.setModel(model); + editor.getViewModel()!.setHasFocus(true); + return editor; } diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index 8558f8fc1c..1246ec9cc6 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -8,40 +8,43 @@ import { IRange } from 'vs/editor/common/core/range'; import { Selection, ISelection } from 'vs/editor/common/core/selection'; import { ICommand, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTestCodeEditor2, createTestCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; export function testCommand( lines: string[], - languageIdentifier: LanguageIdentifier | null, + languageId: string | null, selection: Selection, commandFactory: (selection: Selection) => ICommand, expectedLines: string[], expectedSelection: Selection, - forceTokenization?: boolean + forceTokenization?: boolean, + prepare?: (accessor: ServicesAccessor, disposables: DisposableStore) => void ): void { - let model = createTextModel(lines.join('\n'), undefined, languageIdentifier); - withTestCodeEditor('', { model: model }, (_editor, cursor) => { - if (!cursor) { - return; - } + const [instantiationService, disposables] = createTestCodeEditorServices(); + if (prepare) { + instantiationService.invokeFunction(prepare, disposables); + } + const model = disposables.add(instantiationService.createInstance(TextModel, lines.join('\n'), TextModel.DEFAULT_CREATION_OPTIONS, languageId, null)); + const editor = disposables.add(createTestCodeEditor2(instantiationService, model, {})); + const viewModel = editor.getViewModel()!; - if (forceTokenization) { - model.forceTokenization(model.getLineCount()); - } + if (forceTokenization) { + model.forceTokenization(model.getLineCount()); + } - cursor.setSelections('tests', [selection]); + viewModel.setSelections('tests', [selection]); - cursor.executeCommand(commandFactory(cursor.getSelection()), 'tests'); + viewModel.executeCommand(commandFactory(viewModel.getSelection()), 'tests'); - assert.deepStrictEqual(model.getLinesContent(), expectedLines); + assert.deepStrictEqual(model.getLinesContent(), expectedLines); - let actualSelection = cursor.getSelection(); - assert.deepStrictEqual(actualSelection.toString(), expectedSelection.toString()); + const actualSelection = viewModel.getSelection(); + assert.deepStrictEqual(actualSelection.toString(), expectedSelection.toString()); - }); - model.dispose(); + disposables.dispose(); } /** diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index 61929b7079..6edfa13378 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -57,7 +57,7 @@ suite('MinimapCharRenderer', () => { } function createFakeImageData(width: number, height: number): ImageData { - return { + return { width: width, height: height, data: new Uint8ClampedArray(width * height * Constants.RGBA_CHANNELS_CNT) @@ -78,7 +78,7 @@ suite('MinimapCharRenderer', () => { imageData.data[4 * i + 2] = background.b; imageData.data[4 * i + 3] = 255; } - renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 2, false, false); + renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, 255, background, 255, 2, false, false); let actual: number[] = []; for (let i = 0; i < imageData.data.length; i++) { @@ -108,7 +108,7 @@ suite('MinimapCharRenderer', () => { imageData.data[4 * i + 3] = 255; } - renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 1, false, false); + renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, 255, background, 255, 1, false, false); let actual: number[] = []; for (let i = 0; i < imageData.data.length; i++) { diff --git a/src/vs/editor/test/common/commentMode.ts b/src/vs/editor/test/common/commentMode.ts index f8730ed6d3..fb19c9efea 100644 --- a/src/vs/editor/test/common/commentMode.ts +++ b/src/vs/editor/test/common/commentMode.ts @@ -3,17 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LanguageIdentifier } from 'vs/editor/common/modes'; import { CommentRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; export class CommentMode extends MockMode { - private static readonly _id = new LanguageIdentifier('commentMode', 3); + public static readonly id = 'commentMode'; constructor(commentsConfig: CommentRule) { - super(CommentMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + super(CommentMode.id); + this._register(LanguageConfigurationRegistry.register(this.languageId, { comments: commentsConfig })); } diff --git a/src/vs/editor/test/common/core/lineTokens.test.ts b/src/vs/editor/test/common/core/lineTokens.test.ts index 3f4a2e3f68..d49c29b26d 100644 --- a/src/vs/editor/test/common/core/lineTokens.test.ts +++ b/src/vs/editor/test/common/core/lineTokens.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { IViewLineTokens, LineTokens } from 'vs/editor/common/core/lineTokens'; import { MetadataConsts } from 'vs/editor/common/modes'; +import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; suite('LineTokens', () => { @@ -24,7 +25,7 @@ suite('LineTokens', () => { ) >>> 0; } - return new LineTokens(binTokens, text); + return new LineTokens(binTokens, text, new LanguageIdCodec()); } function createTestLineTokens(): LineTokens { diff --git a/src/vs/editor/test/common/editorTestUtils.ts b/src/vs/editor/test/common/editorTestUtils.ts index 3d33613144..57a8e323e4 100644 --- a/src/vs/editor/test/common/editorTestUtils.ts +++ b/src/vs/editor/test/common/editorTestUtils.ts @@ -3,13 +3,39 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { BracketPairColorizationOptions, DefaultEndOfLine, ITextModelCreationOptions } from 'vs/editor/common/model'; +import { BracketPairColorizationOptions, DefaultEndOfLine, ITextBufferFactory, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; + +class TestTextModel extends TextModel { + public registerDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} export function withEditorModel(text: string[], callback: (model: TextModel) => void): void { let model = createTextModel(text.join('\n')); @@ -29,8 +55,8 @@ export interface IRelaxedTextModelCreationOptions { bracketColorizationOptions?: BracketPairColorizationOptions; } -export function createTextModel(text: string, _options: IRelaxedTextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier: LanguageIdentifier | null = null, uri: URI | null = null): TextModel { - const options: ITextModelCreationOptions = { +function resolveOptions(_options: IRelaxedTextModelCreationOptions): ITextModelCreationOptions { + return { tabSize: (typeof _options.tabSize === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.tabSize : _options.tabSize), indentSize: (typeof _options.indentSize === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.indentSize : _options.indentSize), insertSpaces: (typeof _options.insertSpaces === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.insertSpaces : _options.insertSpaces), @@ -41,8 +67,49 @@ export function createTextModel(text: string, _options: IRelaxedTextModelCreatio largeFileOptimizations: (typeof _options.largeFileOptimizations === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.largeFileOptimizations : _options.largeFileOptimizations), bracketPairColorizationOptions: (typeof _options.bracketColorizationOptions === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.bracketPairColorizationOptions : _options.bracketColorizationOptions), }; - const dialogService = new TestDialogService(); - const notificationService = new TestNotificationService(); - const undoRedoService = new UndoRedoService(dialogService, notificationService); - return new TextModel(text, options, languageIdentifier, uri, undoRedoService); +} + +export function createTextModel(text: string | ITextBufferFactory, _options: IRelaxedTextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, languageId: string | null = null, uri: URI | null = null): TextModel { + const [instantiationService, disposables] = createModelServices(); + const model = createTextModel2(instantiationService, text, _options, languageId, uri); + model.registerDisposable(disposables); + return model; +} + +export function createTextModel2(instantiationService: IInstantiationService, text: string | ITextBufferFactory, _options: IRelaxedTextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, languageId: string | null = null, uri: URI | null = null): TestTextModel { + const options = resolveOptions(_options); + return instantiationService.createInstance(TestTextModel, text, options, languageId, uri); +} + +export function createModelServices(services: ServiceCollection = new ServiceCollection()): [TestInstantiationService, DisposableStore] { + const serviceIdentifiers: ServiceIdentifier[] = []; + const define = (id: ServiceIdentifier, ctor: new (...args: any[]) => T) => { + if (!services.has(id)) { + services.set(id, new SyncDescriptor(ctor)); + } + serviceIdentifiers.push(id); + }; + + define(INotificationService, TestNotificationService); + define(IDialogService, TestDialogService); + define(IUndoRedoService, UndoRedoService); + define(IModeService, ModeServiceImpl); + define(ILanguageConfigurationService, TestLanguageConfigurationService); + define(IConfigurationService, TestConfigurationService); + define(ITextResourcePropertiesService, TestTextResourcePropertiesService); + define(IThemeService, TestThemeService); + define(ILogService, NullLogService); + define(IModelService, ModelServiceImpl); + + const instantiationService = new TestInstantiationService(services); + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => { + for (const id of serviceIdentifiers) { + const instanceOrDescriptor = services.get(id); + if (typeof instanceOrDescriptor.dispose === 'function') { + instanceOrDescriptor.dispose(); + } + } + })); + return [instantiationService, disposables]; } diff --git a/src/vs/editor/test/common/mocks/mockMode.ts b/src/vs/editor/test/common/mocks/mockMode.ts index 551d08af92..1800a26c5f 100644 --- a/src/vs/editor/test/common/mocks/mockMode.ts +++ b/src/vs/editor/test/common/mocks/mockMode.ts @@ -5,27 +5,19 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IMode, LanguageIdentifier } from 'vs/editor/common/modes'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { ILanguageSelection } from 'vs/editor/common/services/modeService'; -export class MockMode extends Disposable implements IMode { - private readonly _languageIdentifier: LanguageIdentifier; - - constructor(languageIdentifier: LanguageIdentifier) { +export class MockMode extends Disposable { + constructor( + public readonly languageId: string + ) { super(); - this._languageIdentifier = languageIdentifier; - } - - public getId(): string { - return this._languageIdentifier.language; - } - - public getLanguageIdentifier(): LanguageIdentifier { - return this._languageIdentifier; + this._register(ModesRegistry.registerLanguage({ id: languageId })); } } export class StaticLanguageSelector implements ILanguageSelection { - readonly onDidChange: Event = Event.None; - constructor(public readonly languageIdentifier: LanguageIdentifier) { } + readonly onDidChange: Event = Event.None; + constructor(public readonly languageId: string) { } } diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts index a36c493f8f..edc1038103 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts @@ -7,8 +7,8 @@ import assert = require('assert'); import { splitLines } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { BeforeEditPositionMapper, TextEditInfo } from 'vs/editor/common/model/bracketPairColorizer/beforeEditPositionMapper'; -import { Length, lengthOfString, lengthToObj, lengthToPosition, toLength } from 'vs/editor/common/model/bracketPairColorizer/length'; +import { BeforeEditPositionMapper, TextEditInfo } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/beforeEditPositionMapper'; +import { Length, lengthOfString, lengthToObj, lengthToPosition, toLength } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/length'; suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { test('Single-Line 1', () => { diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts new file mode 100644 index 0000000000..837e0bb476 --- /dev/null +++ b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert = require('assert'); +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/brackets'; +import { SmallImmutableSet, DenseKeyProvider } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet'; +import { Token, TokenKind } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; + +suite('Bracket Pair Colorizer - Brackets', () => { + test('Basic', () => { + const languageId = 'testMode1'; + const denseKeyProvider = new DenseKeyProvider(); + const getImmutableSet = (elements: string[]) => { + let newSet = SmallImmutableSet.getEmpty(); + elements.forEach(x => newSet = newSet.add(`${languageId}:::${x}`, denseKeyProvider)); + return newSet; + }; + const getKey = (value: string) => { + return denseKeyProvider.getKey(`${languageId}:::${value}`); + }; + + const disposableStore = new DisposableStore(); + disposableStore.add(LanguageConfigurationRegistry.register(languageId, { + brackets: [ + ['{', '}'], ['[', ']'], ['(', ')'], + ['begin', 'end'], ['case', 'endcase'], ['casez', 'endcase'], // Verilog + ['\\left(', '\\right)'], ['\\left(', '\\right.'], ['\\left.', '\\right)'], // LaTeX Parentheses + ['\\left[', '\\right]'], ['\\left[', '\\right.'], ['\\left.', '\\right]'] // LaTeX Brackets + ] + })); + + const languageConfigService = new TestLanguageConfigurationService(); + const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigService.getLanguageConfiguration(l, undefined)); + const bracketsExpected = [ + { text: '{', length: 1, kind: 'OpeningBracket', bracketId: getKey('{'), bracketIds: getImmutableSet(['{']) }, + { text: '[', length: 1, kind: 'OpeningBracket', bracketId: getKey('['), bracketIds: getImmutableSet(['[']) }, + { text: '(', length: 1, kind: 'OpeningBracket', bracketId: getKey('('), bracketIds: getImmutableSet(['(']) }, + { text: 'begin', length: 5, kind: 'OpeningBracket', bracketId: getKey('begin'), bracketIds: getImmutableSet(['begin']) }, + { text: 'case', length: 4, kind: 'OpeningBracket', bracketId: getKey('case'), bracketIds: getImmutableSet(['case']) }, + { text: 'casez', length: 5, kind: 'OpeningBracket', bracketId: getKey('casez'), bracketIds: getImmutableSet(['casez']) }, + { text: '\\left(', length: 6, kind: 'OpeningBracket', bracketId: getKey('\\left('), bracketIds: getImmutableSet(['\\left(']) }, + { text: '\\left.', length: 6, kind: 'OpeningBracket', bracketId: getKey('\\left.'), bracketIds: getImmutableSet(['\\left.']) }, + { text: '\\left[', length: 6, kind: 'OpeningBracket', bracketId: getKey('\\left['), bracketIds: getImmutableSet(['\\left[']) }, + + { text: '}', length: 1, kind: 'ClosingBracket', bracketId: getKey('{'), bracketIds: getImmutableSet(['{']) }, + { text: ']', length: 1, kind: 'ClosingBracket', bracketId: getKey('['), bracketIds: getImmutableSet(['[']) }, + { text: ')', length: 1, kind: 'ClosingBracket', bracketId: getKey('('), bracketIds: getImmutableSet(['(']) }, + { text: 'end', length: 3, kind: 'ClosingBracket', bracketId: getKey('begin'), bracketIds: getImmutableSet(['begin']) }, + { text: 'endcase', length: 7, kind: 'ClosingBracket', bracketId: getKey('case'), bracketIds: getImmutableSet(['case', 'casez']) }, + { text: '\\right)', length: 7, kind: 'ClosingBracket', bracketId: getKey('\\left('), bracketIds: getImmutableSet(['\\left(', '\\left.']) }, + { text: '\\right.', length: 7, kind: 'ClosingBracket', bracketId: getKey('\\left('), bracketIds: getImmutableSet(['\\left(', '\\left[']) }, + { text: '\\right]', length: 7, kind: 'ClosingBracket', bracketId: getKey('\\left['), bracketIds: getImmutableSet(['\\left[', '\\left.']) } + ]; + const bracketsActual = bracketsExpected.map(x => tokenToObject(brackets.getToken(x.text, languageId), x.text)); + + assert.deepStrictEqual(bracketsActual, bracketsExpected); + + disposableStore.dispose(); + }); +}); + +function tokenToObject(token: Token | undefined, text: string): any { + if (token === undefined) { + return undefined; + } + return { + text: text, + length: token.length, + bracketId: token.bracketId, + bracketIds: token.bracketIds, + kind: { + [TokenKind.ClosingBracket]: 'ClosingBracket', + [TokenKind.OpeningBracket]: 'OpeningBracket', + [TokenKind.Text]: 'Text', + }[token.kind], + }; +} diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts index 074f09a9bf..7ca29f440d 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/concat23Trees.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); -import { AstNode, AstNodeKind, ListAstNode, TextAstNode } from 'vs/editor/common/model/bracketPairColorizer/ast'; -import { toLength } from 'vs/editor/common/model/bracketPairColorizer/length'; -import { concat23Trees } from 'vs/editor/common/model/bracketPairColorizer/concat23Trees'; +import { AstNode, AstNodeKind, ListAstNode, TextAstNode } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/ast'; +import { toLength } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/length'; +import { concat23Trees } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/concat23Trees'; suite('Bracket Pair Colorizer - mergeItems', () => { test('Clone', () => { @@ -15,7 +15,7 @@ suite('Bracket Pair Colorizer - mergeItems', () => { new TextAstNode(toLength(1, 1)), ]); - assert.ok(equals(tree, tree.clone())); + assert.ok(equals(tree, tree.deepClone())); }); function equals(node1: AstNode, node2: AstNode): boolean { @@ -33,12 +33,12 @@ suite('Bracket Pair Colorizer - mergeItems', () => { } } - if (!node1.unopenedBrackets.equals(node2.unopenedBrackets)) { + if (!node1.missingOpeningBracketIds.equals(node2.missingOpeningBracketIds)) { return false; } if (node1.kind === AstNodeKind.Pair && node2.kind === AstNodeKind.Pair) { - return node1.category === node2.category; + return true; } else if (node1.kind === node2.kind) { return true; } @@ -47,7 +47,7 @@ suite('Bracket Pair Colorizer - mergeItems', () => { } function testMerge(lists: AstNode[]) { - const node = (concat23Trees(lists.map(l => l.clone())) || ListAstNode.create([])).flattenLists(); + const node = (concat23Trees(lists.map(l => l.deepClone())) || ListAstNode.create([])).flattenLists(); // This trivial merge does not maintain the (2,3) tree invariant. const referenceNode = ListAstNode.create(lists).flattenLists(); @@ -60,30 +60,30 @@ suite('Bracket Pair Colorizer - mergeItems', () => { test('Same Height Lists', () => { const textNode = new TextAstNode(toLength(1, 1)); - const tree = ListAstNode.create([textNode.clone(), textNode.clone()]); - testMerge([tree.clone(), tree.clone(), tree.clone(), tree.clone(), tree.clone()]); + const tree = ListAstNode.create([textNode.deepClone(), textNode.deepClone()]); + testMerge([tree.deepClone(), tree.deepClone(), tree.deepClone(), tree.deepClone(), tree.deepClone()]); }); test('Different Height Lists 1', () => { const textNode = new TextAstNode(toLength(1, 1)); - const tree1 = ListAstNode.create([textNode.clone(), textNode.clone()]); - const tree2 = ListAstNode.create([tree1.clone(), tree1.clone()]); + const tree1 = ListAstNode.create([textNode.deepClone(), textNode.deepClone()]); + const tree2 = ListAstNode.create([tree1.deepClone(), tree1.deepClone()]); testMerge([tree1, tree2]); }); test('Different Height Lists 2', () => { const textNode = new TextAstNode(toLength(1, 1)); - const tree1 = ListAstNode.create([textNode.clone(), textNode.clone()]); - const tree2 = ListAstNode.create([tree1.clone(), tree1.clone()]); + const tree1 = ListAstNode.create([textNode.deepClone(), textNode.deepClone()]); + const tree2 = ListAstNode.create([tree1.deepClone(), tree1.deepClone()]); testMerge([tree2, tree1]); }); test('Different Height Lists 3', () => { const textNode = new TextAstNode(toLength(1, 1)); - const tree1 = ListAstNode.create([textNode.clone(), textNode.clone()]); - const tree2 = ListAstNode.create([tree1.clone(), tree1.clone()]); + const tree1 = ListAstNode.create([textNode.deepClone(), textNode.deepClone()]); + const tree2 = ListAstNode.create([tree1.deepClone(), tree1.deepClone()]); testMerge([tree2, tree1, tree1, tree2, tree2]); }); diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts new file mode 100644 index 0000000000..e811a40bc4 --- /dev/null +++ b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts @@ -0,0 +1,255 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert = require('assert'); +import { Disposable, disposeOnReturn } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { BracketPairInfo } from 'vs/editor/common/model/bracketPairs/bracketPairs'; +import { LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; + +suite('Bracket Pair Colorizer - getBracketPairsInRange', () => { + function createLang() { + return MockLanguage.create({ + configuration: { + colorizedBracketPairs: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ] + }, + }); + } + + test('Basic 1', () => { + disposeOnReturn(store => { + const doc = new AnnotatedDocument(`{ ( [] ¹ ) [ ² { } ] () } []`); + const model = store.add( + createTextModel(doc.text, {}, store.add(createLang()).id) + ); + assert.deepStrictEqual( + model.bracketPairs + .getBracketPairsInRange(doc.range(1, 2)) + .map(bracketPairToJSON), + [ + { + level: 0, + range: '[1,1 -> 1,2]', + openRange: '[1,1 -> 1,2]', + closeRange: '[1,23 -> 1,24]', + }, + { + level: 1, + range: '[1,3 -> 1,4]', + openRange: '[1,3 -> 1,4]', + closeRange: '[1,9 -> 1,10]', + }, + { + level: 1, + range: '[1,11 -> 1,12]', + openRange: '[1,11 -> 1,12]', + closeRange: '[1,18 -> 1,19]', + }, + ] + ); + }); + }); + + test('Basic 2', () => { + disposeOnReturn(store => { + const doc = new AnnotatedDocument(`{ ( [] ¹ ²) [ { } ] () } []`); + const model = store.add( + createTextModel(doc.text, {}, store.add(createLang()).id) + ); + assert.deepStrictEqual( + model.bracketPairs + .getBracketPairsInRange(doc.range(1, 2)) + .map(bracketPairToJSON), + [ + { + level: 0, + range: '[1,1 -> 1,2]', + openRange: '[1,1 -> 1,2]', + closeRange: '[1,23 -> 1,24]', + }, + { + level: 1, + range: '[1,3 -> 1,4]', + openRange: '[1,3 -> 1,4]', + closeRange: '[1,9 -> 1,10]', + }, + ] + ); + }); + }); + + test('Basic Empty', () => { + disposeOnReturn(store => { + const doc = new AnnotatedDocument(`¹ ² { ( [] ) [ { } ] () } []`); + const model = store.add( + createTextModel(doc.text, {}, store.add(createLang()).id) + ); + assert.deepStrictEqual( + model.bracketPairs + .getBracketPairsInRange(doc.range(1, 2)) + .map(bracketPairToJSON), + [] + ); + }); + }); + + test('Basic All', () => { + disposeOnReturn(store => { + const doc = new AnnotatedDocument(`¹ { ( [] ) [ { } ] () } [] ²`); + const model = store.add( + createTextModel(doc.text, {}, store.add(createLang()).id) + ); + assert.deepStrictEqual( + model.bracketPairs + .getBracketPairsInRange(doc.range(1, 2)) + .map(bracketPairToJSON), + [ + { + level: 0, + range: '[1,2 -> 1,3]', + openRange: '[1,2 -> 1,3]', + closeRange: '[1,23 -> 1,24]', + }, + { + level: 1, + range: '[1,4 -> 1,5]', + openRange: '[1,4 -> 1,5]', + closeRange: '[1,9 -> 1,10]', + }, + { + level: 2, + range: '[1,6 -> 1,7]', + openRange: '[1,6 -> 1,7]', + closeRange: '[1,7 -> 1,8]', + }, + { + level: 1, + range: '[1,11 -> 1,12]', + openRange: '[1,11 -> 1,12]', + closeRange: '[1,18 -> 1,19]', + }, + { + level: 2, + range: '[1,14 -> 1,15]', + openRange: '[1,14 -> 1,15]', + closeRange: '[1,16 -> 1,17]', + }, + { + level: 1, + range: '[1,20 -> 1,21]', + openRange: '[1,20 -> 1,21]', + closeRange: '[1,21 -> 1,22]', + }, + { + level: 0, + range: '[1,25 -> 1,26]', + openRange: '[1,25 -> 1,26]', + closeRange: '[1,26 -> 1,27]', + }, + ] + ); + }); + }); +}); + +function bracketPairToJSON(pair: BracketPairInfo): unknown { + return { + level: pair.nestingLevel, + range: pair.openingBracketRange.toString(), + openRange: pair.openingBracketRange.toString(), + closeRange: pair.closingBracketRange?.toString() || null, + }; +} + +class PositionOffsetTransformer { + private readonly lineStartOffsetByLineIdx: number[]; + + constructor(text: string) { + this.lineStartOffsetByLineIdx = []; + this.lineStartOffsetByLineIdx.push(0); + for (let i = 0; i < text.length; i++) { + if (text.charAt(i) === '\n') { + this.lineStartOffsetByLineIdx.push(i + 1); + } + } + } + + getOffset(position: Position): number { + return this.lineStartOffsetByLineIdx[position.lineNumber - 1] + position.column - 1; + } + + getPosition(offset: number): Position { + const lineNumber = this.lineStartOffsetByLineIdx.findIndex(lineStartOffset => lineStartOffset <= offset); + return new Position(lineNumber + 1, offset - this.lineStartOffsetByLineIdx[lineNumber] + 1); + } +} + +class AnnotatedDocument { + public readonly text: string; + private readonly positions: ReadonlyMap; + + constructor(src: string) { + const numbers = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹']; + + let text = ''; + let offsetPositions = new Map(); + + let offset = 0; + for (let i = 0; i < src.length; i++) { + const idx = numbers.indexOf(src[i]); + if (idx >= 0) { + offsetPositions.set(idx, offset); + } else { + text += src[i]; + offset++; + } + } + + this.text = text; + + const mapper = new PositionOffsetTransformer(this.text); + let positions = new Map(); + for (const [idx, offset] of offsetPositions.entries()) { + positions.set(idx, mapper.getPosition(offset)); + } + this.positions = positions; + } + + range(start: number, end: number): Range { + return Range.fromPositions(this.positions.get(start)!, this.positions.get(end)!); + } +} + +interface MockLanguageOptions { + configuration?: LanguageConfiguration +} + +class MockLanguage extends Disposable { + private static id = 0; + + public static create(options: MockLanguageOptions) { + const id = `lang${this.id++}`; + + return new MockLanguage(id, options); + } + + constructor( + public readonly id: string, + options: MockLanguageOptions + ) { + super(); + + if (options.configuration) { + this._register(LanguageConfigurationRegistry.register(id, options.configuration)); + } + } +} diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts index 4e93c6497c..728507cce5 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/length.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); -import { Length, lengthAdd, lengthDiffNonNegative, lengthToObj, toLength } from 'vs/editor/common/model/bracketPairColorizer/length'; +import { Length, lengthAdd, lengthDiffNonNegative, lengthToObj, toLength } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/length'; suite('Bracket Pair Colorizer - Length', () => { function toStr(length: Length): string { diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts index cfcdc9c6a7..c41c122719 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/smallImmutableSet.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); -import { DenseKeyProvider, SmallImmutableSet } from 'vs/editor/common/model/bracketPairColorizer/smallImmutableSet'; +import { DenseKeyProvider, SmallImmutableSet } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet'; suite('Bracket Pair Colorizer - ImmutableSet', () => { test('Basic', () => { diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index 6443710c9f..e42ecb1543 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -4,51 +4,85 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); -import { DisposableStore } from 'vs/base/common/lifecycle'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairColorizer/brackets'; -import { Length, lengthAdd, lengthsToRange, lengthZero } from 'vs/editor/common/model/bracketPairColorizer/length'; -import { TextBufferTokenizer, Token, Tokenizer, TokenKind } from 'vs/editor/common/model/bracketPairColorizer/tokenizer'; +import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/brackets'; +import { Length, lengthAdd, lengthsToRange, lengthZero } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/length'; +import { DenseKeyProvider } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/smallImmutableSet'; +import { TextBufferTokenizer, Token, Tokenizer, TokenKind } from 'vs/editor/common/model/bracketPairs/bracketPairsTree/tokenizer'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { IState, ITokenizationSupport, LanguageId, LanguageIdentifier, MetadataConsts, StandardTokenType, TokenizationRegistry } from 'vs/editor/common/modes'; +import { IState, ITokenizationSupport, LanguageId, MetadataConsts, StandardTokenType, TokenizationRegistry } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { createModelServices, createTextModel2 } from 'vs/editor/test/common/editorTestUtils'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('Bracket Pair Colorizer - Tokenizer', () => { test('Basic', () => { - const mode1 = new LanguageIdentifier('testMode1', 2); + const mode1 = 'testMode1'; + const [instantiationService, disposableStore] = createModelServices(); + const modeService = instantiationService.invokeFunction((accessor) => accessor.get(IModeService)); + disposableStore.add(ModesRegistry.registerLanguage({ id: mode1 })); + const encodedMode1 = modeService.languageIdCodec.encodeLanguageId(mode1); - const tStandard = (text: string) => new TokenInfo(text, mode1.id, StandardTokenType.Other); - const tComment = (text: string) => new TokenInfo(text, mode1.id, StandardTokenType.Comment); + const denseKeyProvider = new DenseKeyProvider(); + + const tStandard = (text: string) => new TokenInfo(text, encodedMode1, StandardTokenType.Other); + const tComment = (text: string) => new TokenInfo(text, encodedMode1, StandardTokenType.Comment); const document = new TokenizedDocument([ tStandard(' { } '), tStandard('be'), tStandard('gin end'), tStandard('\n'), tStandard('hello'), tComment('{'), tStandard('}'), ]); - const disposableStore = new DisposableStore(); - disposableStore.add(TokenizationRegistry.register(mode1.language, document.getTokenizationSupport())); + disposableStore.add(TokenizationRegistry.register(mode1, document.getTokenizationSupport())); disposableStore.add(LanguageConfigurationRegistry.register(mode1, { - brackets: [['{', '}'], ['[', ']'], ['(', ')']], + brackets: [['{', '}'], ['[', ']'], ['(', ')'], ['begin', 'end']], })); - const brackets = new LanguageAgnosticBracketTokens([['begin', 'end']]); - - const model = createTextModel(document.getText(), {}, mode1); + const model = disposableStore.add(createTextModel2(instantiationService, document.getText(), {}, mode1)); model.forceTokenization(model.getLineCount()); + const languageConfigService = new TestLanguageConfigurationService(); + const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigService.getLanguageConfiguration(l, undefined)); + const tokens = readAllTokens(new TextBufferTokenizer(model, brackets)); - assert.deepStrictEqual(toArr(tokens, model), [ - { category: -1, kind: 'Text', languageId: -1, text: ' ', }, - { category: 2000, kind: 'OpeningBracket', languageId: 2, text: '{', }, - { category: -1, kind: 'Text', languageId: -1, text: ' ', }, - { category: 2000, kind: 'ClosingBracket', languageId: 2, text: '}', }, - { category: -1, kind: 'Text', languageId: -1, text: ' ', }, - { category: 2004, kind: 'OpeningBracket', languageId: 2, text: 'begin', }, - { category: -1, kind: 'Text', languageId: -1, text: ' ', }, - { category: 2004, kind: 'ClosingBracket', languageId: 2, text: 'end', }, - { category: -1, kind: 'Text', languageId: -1, text: '\nhello{', }, - { category: 2000, kind: 'ClosingBracket', languageId: 2, text: '}', } + assert.deepStrictEqual(toArr(tokens, model, denseKeyProvider), [ + { text: ' ', bracketId: null, bracketIds: [], kind: 'Text' }, + { + text: '{', + bracketId: 'testMode1:::{', + bracketIds: ['testMode1:::{'], + kind: 'OpeningBracket', + }, + { text: ' ', bracketId: null, bracketIds: [], kind: 'Text' }, + { + text: '}', + bracketId: 'testMode1:::{', + bracketIds: ['testMode1:::{'], + kind: 'ClosingBracket', + }, + { text: ' ', bracketId: null, bracketIds: [], kind: 'Text' }, + { + text: 'begin', + bracketId: 'testMode1:::begin', + bracketIds: ['testMode1:::begin'], + kind: 'OpeningBracket', + }, + { text: ' ', bracketId: null, bracketIds: [], kind: 'Text' }, + { + text: 'end', + bracketId: 'testMode1:::begin', + bracketIds: ['testMode1:::begin'], + kind: 'ClosingBracket', + }, + { text: '\nhello{', bracketId: null, bracketIds: [], kind: 'Text' }, + { + text: '}', + bracketId: 'testMode1:::{', + bracketIds: ['testMode1:::{'], + kind: 'ClosingBracket', + }, ]); disposableStore.dispose(); @@ -67,26 +101,26 @@ function readAllTokens(tokenizer: Tokenizer): Token[] { return tokens; } -function toArr(tokens: Token[], model: TextModel): any[] { +function toArr(tokens: Token[], model: TextModel, keyProvider: DenseKeyProvider): any[] { const result = new Array(); let offset = lengthZero; for (const token of tokens) { - result.push(tokenToObj(token, offset, model)); + result.push(tokenToObj(token, offset, model, keyProvider)); offset = lengthAdd(offset, token.length); } return result; } -function tokenToObj(token: Token, offset: Length, model: TextModel): any { +function tokenToObj(token: Token, offset: Length, model: TextModel, keyProvider: DenseKeyProvider): any { return { text: model.getValueInRange(lengthsToRange(offset, lengthAdd(offset, token.length))), - category: token.category, + bracketId: keyProvider.reverseLookup(token.bracketId) || null, + bracketIds: keyProvider.reverseLookupSet(token.bracketIds), kind: { [TokenKind.ClosingBracket]: 'ClosingBracket', [TokenKind.OpeningBracket]: 'OpeningBracket', [TokenKind.Text]: 'Text', - }[token.kind], - languageId: token.languageId, + }[token.kind] }; } diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index fb389edb74..ada21dac77 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Range } from 'vs/editor/common/core/range'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { LanguageIdentifier, MetadataConsts } from 'vs/editor/common/modes'; +import { MetadataConsts } from 'vs/editor/common/modes'; import { ViewLineToken, ViewLineTokenFactory } from 'vs/editor/test/common/core/viewLineToken'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; @@ -107,7 +107,7 @@ suite('ModelLinesTokens', () => { function testApplyEdits(initial: IBufferLineState[], edits: IEdit[], expected: IBufferLineState[]): void { const initialText = initial.map(el => el.text).join('\n'); - const model = createTextModel(initialText, TextModel.DEFAULT_CREATION_OPTIONS, new LanguageIdentifier('test', 0)); + const model = createTextModel(initialText, TextModel.DEFAULT_CREATION_OPTIONS, 'test'); for (let lineIndex = 0; lineIndex < initial.length; lineIndex++) { const lineTokens = initial[lineIndex].tokens; const lineTextLength = model.getLineMaxColumn(lineIndex + 1) - 1; @@ -129,6 +129,8 @@ suite('ModelLinesTokens', () => { assert.strictEqual(actualLine, expected[lineIndex].text); assertLineTokens(actualTokens, expected[lineIndex].tokens); } + + model.dispose(); } test('single delete 1', () => { @@ -443,7 +445,7 @@ suite('ModelLinesTokens', () => { } test('insertion on empty line', () => { - const model = createTextModel('some text', TextModel.DEFAULT_CREATION_OPTIONS, new LanguageIdentifier('test', 0)); + const model = createTextModel('some text', TextModel.DEFAULT_CREATION_OPTIONS, 'test'); const tokens = TestToken.toTokens([new TestToken(0, 1)]); LineTokens.convertToEndOffset(tokens, model.getLineMaxColumn(1) - 1); model.setLineTokens(1, tokens); @@ -462,6 +464,8 @@ suite('ModelLinesTokens', () => { const actualTokens = model.getLineTokens(1); assertLineTokens(actualTokens, [new TestToken(0, 1)]); + + model.dispose(); }); test('updates tokens on insertion 1', () => { diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index a4fcfa08b3..55a0e4a801 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -47,7 +47,7 @@ suite('Editor Model - Model Modes 1', () => { const LANGUAGE_ID = 'modelModeTest1'; calledFor = []; languageRegistration = modes.TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); - thisModel = createTextModel(TEXT, undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); + thisModel = createTextModel(TEXT, undefined, LANGUAGE_ID); }); teardown(() => { @@ -200,7 +200,7 @@ suite('Editor Model - Model Modes 2', () => { 'Line5'; const LANGUAGE_ID = 'modelModeTest2'; languageRegistration = modes.TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); - thisModel = createTextModel(TEXT, undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); + thisModel = createTextModel(TEXT, undefined, LANGUAGE_ID); }); teardown(() => { diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 4f7a3f0e9e..ef7bae20a1 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -11,11 +11,12 @@ import { Range } from 'vs/editor/common/core/range'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ModelRawContentChangedEvent, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/model/textModelEvents'; -import { IState, LanguageIdentifier, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/modes'; +import { IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { createModelServices, createTextModel, createTextModel2 } from 'vs/editor/test/common/editorTestUtils'; +import { IModeService } from 'vs/editor/common/services/modeService'; // --------- utils @@ -378,25 +379,30 @@ suite('Editor Model - Model Line Separators', () => { suite('Editor Model - Words', () => { - const OUTER_LANGUAGE_ID = new LanguageIdentifier('outerMode', 3); - const INNER_LANGUAGE_ID = new LanguageIdentifier('innerMode', 4); + const OUTER_LANGUAGE_ID = 'outerMode'; + const INNER_LANGUAGE_ID = 'innerMode'; class OuterMode extends MockMode { - constructor() { + constructor( + @IModeService modeService: IModeService + ) { super(OUTER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {})); + const languageIdCodec = modeService.languageIdCodec; - this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, { + this._register(LanguageConfigurationRegistry.register(this.languageId, {})); + + this._register(TokenizationRegistry.register(this.languageId, { getInitialState: (): IState => NULL_STATE, tokenize: undefined!, tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { const tokensArr: number[] = []; - let prevLanguageId: LanguageIdentifier | undefined = undefined; + let prevLanguageId: string | undefined = undefined; for (let i = 0; i < line.length; i++) { const languageId = (line.charAt(i) === 'x' ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID); + const encodedLanguageId = languageIdCodec.encodeLanguageId(languageId); if (prevLanguageId !== languageId) { tokensArr.push(i); - tokensArr.push((languageId.id << MetadataConsts.LANGUAGEID_OFFSET)); + tokensArr.push((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET)); } prevLanguageId = languageId; } @@ -414,7 +420,7 @@ suite('Editor Model - Words', () => { class InnerMode extends MockMode { constructor() { super(INNER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {})); + this._register(LanguageConfigurationRegistry.register(this.languageId, {})); } } @@ -448,12 +454,11 @@ suite('Editor Model - Words', () => { }); test('getWordAtPosition at embedded language boundaries', () => { - const outerMode = new OuterMode(); - const innerMode = new InnerMode(); - disposables.push(outerMode, innerMode); + const [instantiationService, disposables] = createModelServices(); + const outerMode = disposables.add(instantiationService.createInstance(OuterMode)); + disposables.add(new InnerMode()); - const model = createTextModel('abab', undefined, outerMode.getLanguageIdentifier()); - disposables.push(model); + const model = disposables.add(createTextModel2(instantiationService, 'abab', undefined, outerMode.languageId)); assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); @@ -462,15 +467,17 @@ suite('Editor Model - Words', () => { assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); + + disposables.dispose(); }); test('issue #61296: VS code freezes when editing CSS file with emoji', () => { - const MODE_ID = new LanguageIdentifier('testMode', 4); + const MODE_ID = 'testMode'; const mode = new class extends MockMode { constructor() { super(MODE_ID); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + this._register(LanguageConfigurationRegistry.register(this.languageId, { wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g })); } diff --git a/src/vs/editor/test/common/model/modelInjectedText.test.ts b/src/vs/editor/test/common/model/modelInjectedText.test.ts index 162dfb05e5..9e5cca53dc 100644 --- a/src/vs/editor/test/common/model/modelInjectedText.test.ts +++ b/src/vs/editor/test/common/model/modelInjectedText.test.ts @@ -35,6 +35,7 @@ suite('Editor Model - Injected Text Events', () => { options: { after: { content: 'injected1' }, description: 'test1', + showIfCollapsed: true }, range: new Range(1, 1, 1, 1), }]); @@ -51,12 +52,14 @@ suite('Editor Model - Injected Text Events', () => { options: { after: { content: 'injected1' }, description: 'test1', + showIfCollapsed: true }, range: new Range(2, 1, 2, 1), }, { options: { after: { content: 'injected2' }, description: 'test2', + showIfCollapsed: true }, range: new Range(2, 2, 2, 2), }]); diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 49054c9f51..a19b7f0a0f 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -1787,6 +1787,8 @@ suite('snapshot', () => { ]); assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot1)); + + model.dispose(); }); test('immutable snapshot 1', () => { @@ -1807,6 +1809,8 @@ suite('snapshot', () => { ]); assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + + model.dispose(); }); test('immutable snapshot 2', () => { @@ -1827,6 +1831,8 @@ suite('snapshot', () => { ]); assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + + model.dispose(); }); test('immutable snapshot 3', () => { @@ -1846,6 +1852,8 @@ suite('snapshot', () => { ]); assert.notStrictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + + model.dispose(); }); }); diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 1bd3f82b97..03570abac4 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -175,6 +175,7 @@ suite('Editor Model - TextModel', () => { assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\r\nMy Second Line\r\n'.length); assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\r\nMy Second Line\r\nMy Third Line'.length); assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\r\nMy Second Line\r\nMy Third Line'.length); + m.dispose(); m = createTextModel('My First Line\nMy Second Line\nMy Third Line'); assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); @@ -188,6 +189,7 @@ suite('Editor Model - TextModel', () => { assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\nMy Second Line\n'.length); assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\nMy Second Line\nMy Third Line'.length); assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\nMy Second Line\nMy Third Line'.length); + m.dispose(); }); test('guess indentation 1', () => { @@ -687,6 +689,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(2, 9)); assert.deepStrictEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(2, 9)); + + m.dispose(); }); test('validatePosition around high-low surrogate pairs 1', () => { @@ -714,6 +718,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(1, 5)); assert.deepStrictEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(1, 5)); + + m.dispose(); }); test('validatePosition around high-low surrogate pairs 2', () => { @@ -728,6 +734,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.validatePosition(new Position(1, 6)), new Position(1, 6)); assert.deepStrictEqual(m.validatePosition(new Position(1, 7)), new Position(1, 7)); + m.dispose(); + }); test('validatePosition handle NaN.', () => { @@ -740,6 +748,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.validatePosition(new Position(NaN, NaN)), new Position(1, 1)); assert.deepStrictEqual(m.validatePosition(new Position(2, NaN)), new Position(2, 1)); assert.deepStrictEqual(m.validatePosition(new Position(NaN, 3)), new Position(1, 3)); + + m.dispose(); }); test('issue #71480: validatePosition handle floats', () => { @@ -753,6 +763,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.validatePosition(new Position(2, 0.8)), new Position(2, 1), 'f'); assert.deepStrictEqual(m.validatePosition(new Position(1, 1.2)), new Position(1, 1), 'g'); assert.deepStrictEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); + + m.dispose(); }); test('issue #71480: validateRange handle floats', () => { @@ -760,6 +772,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); assert.deepStrictEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); + + m.dispose(); }); test('validateRange around high-low surrogate pairs 1', () => { @@ -788,6 +802,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 5)); assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 5, 1, 5)); + + m.dispose(); }); test('validateRange around high-low surrogate pairs 2', () => { @@ -831,6 +847,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.validateRange(new Range(1, 6, 1, 7)), new Range(1, 6, 1, 7)); assert.deepStrictEqual(m.validateRange(new Range(1, 7, 1, 7)), new Range(1, 7, 1, 7)); + + m.dispose(); }); test('modifyPosition', () => { @@ -861,6 +879,8 @@ suite('Editor Model - TextModel', () => { assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -100), new Position(1, 1)); assert.deepStrictEqual(m.modifyPosition(new Position(2, 2), -100), new Position(1, 1)); assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -18), new Position(1, 1)); + + m.dispose(); }); test('normalizeIndentation 1', () => { @@ -940,6 +960,8 @@ suite('Editor Model - TextModel', () => { assert.strictEqual(model.getLineFirstNonWhitespaceColumn(10), 4, '10'); assert.strictEqual(model.getLineFirstNonWhitespaceColumn(11), 0, '11'); assert.strictEqual(model.getLineFirstNonWhitespaceColumn(12), 0, '12'); + + model.dispose(); }); test('getLineLastNonWhitespaceColumn', () => { @@ -970,12 +992,15 @@ suite('Editor Model - TextModel', () => { assert.strictEqual(model.getLineLastNonWhitespaceColumn(10), 4, '10'); assert.strictEqual(model.getLineLastNonWhitespaceColumn(11), 0, '11'); assert.strictEqual(model.getLineLastNonWhitespaceColumn(12), 0, '12'); + + model.dispose(); }); test('#50471. getValueInRange with invalid range', () => { let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); assert.strictEqual(m.getValueInRange(new Range(1, NaN, 1, 3)), 'My'); assert.strictEqual(m.getValueInRange(new Range(NaN, NaN, NaN, NaN)), ''); + m.dispose(); }); }); @@ -984,11 +1009,13 @@ suite('TextModel.mightContainRTL', () => { test('nope', () => { let model = createTextModel('hello world!'); assert.strictEqual(model.mightContainRTL(), false); + model.dispose(); }); test('yes', () => { let model = createTextModel('Hello,\nזוהי עובדה מבוססת שדעתו'); assert.strictEqual(model.mightContainRTL(), true); + model.dispose(); }); test('setValue resets 1', () => { @@ -996,6 +1023,7 @@ suite('TextModel.mightContainRTL', () => { assert.strictEqual(model.mightContainRTL(), false); model.setValue('Hello,\nזוהי עובדה מבוססת שדעתו'); assert.strictEqual(model.mightContainRTL(), true); + model.dispose(); }); test('setValue resets 2', () => { @@ -1003,6 +1031,7 @@ suite('TextModel.mightContainRTL', () => { assert.strictEqual(model.mightContainRTL(), true); model.setValue('hello world!'); assert.strictEqual(model.mightContainRTL(), false); + model.dispose(); }); }); diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index a8140bd683..4c9f163807 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -722,20 +722,6 @@ suite('TextModelSearch', () => { ); }); - test('issue #65281. \w should match line break.', () => { - assertFindMatches( - [ - 'this/is{', - 'a test', - '}', - ].join('\n'), - 'this/\\w*[^}]*', true, false, null, - [ - [1, 1, 3, 1] - ] - ); - }); - test('Simple find using unicode escape sequences', () => { assertFindMatches( regularText.join('\n'), diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index 6acf58b3cf..d82c257946 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -4,18 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { IFoundBracket } from 'vs/editor/common/model'; +import { IFoundBracket } from 'vs/editor/common/model/bracketPairs/bracketPairs'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { ITokenizationSupport, LanguageId, LanguageIdentifier, MetadataConsts, TokenizationRegistry, StandardTokenType } from 'vs/editor/common/modes'; +import { ITokenizationSupport, MetadataConsts, TokenizationRegistry, StandardTokenType } from 'vs/editor/common/modes'; import { CharacterPair } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { ViewLineToken } from 'vs/editor/test/common/core/viewLineToken'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { createModelServices, createTextModel, createTextModel2 } from 'vs/editor/test/common/editorTestUtils'; suite('TextModelWithTokens', () => { @@ -67,17 +69,19 @@ suite('TextModelWithTokens', () => { } } - const languageIdentifier = new LanguageIdentifier('testMode', LanguageId.PlainText); + const languageId = 'testMode'; + const disposables = new DisposableStore(); - let registration = LanguageConfigurationRegistry.register(languageIdentifier, { + disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + disposables.add(LanguageConfigurationRegistry.register(languageId, { brackets: brackets - }); + })); - let model = createTextModel( + const model = disposables.add(createTextModel( contents.join('\n'), TextModel.DEFAULT_CREATION_OPTIONS, - languageIdentifier - ); + languageId + )); // findPrevBracket { @@ -95,7 +99,7 @@ suite('TextModelWithTokens', () => { } } - let actual = model.findPrevBracket({ + let actual = model.bracketPairs.findPrevBracket({ lineNumber: lineNumber, column: column }); @@ -121,7 +125,7 @@ suite('TextModelWithTokens', () => { } } - let actual = model.findNextBracket({ + let actual = model.bracketPairs.findNextBracket({ lineNumber: lineNumber, column: column }); @@ -131,11 +135,10 @@ suite('TextModelWithTokens', () => { } } - model.dispose(); - registration.dispose(); + disposables.dispose(); } - test('brackets', () => { + test('brackets1', () => { testBrackets([ 'if (a == 3) { return (7 * (a + 5)); }' ], [ @@ -147,39 +150,41 @@ suite('TextModelWithTokens', () => { }); function assertIsNotBracket(model: TextModel, lineNumber: number, column: number) { - const match = model.matchBracket(new Position(lineNumber, column)); + const match = model.bracketPairs.matchBracket(new Position(lineNumber, column)); assert.strictEqual(match, null, 'is not matching brackets at ' + lineNumber + ', ' + column); } function assertIsBracket(model: TextModel, testPosition: Position, expected: [Range, Range]): void { - const actual = model.matchBracket(testPosition); + const actual = model.bracketPairs.matchBracket(testPosition); assert.deepStrictEqual(actual, expected, 'matches brackets at ' + testPosition); } suite('TextModelWithTokens - bracket matching', () => { - const languageIdentifier = new LanguageIdentifier('bracketMode1', LanguageId.PlainText); - let registration: IDisposable; + const languageId = 'bracketMode1'; + let disposables: DisposableStore; setup(() => { - registration = LanguageConfigurationRegistry.register(languageIdentifier, { + disposables = new DisposableStore(); + disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + disposables.add(LanguageConfigurationRegistry.register(languageId, { brackets: [ ['{', '}'], ['[', ']'], ['(', ')'], ] - }); + })); }); teardown(() => { - registration.dispose(); + disposables.dispose(); }); test('bracket matching 1', () => { let text = ')]}{[(' + '\n' + ')]}{[('; - let model = createTextModel(text, undefined, languageIdentifier); + let model = createTextModel(text, undefined, languageId); assertIsNotBracket(model, 1, 1); assertIsNotBracket(model, 1, 2); @@ -207,7 +212,7 @@ suite('TextModelWithTokens - bracket matching', () => { '}, bar: {hallo: [{' + '\n' + '}, {' + '\n' + '}]}}'; - let model = createTextModel(text, undefined, languageIdentifier); + let model = createTextModel(text, undefined, languageId); let brackets: [Position, Range, Range][] = [ [new Position(1, 11), new Range(1, 11, 1, 12), new Range(5, 4, 5, 5)], @@ -260,14 +265,16 @@ suite('TextModelWithTokens', () => { test('bracket matching 3', () => { - const languageIdentifier = new LanguageIdentifier('bracketMode2', LanguageId.PlainText); - const registration = LanguageConfigurationRegistry.register(languageIdentifier, { + const languageId = 'bracketMode2'; + const disposables = new DisposableStore(); + disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + disposables.add(LanguageConfigurationRegistry.register(languageId, { brackets: [ ['if', 'end if'], ['loop', 'end loop'], ['begin', 'end'] ], - }); + })); const text = [ 'begin', @@ -285,7 +292,7 @@ suite('TextModelWithTokens', () => { 'end;', ].join('\n'); - const model = createTextModel(text, undefined, languageIdentifier); + const model = disposables.add(createTextModel(text, undefined, languageId)); // ... is not matched assertIsNotBracket(model, 10, 9); @@ -302,19 +309,20 @@ suite('TextModelWithTokens', () => { assertIsBracket(model, new Position(1, 1), [new Range(1, 1, 1, 6), new Range(6, 1, 6, 4)]); assertIsBracket(model, new Position(6, 1), [new Range(6, 1, 6, 4), new Range(1, 1, 1, 6)]); - model.dispose(); - registration.dispose(); + disposables.dispose(); }); test('bracket matching 4', () => { - const languageIdentifier = new LanguageIdentifier('bracketMode2', LanguageId.PlainText); - const registration = LanguageConfigurationRegistry.register(languageIdentifier, { + const languageId = 'bracketMode2'; + const disposables = new DisposableStore(); + disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + disposables.add(LanguageConfigurationRegistry.register(languageId, { brackets: [ ['recordbegin', 'endrecord'], ['simplerecordbegin', 'endrecord'], ], - }); + })); const text = [ 'recordbegin', @@ -323,7 +331,7 @@ suite('TextModelWithTokens', () => { 'endrecord', ].join('\n'); - const model = createTextModel(text, undefined, languageIdentifier); + const model = disposables.add(createTextModel(text, undefined, languageId)); // ... is matched assertIsBracket(model, new Position(1, 1), [new Range(1, 1, 1, 12), new Range(4, 1, 4, 10)]); @@ -333,19 +341,27 @@ suite('TextModelWithTokens', () => { assertIsBracket(model, new Position(2, 3), [new Range(2, 3, 2, 20), new Range(3, 3, 3, 12)]); assertIsBracket(model, new Position(3, 3), [new Range(3, 3, 3, 12), new Range(2, 3, 2, 20)]); - model.dispose(); - registration.dispose(); + disposables.dispose(); }); test('issue #95843: Highlighting of closing braces is indicating wrong brace when cursor is behind opening brace', () => { - const mode1 = new LanguageIdentifier('testMode1', 3); - const mode2 = new LanguageIdentifier('testMode2', 4); + const [instantiationService, disposables] = createModelServices(); + const mode1 = 'testMode1'; + const mode2 = 'testMode2'; + + const languageIdCodec = instantiationService.invokeFunction((accessor) => accessor.get(IModeService).languageIdCodec); + + disposables.add(ModesRegistry.registerLanguage({ id: mode1 })); + disposables.add(ModesRegistry.registerLanguage({ id: mode2 })); + const encodedMode1 = languageIdCodec!.encodeLanguageId(mode1); + const encodedMode2 = languageIdCodec!.encodeLanguageId(mode2); + const otherMetadata1 = ( - (mode1.id << MetadataConsts.LANGUAGEID_OFFSET) + (encodedMode1 << MetadataConsts.LANGUAGEID_OFFSET) | (StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET) ) >>> 0; const otherMetadata2 = ( - (mode2.id << MetadataConsts.LANGUAGEID_OFFSET) + (encodedMode2 << MetadataConsts.LANGUAGEID_OFFSET) | (StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET) ) >>> 0; @@ -395,17 +411,15 @@ suite('TextModelWithTokens', () => { } }; - const disposableStore = new DisposableStore(); - - disposableStore.add(TokenizationRegistry.register(mode1.language, tokenizationSupport)); - disposableStore.add(LanguageConfigurationRegistry.register(mode1, { + disposables.add(TokenizationRegistry.register(mode1, tokenizationSupport)); + disposables.add(LanguageConfigurationRegistry.register(mode1, { brackets: [ ['{', '}'], ['[', ']'], ['(', ')'] ], })); - disposableStore.add(LanguageConfigurationRegistry.register(mode2, { + disposables.add(LanguageConfigurationRegistry.register(mode2, { brackets: [ ['{', '}'], ['[', ']'], @@ -413,29 +427,40 @@ suite('TextModelWithTokens', () => { ], })); - const model = disposableStore.add(createTextModel([ - 'function f() {', - ' return

{true}

;', - '}', - ].join('\n'), undefined, mode1)); + const model = disposables.add(createTextModel2( + instantiationService, + [ + 'function f() {', + ' return

{true}

;', + '}', + ].join('\n'), + undefined, + mode1 + )); model.forceTokenization(1); model.forceTokenization(2); model.forceTokenization(3); - assert.deepStrictEqual(model.matchBracket(new Position(2, 14)), [new Range(2, 13, 2, 14), new Range(2, 18, 2, 19)]); + assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 14)), [new Range(2, 13, 2, 14), new Range(2, 18, 2, 19)]); - disposableStore.dispose(); + disposables.dispose(); }); test('issue #88075: TypeScript brace matching is incorrect in `${}` strings', () => { - const mode = new LanguageIdentifier('testMode', 3); + const [instantiationService, disposables] = createModelServices(); + const mode = 'testMode'; + + const languageIdCodec = instantiationService.invokeFunction((accessor) => accessor.get(IModeService).languageIdCodec); + + const encodedMode = languageIdCodec!.encodeLanguageId(mode); + const otherMetadata = ( - (mode.id << MetadataConsts.LANGUAGEID_OFFSET) + (encodedMode << MetadataConsts.LANGUAGEID_OFFSET) | (StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET) ) >>> 0; const stringMetadata = ( - (mode.id << MetadataConsts.LANGUAGEID_OFFSET) + (encodedMode << MetadataConsts.LANGUAGEID_OFFSET) | (StandardTokenType.String << MetadataConsts.TOKEN_TYPE_OFFSET) ) >>> 0; @@ -471,31 +496,34 @@ suite('TextModelWithTokens', () => { } }; - const registration1 = TokenizationRegistry.register(mode.language, tokenizationSupport); - const registration2 = LanguageConfigurationRegistry.register(mode, { + disposables.add(TokenizationRegistry.register(mode, tokenizationSupport)); + disposables.add(LanguageConfigurationRegistry.register(mode, { brackets: [ ['{', '}'], ['[', ']'], ['(', ')'] ], - }); + })); - const model = createTextModel([ - 'function hello() {', - ' console.log(`${100}`);', - '}' - ].join('\n'), undefined, mode); + const model = disposables.add(createTextModel2( + instantiationService, + [ + 'function hello() {', + ' console.log(`${100}`);', + '}' + ].join('\n'), + undefined, + mode + )); model.forceTokenization(1); model.forceTokenization(2); model.forceTokenization(3); - assert.deepStrictEqual(model.matchBracket(new Position(2, 23)), null); - assert.deepStrictEqual(model.matchBracket(new Position(2, 20)), null); + assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 23)), null); + assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 20)), null); - model.dispose(); - registration1.dispose(); - registration2.dispose(); + disposables.dispose(); }); }); @@ -531,8 +559,6 @@ suite('TextModelWithTokens regression tests', () => { let _tokenId = 10; const LANG_ID1 = 'indicisiveMode1'; const LANG_ID2 = 'indicisiveMode2'; - const languageIdentifier1 = new LanguageIdentifier(LANG_ID1, 3); - const languageIdentifier2 = new LanguageIdentifier(LANG_ID2, 4); const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, @@ -556,12 +582,12 @@ suite('TextModelWithTokens regression tests', () => { assertViewLineTokens(model, 1, true, [createViewLineToken(12, 1)]); assertViewLineTokens(model, 2, true, [createViewLineToken(9, 1)]); - model.setMode(languageIdentifier1); + model.setMode(LANG_ID1); assertViewLineTokens(model, 1, true, [createViewLineToken(12, 11)]); assertViewLineTokens(model, 2, true, [createViewLineToken(9, 12)]); - model.setMode(languageIdentifier2); + model.setMode(LANG_ID2); assertViewLineTokens(model, 1, false, [createViewLineToken(12, 1)]); assertViewLineTokens(model, 2, false, [createViewLineToken(9, 1)]); @@ -581,16 +607,18 @@ suite('TextModelWithTokens regression tests', () => { test('microsoft/monaco-editor#133: Error: Cannot read property \'modeId\' of undefined', () => { - const languageIdentifier = new LanguageIdentifier('testMode', LanguageId.PlainText); + const languageId = 'testMode'; - let registration = LanguageConfigurationRegistry.register(languageIdentifier, { + const disposables = new DisposableStore(); + disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + disposables.add(LanguageConfigurationRegistry.register(languageId, { brackets: [ ['module', 'end module'], ['sub', 'end sub'] ] - }); + })); - let model = createTextModel([ + const model = disposables.add(createTextModel([ 'Imports System', 'Imports System.Collections.Generic', '', @@ -600,43 +628,50 @@ suite('TextModelWithTokens regression tests', () => { '\tEnd Sub', '', 'End Module', - ].join('\n'), undefined, languageIdentifier); + ].join('\n'), undefined, languageId)); - let actual = model.matchBracket(new Position(4, 1)); + const actual = model.bracketPairs.matchBracket(new Position(4, 1)); assert.deepStrictEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); - model.dispose(); - registration.dispose(); + disposables.dispose(); }); test('issue #11856: Bracket matching does not work as expected if the opening brace symbol is contained in the closing brace symbol', () => { - const languageIdentifier = new LanguageIdentifier('testMode', LanguageId.PlainText); - - let registration = LanguageConfigurationRegistry.register(languageIdentifier, { + const languageId = 'testMode'; + const disposables = new DisposableStore(); + disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + disposables.add(LanguageConfigurationRegistry.register(languageId, { brackets: [ ['sequence', 'endsequence'], ['feature', 'endfeature'] ] - }); + })); - let model = createTextModel([ + const model = disposables.add(createTextModel([ 'sequence "outer"', ' sequence "inner"', ' endsequence', 'endsequence', - ].join('\n'), undefined, languageIdentifier); + ].join('\n'), undefined, languageId)); - let actual = model.matchBracket(new Position(3, 9)); + const actual = model.bracketPairs.matchBracket(new Position(3, 9)); assert.deepStrictEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); - model.dispose(); - registration.dispose(); + disposables.dispose(); }); test('issue #63822: Wrong embedded language detected for empty lines', () => { - const outerMode = new LanguageIdentifier('outerMode', 3); - const innerMode = new LanguageIdentifier('innerMode', 4); + const [instantiationService, disposables] = createModelServices(); + + const outerMode = 'outerMode'; + const innerMode = 'innerMode'; + + disposables.add(ModesRegistry.registerLanguage({ id: outerMode })); + disposables.add(ModesRegistry.registerLanguage({ id: innerMode })); + + const languageIdCodec = instantiationService.invokeFunction((accessor) => accessor.get(IModeService).languageIdCodec); + const encodedInnerMode = languageIdCodec.encodeLanguageId(innerMode); const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, @@ -645,21 +680,20 @@ suite('TextModelWithTokens regression tests', () => { let tokens = new Uint32Array(2); tokens[0] = 0; tokens[1] = ( - innerMode.id << MetadataConsts.LANGUAGEID_OFFSET + encodedInnerMode << MetadataConsts.LANGUAGEID_OFFSET ) >>> 0; return new TokenizationResult2(tokens, state); } }; - let registration = TokenizationRegistry.register(outerMode.language, tokenizationSupport); + disposables.add(TokenizationRegistry.register(outerMode, tokenizationSupport)); - let model = createTextModel('A model with one line', undefined, outerMode); + const model = disposables.add(createTextModel2(instantiationService, 'A model with one line', undefined, outerMode)); model.forceTokenization(1); - assert.strictEqual(model.getLanguageIdAtPosition(1, 1), innerMode.id); + assert.strictEqual(model.getLanguageIdAtPosition(1, 1), innerMode); - model.dispose(); - registration.dispose(); + disposables.dispose(); }); }); diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index a2c4bafd71..3e6089f5a6 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -11,6 +11,7 @@ import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { MetadataConsts, TokenMetadata, FontStyle, ColorId } from 'vs/editor/common/modes'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; +import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; suite('TokensStore', () => { @@ -214,7 +215,8 @@ suite('TokensStore', () => { }); test('partial tokens 1', () => { - const store = new TokensStore2(); + const codec = new LanguageIdCodec(); + const store = new TokensStore2(codec); // setPartial: [1,1 -> 31,2], [(5,5-10),(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10)] store.setPartial(new Range(1, 1, 31, 2), [ @@ -251,12 +253,13 @@ suite('TokensStore', () => { ]))) ]); - const lineTokens = store.addSemanticTokens(10, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); + const lineTokens = store.addSemanticTokens(10, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`, codec)); assert.strictEqual(lineTokens.getCount(), 3); }); test('partial tokens 2', () => { - const store = new TokensStore2(); + const codec = new LanguageIdCodec(); + const store = new TokensStore2(codec); // setPartial: [1,1 -> 31,2], [(5,5-10),(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10)] store.setPartial(new Range(1, 1, 31, 2), [ @@ -292,12 +295,13 @@ suite('TokensStore', () => { ]))) ]); - const lineTokens = store.addSemanticTokens(20, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); + const lineTokens = store.addSemanticTokens(20, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`, codec)); assert.strictEqual(lineTokens.getCount(), 3); }); test('partial tokens 3', () => { - const store = new TokensStore2(); + const codec = new LanguageIdCodec(); + const store = new TokensStore2(codec); // setPartial: [1,1 -> 31,2], [(5,5-10),(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10)] store.setPartial(new Range(1, 1, 31, 2), [ @@ -319,12 +323,13 @@ suite('TokensStore', () => { ]))) ]); - const lineTokens = store.addSemanticTokens(5, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); + const lineTokens = store.addSemanticTokens(5, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`, codec)); assert.strictEqual(lineTokens.getCount(), 3); }); test('issue #94133: Semantic colors stick around when using (only) range provider', () => { - const store = new TokensStore2(); + const codec = new LanguageIdCodec(); + const store = new TokensStore2(codec); // setPartial: [1,1 -> 1,20] [(1,9-11)] store.setPartial(new Range(1, 1, 1, 20), [ @@ -336,7 +341,7 @@ suite('TokensStore', () => { // setPartial: [1,1 -> 1,20], [] store.setPartial(new Range(1, 1, 1, 20), []); - const lineTokens = store.addSemanticTokens(1, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); + const lineTokens = store.addSemanticTokens(1, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`, codec)); assert.strictEqual(lineTokens.getCount(), 1); }); @@ -362,7 +367,8 @@ suite('TokensStore', () => { return new MultilineTokens2(firstLineNumber, new SparseEncodedTokens(new Uint32Array(result))); } - const store = new TokensStore2(); + const codec = new LanguageIdCodec(); + const store = new TokensStore2(codec); // setPartial [36446,1 -> 36475,115] [(36448,24-29),(36448,33-46),(36448,47-54),(36450,25-35),(36450,36-50),(36451,28-33),(36451,36-49),(36451,50-57),(36452,35-53),(36452,54-62),(36454,33-38),(36454,41-54),(36454,55-60),(36455,35-53),(36455,54-62),(36457,33-44),(36457,45-49),(36457,50-56),(36457,62-83),(36457,84-88),(36458,35-53),(36458,54-62),(36460,33-37),(36460,38-42),(36460,47-57),(36460,58-67),(36461,35-53),(36461,54-62),(36463,34-38),(36463,39-45),(36463,46-51),(36463,54-63),(36463,64-71),(36463,76-80),(36463,81-87),(36463,88-92),(36463,97-107),(36463,108-119),(36464,35-53),(36464,54-62),(36466,33-71),(36466,72-76),(36467,35-53),(36467,54-62),(36469,24-29),(36469,33-46),(36469,47-54),(36470,24-35),(36470,38-46),(36473,25-35),(36473,36-51),(36474,28-33),(36474,36-49),(36474,50-58),(36475,35-53),(36475,54-62)] store.setPartial( new Range(36446, 1, 36475, 115), @@ -384,7 +390,7 @@ suite('TokensStore', () => { [createTokens('[(36442,25-35),(36442,36-50),(36443,30-39),(36443,42-46),(36443,47-53),(36443,54-58),(36443,63-73),(36443,74-84),(36443,87-91),(36443,92-98),(36443,101-105),(36443,106-112),(36443,113-119),(36444,28-37),(36444,38-42),(36444,47-57),(36444,58-75),(36444,80-95),(36444,96-105),(36445,35-53),(36445,54-62),(36448,24-29),(36448,33-46),(36448,47-54),(36450,25-35),(36450,36-50),(36451,28-33),(36451,36-49),(36451,50-57),(36452,35-53),(36452,54-62),(36454,33-38),(36454,41-54),(36454,55-60),(36455,35-53),(36455,54-62),(36457,33-44),(36457,45-49),(36457,50-56),(36457,62-83),(36457,84-88),(36458,35-53),(36458,54-62),(36460,33-37),(36460,38-42),(36460,47-57),(36460,58-67),(36461,35-53),(36461,54-62),(36463,34-38),(36463,39-45),(36463,46-51),(36463,54-63),(36463,64-71),(36463,76-80),(36463,81-87),(36463,88-92),(36463,97-107),(36463,108-119),(36464,35-53),(36464,54-62),(36466,33-71),(36466,72-76),(36467,35-53),(36467,54-62),(36469,24-29),(36469,33-46),(36469,47-54),(36470,24-35)]')] ); - const lineTokens = store.addSemanticTokens(36451, new LineTokens(new Uint32Array([60, 1]), ` if (flags & ModifierFlags.Ambient) {`)); + const lineTokens = store.addSemanticTokens(36451, new LineTokens(new Uint32Array([60, 1]), ` if (flags & ModifierFlags.Ambient) {`, codec)); assert.strictEqual(lineTokens.getCount(), 7); }); @@ -408,7 +414,8 @@ suite('TokensStore', () => { return r; } - const store = new TokensStore2(); + const codec = new LanguageIdCodec(); + const store = new TokensStore2(codec); store.set([ new MultilineTokens2(1, new SparseEncodedTokens(new Uint32Array([ @@ -421,7 +428,7 @@ suite('TokensStore', () => { 14, createTMMetadata(1, FontStyle.None, 53), 17, createTMMetadata(6, FontStyle.None, 53), 18, createTMMetadata(1, FontStyle.None, 53), - ]), `const hello = 123;`)); + ]), `const hello = 123;`, codec)); const actual = toArr(lineTokens); assert.deepStrictEqual(actual, [ diff --git a/src/vs/editor/test/common/modes/languageConfiguration.test.ts b/src/vs/editor/test/common/modes/languageConfiguration.test.ts index 8761506d08..db585bf0b9 100644 --- a/src/vs/editor/test/common/modes/languageConfiguration.test.ts +++ b/src/vs/editor/test/common/modes/languageConfiguration.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { LanguageIdentifier, StandardTokenType } from 'vs/editor/common/modes'; +import { StandardTokenType } from 'vs/editor/common/modes'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; @@ -91,10 +91,10 @@ suite('StandardAutoClosingPairConditional', () => { }); test('language configurations priorities', () => { - const id = new LanguageIdentifier('testLang1', 15); + const id = 'testLang1'; const d1 = LanguageConfigurationRegistry.register(id, { comments: { lineComment: '1' } }, 100); const d2 = LanguageConfigurationRegistry.register(id, { comments: { lineComment: '2' } }, 10); - assert.strictEqual(LanguageConfigurationRegistry.getComments(id.id)?.lineCommentToken, '1'); + assert.strictEqual(LanguageConfigurationRegistry.getComments(id)?.lineCommentToken, '1'); d1.dispose(); d2.dispose(); }); diff --git a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts index 09e494dc26..9e94e0a69a 100644 --- a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts +++ b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { LanguageIdentifier, StandardTokenType } from 'vs/editor/common/modes'; +import { StandardTokenType } from 'vs/editor/common/modes'; import { BracketElectricCharacterSupport, IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter'; import { RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets'; import { TokenText, createFakeScopedLineTokens } from 'vs/editor/test/common/modesTestUtils'; -const fakeLanguageIdentifier = new LanguageIdentifier('test', 3); +const fakeLanguageId = 'test'; suite('Editor Modes - Auto Indentation', () => { function _testOnElectricCharacter(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number): IElectricAction | null { @@ -28,7 +28,7 @@ suite('Editor Modes - Auto Indentation', () => { test('getElectricCharacters uses all sources and dedups', () => { let sup = new BracketElectricCharacterSupport( - new RichEditBrackets(fakeLanguageIdentifier, [ + new RichEditBrackets(fakeLanguageId, [ ['{', '}'], ['(', ')'] ]) @@ -39,7 +39,7 @@ suite('Editor Modes - Auto Indentation', () => { test('matchOpenBracket', () => { let sup = new BracketElectricCharacterSupport( - new RichEditBrackets(fakeLanguageIdentifier, [ + new RichEditBrackets(fakeLanguageId, [ ['{', '}'], ['(', ')'] ]) diff --git a/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts b/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts new file mode 100644 index 0000000000..5ace96f00f --- /dev/null +++ b/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ILanguageConfigurationService, LanguageConfigurationRegistry, LanguageConfigurationServiceChangeEvent, ResolvedLanguageConfiguration } from 'vs/editor/common/modes/languageConfigurationRegistry'; + +export class TestLanguageConfigurationService implements ILanguageConfigurationService { + _serviceBrand: undefined; + + private registration: IDisposable | undefined = undefined; + + private readonly onDidChangeEmitter = new Emitter({ + onFirstListenerAdd: () => { + this.registration = LanguageConfigurationRegistry.onDidChange((e) => { + this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(e.languageId)); + }); + }, + onLastListenerRemove: () => { + this.registration?.dispose(); + this.registration = undefined; + } + }); + public readonly onDidChange = this.onDidChangeEmitter.event; + + getLanguageConfiguration(languageId: string, resource?: URI): ResolvedLanguageConfiguration { + return LanguageConfigurationRegistry.getLanguageConfiguration(languageId) ?? + new ResolvedLanguageConfiguration('unknown', {}); + } +} diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index bc4861587f..03cd45b892 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -5,8 +5,9 @@ import * as assert from 'assert'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { ColorId, FontStyle, IState, LanguageIdentifier, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/modes'; +import { ColorId, FontStyle, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/modes'; import { tokenizeLineToHTML, tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; +import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; import { ViewLineToken, ViewLineTokens } from 'vs/editor/test/common/core/viewLineToken'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; @@ -18,9 +19,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { test('TextToHtmlTokenizer 1', () => { let mode = new Mode(); - let support = TokenizationRegistry.get(mode.getId())!; + let support = TokenizationRegistry.get(mode.languageId)!; - let actual = tokenizeToString('.abc..def...gh', support); + let actual = tokenizeToString('.abc..def...gh', new LanguageIdCodec(), support); let expected = [ { className: 'mtk7', text: '.' }, { className: 'mtk9', text: 'abc' }, @@ -38,9 +39,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { test('TextToHtmlTokenizer 2', () => { let mode = new Mode(); - let support = TokenizationRegistry.get(mode.getId())!; + let support = TokenizationRegistry.get(mode.languageId)!; - let actual = tokenizeToString('.abc..def...gh\n.abc..def...gh', support); + let actual = tokenizeToString('.abc..def...gh\n.abc..def...gh', new LanguageIdCodec(), support); let expected1 = [ { className: 'mtk7', text: '.' }, { className: 'mtk9', text: 'abc' }, @@ -109,9 +110,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'Ciao', - ' ', + ' ', 'hello', - ' ', + ' ', 'world!', '
' ].join('') @@ -122,9 +123,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'Ciao', - ' ', + ' ', 'hello', - ' ', + ' ', 'w', '
' ].join('') @@ -135,9 +136,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'Ciao', - ' ', + ' ', 'hello', - ' ', + ' ', '
' ].join('') ); @@ -147,9 +148,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'iao', - ' ', + ' ', 'hello', - ' ', + ' ', '
' ].join('') ); @@ -160,7 +161,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { '
', ' ', 'hello', - ' ', + ' ', '
' ].join('') ); @@ -170,7 +171,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'hello', - ' ', + ' ', '
' ].join('') ); @@ -241,11 +242,11 @@ suite('Editor Modes - textToHtmlTokenizer', () => { tokenizeLineToHTML(text, lineTokens, colorMap, 0, 21, 4, true), [ '
', - '  ', + '  ', 'Ciao', - '   ', + '   ', 'hello', - ' ', + ' ', 'world!', '
' ].join('') @@ -255,11 +256,11 @@ suite('Editor Modes - textToHtmlTokenizer', () => { tokenizeLineToHTML(text, lineTokens, colorMap, 0, 17, 4, true), [ '
', - '  ', + '  ', 'Ciao', - '   ', + '   ', 'hello', - ' ', + ' ', 'wo', '
' ].join('') @@ -269,7 +270,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { tokenizeLineToHTML(text, lineTokens, colorMap, 0, 3, 4, true), [ '
', - '  ', + '  ', 'C', '
' ].join('') @@ -280,11 +281,11 @@ suite('Editor Modes - textToHtmlTokenizer', () => { class Mode extends MockMode { - private static readonly _id = new LanguageIdentifier('textToHtmlTokenizerMode', 3); + private static readonly _id = 'textToHtmlTokenizerMode'; constructor() { super(Mode._id); - this._register(TokenizationRegistry.register(this.getId(), { + this._register(TokenizationRegistry.register(this.languageId, { getInitialState: (): IState => null!, tokenize: undefined!, tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { diff --git a/src/vs/editor/test/common/modesTestUtils.ts b/src/vs/editor/test/common/modesTestUtils.ts index 7a3bb863db..5a669468aa 100644 --- a/src/vs/editor/test/common/modesTestUtils.ts +++ b/src/vs/editor/test/common/modesTestUtils.ts @@ -6,6 +6,7 @@ import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { MetadataConsts, StandardTokenType } from 'vs/editor/common/modes'; import { ScopedLineTokens, createScopedLineTokens } from 'vs/editor/common/modes/supports'; +import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; export interface TokenText { text: string; @@ -30,5 +31,5 @@ export function createFakeScopedLineTokens(rawTokens: TokenText[]): ScopedLineTo } LineTokens.convertToEndOffset(tokens, line.length); - return createScopedLineTokens(new LineTokens(tokens, line), 0); + return createScopedLineTokens(new LineTokens(tokens, line, new LanguageIdCodec()), 0); } diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index cebab44aa6..b8ce406ac8 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -20,6 +20,8 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getRegisteredLanguageNames(), []); + + registry.dispose(); }); test('mode with alias does have a name', () => { @@ -34,6 +36,8 @@ suite('LanguagesRegistry', () => { assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['ModeName']); assert.deepStrictEqual(registry.getLanguageName('modeId'), 'ModeName'); + + registry.dispose(); }); test('mode without alias gets a name', () => { @@ -47,6 +51,8 @@ suite('LanguagesRegistry', () => { assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['modeId']); assert.deepStrictEqual(registry.getLanguageName('modeId'), 'modeId'); + + registry.dispose(); }); test('bug #4360: f# not shown in status bar', () => { @@ -68,6 +74,8 @@ suite('LanguagesRegistry', () => { assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['ModeName']); assert.deepStrictEqual(registry.getLanguageName('modeId'), 'ModeName'); + + registry.dispose(); }); test('issue #5278: Extension cannot override language name anymore', () => { @@ -89,6 +97,8 @@ suite('LanguagesRegistry', () => { assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['BetterModeName']); assert.deepStrictEqual(registry.getLanguageName('modeId'), 'BetterModeName'); + + registry.dispose(); }); test('mimetypes are generated if necessary', () => { @@ -99,6 +109,8 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); + + registry.dispose(); }); test('first mimetype wins', () => { @@ -110,6 +122,8 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/modeId'); + + registry.dispose(); }); test('first mimetype wins 2', () => { @@ -125,6 +139,8 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); + + registry.dispose(); }); test('aliases', () => { @@ -135,7 +151,7 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('a'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); @@ -145,9 +161,9 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['A1']); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), []); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A1'), ['a']); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A2'), []); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('a'), null); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('A1'), 'a'); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('A2'), null); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); @@ -159,17 +175,19 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['A3']); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), []); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A1'), []); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A2'), []); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A3'), ['a']); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A4'), []); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('a'), null); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('A1'), null); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('A2'), null); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('A3'), 'a'); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('A4'), null); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a3'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a4'), 'a'); assert.deepStrictEqual(registry.getLanguageName('a'), 'A3'); + + registry.dispose(); }); test('empty aliases array means no alias', () => { @@ -180,7 +198,7 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('a'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); @@ -190,12 +208,14 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepStrictEqual(registry.getModeIdsFromLanguageName('b'), []); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdFromLanguageName('b'), null); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('b'), 'b'); assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); assert.deepStrictEqual(registry.getLanguageName('b'), null); + + registry.dispose(); }); test('extensions', () => { @@ -219,6 +239,8 @@ suite('LanguagesRegistry', () => { assert.deepStrictEqual(registry.getExtensions('a'), []); assert.deepStrictEqual(registry.getExtensions('aname'), []); assert.deepStrictEqual(registry.getExtensions('aName'), ['aExt', 'aExt2']); + + registry.dispose(); }); test('extensions of primary language registration come first', () => { @@ -245,6 +267,8 @@ suite('LanguagesRegistry', () => { }]); assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt'); + + registry.dispose(); }); test('filenames', () => { @@ -268,6 +292,8 @@ suite('LanguagesRegistry', () => { assert.deepStrictEqual(registry.getFilenames('a'), []); assert.deepStrictEqual(registry.getFilenames('aname'), []); assert.deepStrictEqual(registry.getFilenames('aName'), ['aFilename', 'aFilename2']); + + registry.dispose(); }); test('configuration', () => { @@ -291,5 +317,7 @@ suite('LanguagesRegistry', () => { assert.deepStrictEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename'), URI.file('/path/to/aFilename2')]); assert.deepStrictEqual(registry.getConfigurationFiles('aname'), []); assert.deepStrictEqual(registry.getConfigurationFiles('aName'), []); + + registry.dispose(); }); }); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 70ad341bb5..6d1c9c5daf 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -31,23 +31,35 @@ import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { getDocumentSemanticTokens, isSemanticTokens } from 'vs/editor/common/services/getSemanticTokens'; const GENERATE_TESTS = false; suite('ModelService', () => { + let disposables: DisposableStore; let modelService: ModelServiceImpl; setup(() => { + disposables = new DisposableStore(); const configService = new TestConfigurationService(); configService.setUserConfiguration('files', { 'eol': '\n' }); configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot')); const dialogService = new TestDialogService(); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService())); + modelService = disposables.add(new ModelServiceImpl( + configService, + new TestTextResourcePropertiesService(configService), + new TestThemeService(), + new NullLogService(), + new UndoRedoService(dialogService, new TestNotificationService()), + disposables.add(new ModeServiceImpl()), + new TestLanguageConfigurationService() + )); }); teardown(() => { - modelService.dispose(); + disposables.dispose(); }); test('EOL setting respected depending on root', () => { @@ -62,14 +74,14 @@ suite('ModelService', () => { test('_computeEdits no change', function () { - const model = createTextModel( + const model = disposables.add(createTextModel( [ 'This is line one', //16 'and this is line number two', //27 'it is followed by #3', //20 'and finished with the fourth.', //29 ].join('\n') - ); + )); const textBuffer = createTextBuffer( [ @@ -88,14 +100,14 @@ suite('ModelService', () => { test('_computeEdits first line changed', function () { - const model = createTextModel( + const model = disposables.add(createTextModel( [ 'This is line one', //16 'and this is line number two', //27 'it is followed by #3', //20 'and finished with the fourth.', //29 ].join('\n') - ); + )); const textBuffer = createTextBuffer( [ @@ -116,14 +128,14 @@ suite('ModelService', () => { test('_computeEdits EOL changed', function () { - const model = createTextModel( + const model = disposables.add(createTextModel( [ 'This is line one', //16 'and this is line number two', //27 'it is followed by #3', //20 'and finished with the fourth.', //29 ].join('\n') - ); + )); const textBuffer = createTextBuffer( [ @@ -142,14 +154,14 @@ suite('ModelService', () => { test('_computeEdits EOL and other change 1', function () { - const model = createTextModel( + const model = disposables.add(createTextModel( [ 'This is line one', //16 'and this is line number two', //27 'it is followed by #3', //20 'and finished with the fourth.', //29 ].join('\n') - ); + )); const textBuffer = createTextBuffer( [ @@ -178,13 +190,13 @@ suite('ModelService', () => { test('_computeEdits EOL and other change 2', function () { - const model = createTextModel( + const model = disposables.add(createTextModel( [ 'package main', // 1 'func foo() {', // 2 '}' // 3 ].join('\n') - ); + )); const textBuffer = createTextBuffer( [ @@ -334,6 +346,8 @@ suite('ModelService', () => { // undo model2.undo(); assert.strictEqual(model2.getValue(), 'text'); + // dispose it + modelService.destroyModel(resource); }); test('maintains version id and alternative version id for same resource and same content', () => { @@ -353,6 +367,8 @@ suite('ModelService', () => { const model2 = modelService.createModel('text1', null, resource); assert.strictEqual(model2.getVersionId(), versionId); assert.strictEqual(model2.getAlternativeVersionId(), alternativeVersionId); + // dispose it + modelService.destroyModel(resource); }); test('does not maintain undo for same resource and different content', () => { @@ -371,6 +387,8 @@ suite('ModelService', () => { // undo model2.undo(); assert.strictEqual(model2.getValue(), 'text2'); + // dispose it + modelService.destroyModel(resource); }); test('setValue should clear undo stack', () => { @@ -383,6 +401,8 @@ suite('ModelService', () => { model.setValue('text2'); model.undo(); assert.strictEqual(model.getValue(), 'text2'); + // dispose it + modelService.destroyModel(resource); }); }); @@ -404,7 +424,9 @@ suite('ModelSemanticColoring', () => { new TestTextResourcePropertiesService(configService), themeService, new NullLogService(), - new UndoRedoService(new TestDialogService(), new TestNotificationService()) + new UndoRedoService(new TestDialogService(), new TestNotificationService()), + disposables.add(new ModeServiceImpl()), + new TestLanguageConfigurationService() )); modeService = disposables.add(new ModeServiceImpl(false)); }); @@ -465,6 +487,81 @@ suite('ModelSemanticColoring', () => { // assert that it got called twice assert.strictEqual(callCount, 2); }); + + test('DocumentSemanticTokens should be pick the token provider with actual items', async () => { + + let callCount = 0; + disposables.add(ModesRegistry.registerLanguage({ id: 'testMode2' })); + disposables.add(DocumentSemanticTokensProviderRegistry.register('testMode2', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class1'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + callCount++; + // For a secondary request return a different value + if (lastResultId) { + return { + data: new Uint32Array([2, 1, 1, 1, 1, 0, 2, 1, 1, 1]) + }; + } + return { + resultId: '1', + data: new Uint32Array([0, 1, 1, 1, 1, 0, 2, 1, 1, 1]) + }; + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + disposables.add(DocumentSemanticTokensProviderRegistry.register('testMode2', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class2'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + callCount++; + return null; + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + function toArr(arr: Uint32Array): number[] { + let result: number[] = []; + for (let i = 0; i < arr.length; i++) { + result[i] = arr[i]; + } + return result; + } + + const textModel = modelService.createModel('Hello world 2', modeService.create('testMode2')); + try { + let result = await getDocumentSemanticTokens(textModel, null, null, CancellationToken.None); + assert.ok(result, `We should have tokens (1)`); + assert.ok(result.tokens, `Tokens are found from multiple providers (1)`); + assert.ok(isSemanticTokens(result.tokens), `Tokens are full (1)`); + assert.ok(result.tokens.resultId, `Token result id found from multiple providers (1)`); + assert.deepStrictEqual(toArr(result.tokens.data), [0, 1, 1, 1, 1, 0, 2, 1, 1, 1], `Token data returned for multiple providers (1)`); + assert.deepStrictEqual(callCount, 2, `Called both token providers (1)`); + assert.deepStrictEqual(result.provider.getLegend(), { tokenTypes: ['class1'], tokenModifiers: [] }, `Legend matches the tokens (1)`); + + // Make a second request. Make sure we get the secondary value + result = await getDocumentSemanticTokens(textModel, result.provider, result.tokens.resultId, CancellationToken.None); + assert.ok(result, `We should have tokens (2)`); + assert.ok(result.tokens, `Tokens are found from multiple providers (2)`); + assert.ok(isSemanticTokens(result.tokens), `Tokens are full (2)`); + assert.ok(!result.tokens.resultId, `Token result id found from multiple providers (2)`); + assert.deepStrictEqual(toArr(result.tokens.data), [2, 1, 1, 1, 1, 0, 2, 1, 1, 1], `Token data returned for multiple providers (2)`); + assert.deepStrictEqual(callCount, 4, `Called both token providers (2)`); + assert.deepStrictEqual(result.provider.getLegend(), { tokenTypes: ['class1'], tokenModifiers: [] }, `Legend matches the tokens (2)`); + } finally { + disposables.clear(); + + // Wait for scheduler to finish + await timeout(0); + + // Now dispose the text model + textModel.dispose(); + } + }); }); function assertComputeEdits(lines1: string[], lines2: string[]): void { @@ -480,6 +577,7 @@ function assertComputeEdits(lines1: string[], lines2: string[]): void { model.pushEditOperations([], edits, null); assert.strictEqual(model.getValue(), lines2.join('\n')); + model.dispose(); } function getRandomInt(min: number, max: number): number { diff --git a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts new file mode 100644 index 0000000000..e5a7969dcf --- /dev/null +++ b/src/vs/editor/test/common/services/semanticTokensProviderStyling.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 { DisposableStore } from 'vs/base/common/lifecycle'; +import { MultilineTokens2, SparseEncodedTokens } from 'vs/editor/common/model/tokensStore'; +import { MetadataConsts } from 'vs/editor/common/modes'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; +import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; +import { createModelServices } from 'vs/editor/test/common/editorTestUtils'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IThemeService, ITokenStyle } from 'vs/platform/theme/common/themeService'; + +suite('ModelService', () => { + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + + setup(() => { + [instantiationService, disposables] = createModelServices(); + }); + + teardown(() => { + disposables.dispose(); + }); + + test('issue #134973: invalid semantic tokens should be handled better', () => { + const languageId = 'java'; + disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + const legend = { + tokenTypes: ['st0', 'st1', 'st2', 'st3', 'st4', 'st5', 'st6', 'st7', 'st8', 'st9', 'st10'], + tokenModifiers: [] + }; + instantiationService.stub(IThemeService, >{ + getColorTheme() { + return { + getTokenStyleMetadata: (tokenType, tokenModifiers, languageId): ITokenStyle => { + return { + foreground: parseInt(tokenType.substr(2), 10) + }; + } + }; + } + }); + const styling = instantiationService.createInstance(SemanticTokensProviderStyling, legend); + const badTokens = { + data: new Uint32Array([ + 0, 13, 16, 1, 0, + 1, 2, 6, 2, 0, + 0, 7, 6, 3, 0, + 0, 15, 8, 4, 0, + 0, 17, 1, 5, 0, + 0, 7, 5, 6, 0, + 1, 12, 8, 7, 0, + 0, 19, 5, 8, 0, + 0, 7, 1, 9, 0, + 0, 4294967294, 5, 10, 0 + ]) + }; + const result = toMultilineTokens2(badTokens, styling, languageId); + const expected = new MultilineTokens2(1, new SparseEncodedTokens(new Uint32Array([ + 0, 13, 29, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (1 << MetadataConsts.FOREGROUND_OFFSET)), + 1, 2, 8, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (2 << MetadataConsts.FOREGROUND_OFFSET)), + 1, 9, 15, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (3 << MetadataConsts.FOREGROUND_OFFSET)), + 1, 24, 32, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (4 << MetadataConsts.FOREGROUND_OFFSET)), + 1, 41, 42, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (5 << MetadataConsts.FOREGROUND_OFFSET)), + 1, 48, 53, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (6 << MetadataConsts.FOREGROUND_OFFSET)), + 2, 12, 20, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (7 << MetadataConsts.FOREGROUND_OFFSET)), + 2, 31, 36, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (8 << MetadataConsts.FOREGROUND_OFFSET)), + 2, 36, 41, (MetadataConsts.SEMANTIC_USE_FOREGROUND | (9 << MetadataConsts.FOREGROUND_OFFSET)), + ]))); + assert.deepStrictEqual(result.toString(), expected.toString()); + }); +}); diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index f678df56a0..7ad0a931cc 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -58,7 +58,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 2, 2, 1, 3] - psc.changeValue(1, 2); + psc.setValue(1, 2); assert.strictEqual(psc.getTotalSum(), 9); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 3); @@ -67,7 +67,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(psc.getPrefixSum(4), 9); // [1, 0, 2, 1, 3] - psc.changeValue(1, 0); + psc.setValue(1, 0); assert.strictEqual(psc.getTotalSum(), 7); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -100,7 +100,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 1, 3] - psc.changeValue(2, 0); + psc.setValue(2, 0); assert.strictEqual(psc.getTotalSum(), 5); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -127,7 +127,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 0, 3] - psc.changeValue(3, 0); + psc.setValue(3, 0); assert.strictEqual(psc.getTotalSum(), 4); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -151,9 +151,9 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 1, 0, 1, 1] - psc.changeValue(1, 1); - psc.changeValue(3, 1); - psc.changeValue(4, 1); + psc.setValue(1, 1); + psc.setValue(3, 1); + psc.setValue(4, 1); assert.strictEqual(psc.getTotalSum(), 4); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 2); diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index 2fd81a3df0..03576e9be3 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -14,7 +14,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import * as modes from 'vs/editor/common/modes'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; -import { ISimpleModel, SplitLine, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { ISimpleModel, ModelLineProjection, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; import { LineBreakData, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -326,8 +326,8 @@ suite('SplitLinesCollection', () => { ] ]; - let model: TextModel | null = null; - let languageRegistration: IDisposable | null = null; + let model: TextModel; + let languageRegistration: IDisposable; setup(() => { let _lineIndex = 0; @@ -349,16 +349,14 @@ suite('SplitLinesCollection', () => { }; const LANGUAGE_ID = 'modelModeTest1'; languageRegistration = modes.TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); - model = createTextModel(_text.join('\n'), undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); + model = createTextModel(_text.join('\n'), undefined, LANGUAGE_ID); // force tokenization model.forceTokenization(model.getLineCount()); }); teardown(() => { - model!.dispose(); - model = null; - languageRegistration!.dispose(); - languageRegistration = null; + model.dispose(); + languageRegistration.dispose(); }); @@ -433,7 +431,7 @@ suite('SplitLinesCollection', () => { } test('getViewLinesData - no wrapping', () => { - withSplitLinesCollection(model!, 'off', 0, (splitLinesCollection) => { + withSplitLinesCollection(model, 'off', 0, (splitLinesCollection) => { assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); @@ -567,7 +565,7 @@ suite('SplitLinesCollection', () => { }); test('getViewLinesData - with wrapping', () => { - withSplitLinesCollection(model!, 'wordWrapColumn', 30, (splitLinesCollection) => { + withSplitLinesCollection(model, 'wordWrapColumn', 30, (splitLinesCollection) => { assert.strictEqual(splitLinesCollection.getViewLineCount(), 12); assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); @@ -740,17 +738,18 @@ suite('SplitLinesCollection', () => { }); test('getViewLinesData - with wrapping and injected text', () => { - model!.deltaDecorations([], [{ + model.deltaDecorations([], [{ range: new Range(1, 9, 1, 9), options: { description: 'example', after: { content: 'very very long injected text that causes a line break' - } + }, + showIfCollapsed: true, } }]); - withSplitLinesCollection(model!, 'wordWrapColumn', 30, (splitLinesCollection) => { + withSplitLinesCollection(model, 'wordWrapColumn', 30, (splitLinesCollection) => { assert.strictEqual(splitLinesCollection.getViewLineCount(), 14); assert.strictEqual(splitLinesCollection.getViewLineMaxColumn(1), 24); @@ -947,8 +946,8 @@ function pos(lineNumber: number, column: number): Position { return new Position(lineNumber, column); } -function createSplitLine(splitLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number, isVisible: boolean = true): SplitLine { - return new SplitLine(createLineBreakData(splitLengths, breakingOffsetsVisibleColumn, wrappedTextIndentWidth), isVisible); +function createSplitLine(splitLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number, isVisible: boolean = true): ModelLineProjection { + return new ModelLineProjection(createLineBreakData(splitLengths, breakingOffsetsVisibleColumn, wrappedTextIndentWidth), isVisible); } function createLineBreakData(breakingLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number): LineBreakData { diff --git a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts index 02ad6171b3..04f45e6fd8 100644 --- a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts @@ -310,7 +310,8 @@ suite('ViewModel', () => { description: 'test', before: { content: 'bar' - } + }, + showIfCollapsed: true } }, { @@ -319,7 +320,8 @@ suite('ViewModel', () => { description: 'test', before: { content: 'bz' - } + }, + showIfCollapsed: true } }, ]); diff --git a/src/vs/loader.js b/src/vs/loader.js index 6639473c84..d7f9bbf4b1 100644 --- a/src/vs/loader.js +++ b/src/vs/loader.js @@ -30,6 +30,7 @@ var AMDLoader; this._isNode = false; this._isElectronRenderer = false; this._isWebWorker = false; + this._isElectronNodeIntegrationWebWorker = false; } Object.defineProperty(Environment.prototype, "isWindows", { get: function () { @@ -63,6 +64,14 @@ var AMDLoader; enumerable: false, configurable: true }); + Object.defineProperty(Environment.prototype, "isElectronNodeIntegrationWebWorker", { + get: function () { + this._detect(); + return this._isElectronNodeIntegrationWebWorker; + }, + enumerable: false, + configurable: true + }); Environment.prototype._detect = function () { if (this._detected) { return; @@ -72,6 +81,7 @@ var AMDLoader; this._isNode = (typeof module !== 'undefined' && !!module.exports); this._isElectronRenderer = (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'renderer'); this._isWebWorker = (typeof AMDLoader.global.importScripts === 'function'); + this._isElectronNodeIntegrationWebWorker = this._isWebWorker && (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'worker'); }; Environment._isWindows = function () { if (typeof navigator !== 'undefined') { @@ -466,17 +476,19 @@ var AMDLoader; * Transform a module id to a location. Appends .js to module ids */ Configuration.prototype.moduleIdToPaths = function (moduleId) { - var isNodeModule = ((this.nodeModulesMap[moduleId] === true) - || (this.options.amdModulesPattern instanceof RegExp && !this.options.amdModulesPattern.test(moduleId))); - if (isNodeModule) { - // This is a node module... - if (this.isBuild()) { - // ...and we are at build time, drop it - return ['empty:']; - } - else { - // ...and at runtime we create a `shortcut`-path - return ['node|' + moduleId]; + if (this._env.isNode) { + var isNodeModule = ((this.nodeModulesMap[moduleId] === true) + || (this.options.amdModulesPattern instanceof RegExp && !this.options.amdModulesPattern.test(moduleId))); + if (isNodeModule) { + // This is a node module... + if (this.isBuild()) { + // ...and we are at build time, drop it + return ['empty:']; + } + else { + // ...and at runtime we create a `shortcut`-path + return ['node|' + moduleId]; + } } } var result = moduleId; @@ -682,39 +694,76 @@ var AMDLoader; }; return BrowserScriptLoader; }()); + function canUseEval(moduleManager) { + var trustedTypesPolicy = moduleManager.getConfig().getOptionsLiteral().trustedTypesPolicy; + try { + var func = (trustedTypesPolicy + ? self.eval(trustedTypesPolicy.createScript('', 'true')) + : new Function('true')); + func.call(self); + return true; + } + catch (err) { + return false; + } + } var WorkerScriptLoader = /** @class */ (function () { function WorkerScriptLoader() { + this._cachedCanUseEval = null; } - WorkerScriptLoader.prototype.load = function (moduleManager, scriptSrc, callback, errorback) { - var trustedTypesPolicy = moduleManager.getConfig().getOptionsLiteral().trustedTypesPolicy; - var isCrossOrigin = (/^((http:)|(https:)|(file:))/.test(scriptSrc) && scriptSrc.substring(0, self.origin.length) !== self.origin); - if (!isCrossOrigin) { - // use `fetch` if possible because `importScripts` - // is synchronous and can lead to deadlocks on Safari - fetch(scriptSrc).then(function (response) { - if (response.status !== 200) { - throw new Error(response.statusText); - } - return response.text(); - }).then(function (text) { - text = text + "\n//# sourceURL=" + scriptSrc; - var func = (trustedTypesPolicy - ? self.eval(trustedTypesPolicy.createScript('', text)) - : new Function(text)); - func.call(self); - callback(); - }).then(undefined, errorback); - return; + WorkerScriptLoader.prototype._canUseEval = function (moduleManager) { + if (this._cachedCanUseEval === null) { + this._cachedCanUseEval = canUseEval(moduleManager); } - try { - if (trustedTypesPolicy) { - scriptSrc = trustedTypesPolicy.createScriptURL(scriptSrc); + return this._cachedCanUseEval; + }; + WorkerScriptLoader.prototype.load = function (moduleManager, scriptSrc, callback, errorback) { + if (/^node\|/.test(scriptSrc)) { + var opts = moduleManager.getConfig().getOptionsLiteral(); + var nodeRequire = ensureRecordedNodeRequire(moduleManager.getRecorder(), (opts.nodeRequire || AMDLoader.global.nodeRequire)); + var pieces = scriptSrc.split('|'); + var moduleExports_2 = null; + try { + moduleExports_2 = nodeRequire(pieces[1]); } - importScripts(scriptSrc); + catch (err) { + errorback(err); + return; + } + moduleManager.enqueueDefineAnonymousModule([], function () { return moduleExports_2; }); callback(); } - catch (e) { - errorback(e); + else { + var trustedTypesPolicy_1 = moduleManager.getConfig().getOptionsLiteral().trustedTypesPolicy; + var isCrossOrigin = (/^((http:)|(https:)|(file:))/.test(scriptSrc) && scriptSrc.substring(0, self.origin.length) !== self.origin); + if (!isCrossOrigin && this._canUseEval(moduleManager)) { + // use `fetch` if possible because `importScripts` + // is synchronous and can lead to deadlocks on Safari + fetch(scriptSrc).then(function (response) { + if (response.status !== 200) { + throw new Error(response.statusText); + } + return response.text(); + }).then(function (text) { + text = text + "\n//# sourceURL=" + scriptSrc; + var func = (trustedTypesPolicy_1 + ? self.eval(trustedTypesPolicy_1.createScript('', text)) + : new Function(text)); + func.call(self); + callback(); + }).then(undefined, errorback); + return; + } + try { + if (trustedTypesPolicy_1) { + scriptSrc = trustedTypesPolicy_1.createScriptURL(scriptSrc); + } + importScripts(scriptSrc); + callback(); + } + catch (e) { + errorback(e); + } } }; return WorkerScriptLoader; @@ -818,15 +867,15 @@ var AMDLoader; var recorder = moduleManager.getRecorder(); if (/^node\|/.test(scriptSrc)) { var pieces = scriptSrc.split('|'); - var moduleExports_2 = null; + var moduleExports_3 = null; try { - moduleExports_2 = nodeRequire(pieces[1]); + moduleExports_3 = nodeRequire(pieces[1]); } catch (err) { errorback(err); return; } - moduleManager.enqueueDefineAnonymousModule([], function () { return moduleExports_2; }); + moduleManager.enqueueDefineAnonymousModule([], function () { return moduleExports_3; }); callback(); } else { @@ -1482,7 +1531,7 @@ var AMDLoader; ModuleManager.prototype._onLoadError = function (moduleId, err) { var error = this._createLoadError(moduleId, err); if (!this._modules2[moduleId]) { - this._modules2[moduleId] = new Module(moduleId, this._moduleIdProvider.getStrModuleId(moduleId), [], function () { }, function () { }, null); + this._modules2[moduleId] = new Module(moduleId, this._moduleIdProvider.getStrModuleId(moduleId), [], function () { }, null, null); } // Find any 'local' error handlers, walk the entire chain of inverse dependencies if necessary. var seenModuleId = []; @@ -1873,9 +1922,7 @@ var AMDLoader; RequireFunc.getStats = function () { return moduleManager.getLoaderEvents(); }; - RequireFunc.define = function () { - return DefineFunc.apply(null, arguments); - }; + RequireFunc.define = DefineFunc; function init() { if (typeof AMDLoader.global.require !== 'undefined' || typeof require !== 'undefined') { var _nodeRequire = (AMDLoader.global.require || require); @@ -1887,7 +1934,7 @@ var AMDLoader; RequireFunc.__$__nodeRequire = nodeRequire; } } - if (env.isNode && !env.isElectronRenderer) { + if (env.isNode && !env.isElectronRenderer && !env.isElectronNodeIntegrationWebWorker) { module.exports = RequireFunc; require = RequireFunc; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8649e0cb74..84ac121e5a 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -215,7 +215,6 @@ declare namespace monaco { query: string; fragment: string; } - /** * 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 @@ -247,42 +246,42 @@ declare namespace monaco { DownArrow = 18, Insert = 19, Delete = 20, - KEY_0 = 21, - KEY_1 = 22, - KEY_2 = 23, - KEY_3 = 24, - KEY_4 = 25, - KEY_5 = 26, - KEY_6 = 27, - KEY_7 = 28, - KEY_8 = 29, - KEY_9 = 30, - KEY_A = 31, - KEY_B = 32, - KEY_C = 33, - KEY_D = 34, - KEY_E = 35, - KEY_F = 36, - KEY_G = 37, - KEY_H = 38, - KEY_I = 39, - KEY_J = 40, - KEY_K = 41, - KEY_L = 42, - KEY_M = 43, - KEY_N = 44, - KEY_O = 45, - KEY_P = 46, - KEY_Q = 47, - KEY_R = 48, - KEY_S = 49, - KEY_T = 50, - KEY_U = 51, - KEY_V = 52, - KEY_W = 53, - KEY_X = 54, - KEY_Y = 55, - KEY_Z = 56, + Digit0 = 21, + Digit1 = 22, + Digit2 = 23, + Digit3 = 24, + Digit4 = 25, + Digit5 = 26, + Digit6 = 27, + Digit7 = 28, + Digit8 = 29, + Digit9 = 30, + KeyA = 31, + KeyB = 32, + KeyC = 33, + KeyD = 34, + KeyE = 35, + KeyF = 36, + KeyG = 37, + KeyH = 38, + KeyI = 39, + KeyJ = 40, + KeyK = 41, + KeyL = 42, + KeyM = 43, + KeyN = 44, + KeyO = 45, + KeyP = 46, + KeyQ = 47, + KeyR = 48, + KeyS = 49, + KeyT = 50, + KeyU = 51, + KeyV = 52, + KeyW = 53, + KeyX = 54, + KeyY = 55, + KeyZ = 56, Meta = 57, ContextMenu = 58, F1 = 59, @@ -310,57 +309,57 @@ declare namespace monaco { * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the ';:' key */ - US_SEMICOLON = 80, + Semicolon = 80, /** * For any country/region, the '+' key * For the US standard keyboard, the '=+' key */ - US_EQUAL = 81, + Equal = 81, /** * For any country/region, the ',' key * For the US standard keyboard, the ',<' key */ - US_COMMA = 82, + Comma = 82, /** * For any country/region, the '-' key * For the US standard keyboard, the '-_' key */ - US_MINUS = 83, + Minus = 83, /** * For any country/region, the '.' key * For the US standard keyboard, the '.>' key */ - US_DOT = 84, + Period = 84, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the '/?' key */ - US_SLASH = 85, + Slash = 85, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the '`~' key */ - US_BACKTICK = 86, + Backquote = 86, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the '[{' key */ - US_OPEN_SQUARE_BRACKET = 87, + BracketLeft = 87, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the '\|' key */ - US_BACKSLASH = 88, + Backslash = 88, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the ']}' key */ - US_CLOSE_SQUARE_BRACKET = 89, + BracketRight = 89, /** * Used for miscellaneous characters; it can vary by keyboard. * For the US standard keyboard, the ''"' key */ - US_QUOTE = 90, + Quote = 90, /** * Used for miscellaneous characters; it can vary by keyboard. */ @@ -368,34 +367,48 @@ declare namespace monaco { /** * Either the angle bracket key or the backslash key on the RT 102-key keyboard. */ - OEM_102 = 92, - NUMPAD_0 = 93, - NUMPAD_1 = 94, - NUMPAD_2 = 95, - NUMPAD_3 = 96, - NUMPAD_4 = 97, - NUMPAD_5 = 98, - NUMPAD_6 = 99, - NUMPAD_7 = 100, - NUMPAD_8 = 101, - NUMPAD_9 = 102, - NUMPAD_MULTIPLY = 103, - NUMPAD_ADD = 104, + IntlBackslash = 92, + Numpad0 = 93, + Numpad1 = 94, + Numpad2 = 95, + Numpad3 = 96, + Numpad4 = 97, + Numpad5 = 98, + Numpad6 = 99, + Numpad7 = 100, + Numpad8 = 101, + Numpad9 = 102, + NumpadMultiply = 103, + NumpadAdd = 104, NUMPAD_SEPARATOR = 105, - NUMPAD_SUBTRACT = 106, - NUMPAD_DECIMAL = 107, - NUMPAD_DIVIDE = 108, + NumpadSubtract = 106, + NumpadDecimal = 107, + NumpadDivide = 108, /** * Cover all key codes when IME is processing input. */ KEY_IN_COMPOSITION = 109, ABNT_C1 = 110, ABNT_C2 = 111, + AudioVolumeMute = 112, + AudioVolumeUp = 113, + AudioVolumeDown = 114, + BrowserSearch = 115, + BrowserHome = 116, + BrowserBack = 117, + BrowserForward = 118, + MediaTrackNext = 119, + MediaTrackPrevious = 120, + MediaStop = 121, + MediaPlayPause = 122, + LaunchMediaPlayer = 123, + LaunchMail = 124, + LaunchApp2 = 125, /** * Placed last to cover the length of the enum. * Please do not depend on this value! */ - MAX_VALUE = 112 + MAX_VALUE = 126 } export class KeyMod { static readonly CtrlCmd: number; @@ -409,6 +422,7 @@ declare namespace monaco { readonly value: string; readonly isTrusted?: boolean; readonly supportThemeIcons?: boolean; + readonly supportHtml?: boolean; uris?: { [href: string]: UriComponents; }; @@ -788,6 +802,10 @@ declare namespace monaco { * Get the position at `positionLineNumber` and `positionColumn`. */ getPosition(): Position; + /** + * Get the position at the start of the selection. + */ + getSelectionStart(): Position; /** * Create a new selection with a different `selectionStartLineNumber` and `selectionStartColumn`. */ @@ -796,6 +814,10 @@ declare namespace monaco { * Create a `Selection` from one or two positions */ static fromPositions(start: IPosition, end?: IPosition): Selection; + /** + * Creates a `Selection` from a range, given a direction. + */ + static fromRange(range: Range, direction: SelectionDirection): Selection; /** * Create a `Selection` from an `ISelection`. */ @@ -866,7 +888,7 @@ declare namespace monaco.editor { * `domElement` should be empty (not contain other dom nodes). * The editor will read the size of `domElement`. */ - export function createDiffEditor(domElement: HTMLElement, options?: IDiffEditorConstructionOptions, override?: IEditorOverrideServices): IStandaloneDiffEditor; + export function createDiffEditor(domElement: HTMLElement, options?: IStandaloneDiffEditorConstructionOptions, override?: IEditorOverrideServices): IStandaloneDiffEditor; export interface IDiffNavigatorOptions { readonly followsCaret?: boolean; @@ -1179,12 +1201,12 @@ declare namespace monaco.editor { model?: ITextModel | null; /** * The initial value of the auto created model in the editor. - * To not create automatically a model, use `model: null`. + * To not automatically create a model, use `model: null`. */ value?: string; /** * The initial language of the auto created model in the editor. - * To not create automatically a model, use `model: null`. + * To not automatically create a model, use `model: null`. */ language?: string; /** @@ -1207,12 +1229,17 @@ declare namespace monaco.editor { * Defaults to "https://go.microsoft.com/fwlink/?linkid=852450" */ accessibilityHelpUrl?: string; + /** + * Container element to use for ARIA messages. + * Defaults to document.body. + */ + ariaContainerElement?: HTMLElement; } /** * The options to create a diff editor. */ - export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + export interface IStandaloneDiffEditorConstructionOptions extends IDiffEditorConstructionOptions { /** * Initial theme to be used for rendering. * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. @@ -1402,7 +1429,8 @@ declare namespace monaco.editor { isWholeLine?: boolean; /** * Specifies the stack order of a decoration. - * A decoration with greater stack order is always in front of a decoration with a lower stack order. + * A decoration with greater stack order is always in front of a decoration with + * a lower stack order when the decorations are on the same line. */ zIndex?: number; /** @@ -1870,7 +1898,7 @@ declare namespace monaco.editor { /** * Get the language associated with this model. */ - getModeId(): string; + getLanguageId(): string; /** * Get the word under or besides `position`. * @param position The position to look for a word. @@ -2035,8 +2063,7 @@ declare namespace monaco.editor { */ onWillDispose(listener: () => void): IDisposable; /** - * Destroy this model. This will unbind the model from the mode - * and make all necessary clean-up to release this object to the GC. + * Destroy this model. */ dispose(): void; /** @@ -2467,7 +2494,7 @@ declare namespace monaco.editor { }; /** - * An event describing that the current mode associated with a model has changed. + * An event describing that the current language associated with a model has changed. */ export interface IModelLanguageChangedEvent { /** @@ -3210,16 +3237,6 @@ declare namespace monaco.editor { * Defaults to false. */ renderControlCharacters?: boolean; - /** - * Enable rendering of indent guides. - * Defaults to true. - */ - renderIndentGuides?: boolean; - /** - * Enable highlighting of the active indent guide. - * Defaults to true. - */ - highlightActiveIndentGuide?: boolean; /** * Enable rendering of current line highlight. * Defaults to all. @@ -3280,12 +3297,13 @@ declare namespace monaco.editor { * Control if the editor should use shadow DOM. */ useShadowDOM?: boolean; + /** + * Controls the behavior of editor guides. + */ + guides?: IGuidesOptions; } - /** - * Configuration options for the diff editor. - */ - export interface IDiffEditorOptions extends IEditorOptions { + export interface IDiffEditorBaseOptions { /** * Allow the user to resize the diff editor split view. * Defaults to true. @@ -3301,6 +3319,11 @@ declare namespace monaco.editor { * Defaults to 5000. */ maxComputationTime?: number; + /** + * Maximum supported file size in MB. + * Defaults to 50. + */ + maxFileSize?: number; /** * Compute the diff by ignoring leading/trailing whitespace * Defaults to true. @@ -3325,11 +3348,6 @@ declare namespace monaco.editor { * Defaults to false. */ diffCodeLens?: boolean; - /** - * Is the diff editor inside another editor - * Defaults to false - */ - isInEmbeddedEditor?: boolean; /** * Is the diff editor should render overview ruler * Defaults to true @@ -3339,14 +3357,12 @@ declare namespace monaco.editor { * Control the wrapping of the diff editor. */ diffWordWrap?: 'off' | 'on' | 'inherit'; - /** - * Aria label for original editor. - */ - originalAriaLabel?: string; - /** - * Aria label for modified editor. - */ - modifiedAriaLabel?: string; + } + + /** + * Configuration options for the diff editor. + */ + export interface IDiffEditorOptions extends IEditorOptions, IDiffEditorBaseOptions { } /** @@ -3385,8 +3401,6 @@ declare namespace monaco.editor { ignoreEmptyLines?: boolean; } - export type EditorCommentsOptions = Readonly>; - /** * The kind of animation in which the editor's cursor should be rendered. */ @@ -3470,8 +3484,6 @@ declare namespace monaco.editor { loop?: boolean; } - export type EditorFindOptions = Readonly>; - export type GoToLocationValues = 'peek' | 'gotoAndPeek' | 'goto'; /** @@ -3491,8 +3503,6 @@ declare namespace monaco.editor { alternativeReferenceCommand?: string; } - export type GoToLocationOptions = Readonly>; - /** * Configuration options for editor hover */ @@ -3512,10 +3522,13 @@ declare namespace monaco.editor { * Defaults to true. */ sticky?: boolean; + /** + * Should the hover be shown above the line if possible? + * Defaults to false. + */ + above?: boolean; } - export type EditorHoverOptions = Readonly>; - /** * A description for the overview ruler position. */ @@ -3641,8 +3654,6 @@ declare namespace monaco.editor { enabled?: boolean; } - export type EditorLightbulbOptions = Readonly>; - /** * Configuration options for editor inlayHints */ @@ -3664,8 +3675,6 @@ declare namespace monaco.editor { fontFamily?: string; } - export type EditorInlayHintsOptions = Readonly>; - /** * Configuration options for editor minimap */ @@ -3706,8 +3715,6 @@ declare namespace monaco.editor { scale?: number; } - export type EditorMinimapOptions = Readonly>; - /** * Configuration options for editor padding */ @@ -3722,11 +3729,6 @@ declare namespace monaco.editor { bottom?: number; } - export interface InternalEditorPaddingOptions { - readonly top: number; - readonly bottom: number; - } - /** * Configuration options for parameter hints */ @@ -3743,8 +3745,6 @@ declare namespace monaco.editor { cycle?: boolean; } - export type InternalParameterHintOptions = Readonly>; - /** * Configuration options for quick suggestions */ @@ -3754,8 +3754,6 @@ declare namespace monaco.editor { strings?: boolean; } - export type ValidQuickSuggestionsOptions = boolean | Readonly>; - export type LineNumbersType = 'on' | 'off' | 'relative' | 'interval' | ((lineNumber: number) => string); export enum RenderLineNumbersType { @@ -3885,8 +3883,6 @@ declare namespace monaco.editor { mode?: 'prefix' | 'subword' | 'subwordSmart'; } - export type InternalInlineSuggestOptions = Readonly>; - export interface IBracketPairColorizationOptions { /** * Enable or disable bracket pair colorization. @@ -3894,7 +3890,33 @@ declare namespace monaco.editor { enabled?: boolean; } - export type InternalBracketPairColorizationOptions = Readonly>; + export interface IGuidesOptions { + /** + * Enable rendering of bracket pair guides. + * Defaults to false. + */ + bracketPairs?: boolean | 'active'; + /** + * Enable rendering of vertical bracket pair guides. + * Defaults to 'active'. + */ + bracketPairsHorizontal?: boolean | 'active'; + /** + * Enable highlighting of the active bracket pair. + * Defaults to true. + */ + highlightActiveBracketPair?: boolean; + /** + * Enable rendering of indent guides. + * Defaults to true. + */ + indentation?: boolean; + /** + * Enable highlighting of the active indent guide. + * Defaults to true. + */ + highlightActiveIndentation?: boolean; + } /** * Configuration options for editor suggest widget @@ -4054,14 +4076,10 @@ declare namespace monaco.editor { showSnippets?: boolean; } - export type InternalSuggestOptions = Readonly>; - export interface ISmartSelectOptions { selectLeadingAndTrailingWhitespace?: boolean; } - export type SmartSelectOptions = Readonly>; - /** * Describes how to indent wrapped lines. */ @@ -4105,45 +4123,45 @@ declare namespace monaco.editor { automaticLayout = 10, autoSurround = 11, bracketPairColorization = 12, - codeLens = 13, - codeLensFontFamily = 14, - codeLensFontSize = 15, - colorDecorators = 16, - columnSelection = 17, - comments = 18, - contextmenu = 19, - copyWithSyntaxHighlighting = 20, - cursorBlinking = 21, - cursorSmoothCaretAnimation = 22, - cursorStyle = 23, - cursorSurroundingLines = 24, - cursorSurroundingLinesStyle = 25, - cursorWidth = 26, - disableLayerHinting = 27, - disableMonospaceOptimizations = 28, - domReadOnly = 29, - dragAndDrop = 30, - emptySelectionClipboard = 31, - extraEditorClassName = 32, - fastScrollSensitivity = 33, - find = 34, - fixedOverflowWidgets = 35, - folding = 36, - foldingStrategy = 37, - foldingHighlight = 38, - foldingImportsByDefault = 39, - unfoldOnClickAfterEndOfLine = 40, - fontFamily = 41, - fontInfo = 42, - fontLigatures = 43, - fontSize = 44, - fontWeight = 45, - formatOnPaste = 46, - formatOnType = 47, - glyphMargin = 48, - gotoLocation = 49, - hideCursorInOverviewRuler = 50, - highlightActiveIndentGuide = 51, + guides = 13, + codeLens = 14, + codeLensFontFamily = 15, + codeLensFontSize = 16, + colorDecorators = 17, + columnSelection = 18, + comments = 19, + contextmenu = 20, + copyWithSyntaxHighlighting = 21, + cursorBlinking = 22, + cursorSmoothCaretAnimation = 23, + cursorStyle = 24, + cursorSurroundingLines = 25, + cursorSurroundingLinesStyle = 26, + cursorWidth = 27, + disableLayerHinting = 28, + disableMonospaceOptimizations = 29, + domReadOnly = 30, + dragAndDrop = 31, + emptySelectionClipboard = 32, + extraEditorClassName = 33, + fastScrollSensitivity = 34, + find = 35, + fixedOverflowWidgets = 36, + folding = 37, + foldingStrategy = 38, + foldingHighlight = 39, + foldingImportsByDefault = 40, + unfoldOnClickAfterEndOfLine = 41, + fontFamily = 42, + fontInfo = 43, + fontLigatures = 44, + fontSize = 45, + fontWeight = 46, + formatOnPaste = 47, + formatOnType = 48, + glyphMargin = 49, + gotoLocation = 50, + hideCursorInOverviewRuler = 51, hover = 52, inDiffEditor = 53, inlineSuggest = 54, @@ -4175,56 +4193,56 @@ declare namespace monaco.editor { readOnly = 80, renameOnType = 81, renderControlCharacters = 82, - renderIndentGuides = 83, - renderFinalNewline = 84, - renderLineHighlight = 85, - renderLineHighlightOnlyWhenFocus = 86, - renderValidationDecorations = 87, - renderWhitespace = 88, - revealHorizontalRightPadding = 89, - roundedSelection = 90, - rulers = 91, - scrollbar = 92, - scrollBeyondLastColumn = 93, - scrollBeyondLastLine = 94, - scrollPredominantAxis = 95, - selectionClipboard = 96, - selectionHighlight = 97, - selectOnLineNumbers = 98, - showFoldingControls = 99, - showUnused = 100, - snippetSuggestions = 101, - smartSelect = 102, - smoothScrolling = 103, - stickyTabStops = 104, - stopRenderingLineAfter = 105, - suggest = 106, - suggestFontSize = 107, - suggestLineHeight = 108, - suggestOnTriggerCharacters = 109, - suggestSelection = 110, - tabCompletion = 111, - tabIndex = 112, - unusualLineTerminators = 113, - useShadowDOM = 114, - useTabStops = 115, - wordSeparators = 116, - wordWrap = 117, - wordWrapBreakAfterCharacters = 118, - wordWrapBreakBeforeCharacters = 119, - wordWrapColumn = 120, - wordWrapOverride1 = 121, - wordWrapOverride2 = 122, - wrappingIndent = 123, - wrappingStrategy = 124, - showDeprecated = 125, - inlayHints = 126, - editorClassName = 127, - pixelRatio = 128, - tabFocusMode = 129, - layoutInfo = 130, - wrappingInfo = 131 + renderFinalNewline = 83, + renderLineHighlight = 84, + renderLineHighlightOnlyWhenFocus = 85, + renderValidationDecorations = 86, + renderWhitespace = 87, + revealHorizontalRightPadding = 88, + roundedSelection = 89, + rulers = 90, + scrollbar = 91, + scrollBeyondLastColumn = 92, + scrollBeyondLastLine = 93, + scrollPredominantAxis = 94, + selectionClipboard = 95, + selectionHighlight = 96, + selectOnLineNumbers = 97, + showFoldingControls = 98, + showUnused = 99, + snippetSuggestions = 100, + smartSelect = 101, + smoothScrolling = 102, + stickyTabStops = 103, + stopRenderingLineAfter = 104, + suggest = 105, + suggestFontSize = 106, + suggestLineHeight = 107, + suggestOnTriggerCharacters = 108, + suggestSelection = 109, + tabCompletion = 110, + tabIndex = 111, + unusualLineTerminators = 112, + useShadowDOM = 113, + useTabStops = 114, + wordSeparators = 115, + wordWrap = 116, + wordWrapBreakAfterCharacters = 117, + wordWrapBreakBeforeCharacters = 118, + wordWrapColumn = 119, + wordWrapOverride1 = 120, + wordWrapOverride2 = 121, + wrappingIndent = 122, + wrappingStrategy = 123, + showDeprecated = 124, + inlayHints = 125, + editorClassName = 126, + pixelRatio = 127, + tabFocusMode = 128, + layoutInfo = 129, + wrappingInfo = 130 } + export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; acceptSuggestionOnEnter: IEditorOption; @@ -4238,14 +4256,15 @@ declare namespace monaco.editor { autoIndent: IEditorOption; automaticLayout: IEditorOption; autoSurround: IEditorOption; - bracketPairColorization: IEditorOption; + bracketPairColorization: IEditorOption>>; + bracketPairGuides: IEditorOption>>; stickyTabStops: IEditorOption; codeLens: IEditorOption; codeLensFontFamily: IEditorOption; codeLensFontSize: IEditorOption; colorDecorators: IEditorOption; columnSelection: IEditorOption; - comments: IEditorOption; + comments: IEditorOption>>; contextmenu: IEditorOption; copyWithSyntaxHighlighting: IEditorOption; cursorBlinking: IEditorOption; @@ -4261,7 +4280,7 @@ declare namespace monaco.editor { emptySelectionClipboard: IEditorOption; extraEditorClassName: IEditorOption; fastScrollSensitivity: IEditorOption; - find: IEditorOption; + find: IEditorOption>>; fixedOverflowWidgets: IEditorOption; folding: IEditorOption; foldingStrategy: IEditorOption; @@ -4276,13 +4295,12 @@ declare namespace monaco.editor { formatOnPaste: IEditorOption; formatOnType: IEditorOption; glyphMargin: IEditorOption; - gotoLocation: IEditorOption; + gotoLocation: IEditorOption>>; hideCursorInOverviewRuler: IEditorOption; - highlightActiveIndentGuide: IEditorOption; - hover: IEditorOption; + hover: IEditorOption>>; inDiffEditor: IEditorOption; letterSpacing: IEditorOption; - lightbulb: IEditorOption; + lightbulb: IEditorOption>>; lineDecorationsWidth: IEditorOption; lineHeight: IEditorOption; lineNumbers: IEditorOption; @@ -4290,7 +4308,7 @@ declare namespace monaco.editor { linkedEditing: IEditorOption; links: IEditorOption; matchBrackets: IEditorOption; - minimap: IEditorOption; + minimap: IEditorOption>>; mouseStyle: IEditorOption; mouseWheelScrollSensitivity: IEditorOption; mouseWheelZoom: IEditorOption; @@ -4300,16 +4318,15 @@ declare namespace monaco.editor { occurrencesHighlight: IEditorOption; overviewRulerBorder: IEditorOption; overviewRulerLanes: IEditorOption; - padding: IEditorOption; - parameterHints: IEditorOption; + padding: IEditorOption>>; + parameterHints: IEditorOption>>; peekWidgetDefaultFocus: IEditorOption; definitionLinkOpensInPeek: IEditorOption; - quickSuggestions: IEditorOption; + quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; readOnly: IEditorOption; renameOnType: IEditorOption; renderControlCharacters: IEditorOption; - renderIndentGuides: IEditorOption; renderFinalNewline: IEditorOption; renderLineHighlight: IEditorOption; renderLineHighlightOnlyWhenFocus: IEditorOption; @@ -4328,13 +4345,13 @@ declare namespace monaco.editor { showFoldingControls: IEditorOption; showUnused: IEditorOption; showDeprecated: IEditorOption; - inlayHints: IEditorOption; + inlayHints: IEditorOption>>; snippetSuggestions: IEditorOption; - smartSelect: IEditorOption; + smartSelect: IEditorOption>>; smoothScrolling: IEditorOption; stopRenderingLineAfter: IEditorOption; - suggest: IEditorOption; - inlineSuggest: IEditorOption; + suggest: IEditorOption>>; + inlineSuggest: IEditorOption>>; suggestFontSize: IEditorOption; suggestLineHeight: IEditorOption; suggestOnTriggerCharacters: IEditorOption; @@ -4679,7 +4696,7 @@ declare namespace monaco.editor { */ export interface IPasteEvent { readonly range: Range; - readonly mode: string | null; + readonly languageId: string | null; } export interface IEditorConstructionOptions extends IEditorOptions { @@ -4704,6 +4721,19 @@ declare namespace monaco.editor { * Defaults to an internal DOM node. */ overflowWidgetsDomNode?: HTMLElement; + /** + * Aria label for original editor. + */ + originalAriaLabel?: string; + /** + * Aria label for modified editor. + */ + modifiedAriaLabel?: string; + /** + * Is the diff editor inside another editor + * Defaults to false + */ + isInEmbeddedEditor?: boolean; } /** @@ -4714,47 +4744,47 @@ declare namespace monaco.editor { * An event emitted when the content of the current model has changed. * @event */ - onDidChangeModelContent(listener: (e: IModelContentChangedEvent) => void): IDisposable; + onDidChangeModelContent: IEvent; /** * An event emitted when the language of the current model has changed. * @event */ - onDidChangeModelLanguage(listener: (e: IModelLanguageChangedEvent) => void): IDisposable; + onDidChangeModelLanguage: IEvent; /** * An event emitted when the language configuration of the current model has changed. * @event */ - onDidChangeModelLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) => void): IDisposable; + onDidChangeModelLanguageConfiguration: IEvent; /** * An event emitted when the options of the current model has changed. * @event */ - onDidChangeModelOptions(listener: (e: IModelOptionsChangedEvent) => void): IDisposable; + onDidChangeModelOptions: IEvent; /** * An event emitted when the configuration of the editor has changed. (e.g. `editor.updateOptions()`) * @event */ - onDidChangeConfiguration(listener: (e: ConfigurationChangedEvent) => void): IDisposable; + onDidChangeConfiguration: IEvent; /** * An event emitted when the cursor position has changed. * @event */ - onDidChangeCursorPosition(listener: (e: ICursorPositionChangedEvent) => void): IDisposable; + onDidChangeCursorPosition: IEvent; /** * An event emitted when the cursor selection has changed. * @event */ - onDidChangeCursorSelection(listener: (e: ICursorSelectionChangedEvent) => void): IDisposable; + onDidChangeCursorSelection: IEvent; /** * An event emitted when the model of this editor has changed (e.g. `editor.setModel()`). * @event */ - onDidChangeModel(listener: (e: IModelChangedEvent) => void): IDisposable; + onDidChangeModel: IEvent; /** * An event emitted when the decorations of the current model have changed. * @event */ - onDidChangeModelDecorations(listener: (e: IModelDecorationsChangedEvent) => void): IDisposable; + onDidChangeModelDecorations: IEvent; /** * An event emitted when the text inside this editor gained focus (i.e. cursor starts blinking). * @event @@ -4792,57 +4822,62 @@ declare namespace monaco.editor { * An event emitted when users paste text in the editor. * @event */ - onDidPaste(listener: (e: IPasteEvent) => void): IDisposable; + onDidPaste: IEvent; /** * An event emitted on a "mouseup". * @event */ - onMouseUp(listener: (e: IEditorMouseEvent) => void): IDisposable; + onMouseUp: IEvent; /** * An event emitted on a "mousedown". * @event */ - onMouseDown(listener: (e: IEditorMouseEvent) => void): IDisposable; + onMouseDown: IEvent; /** * An event emitted on a "contextmenu". * @event */ - onContextMenu(listener: (e: IEditorMouseEvent) => void): IDisposable; + onContextMenu: IEvent; /** * An event emitted on a "mousemove". * @event */ - onMouseMove(listener: (e: IEditorMouseEvent) => void): IDisposable; + onMouseMove: IEvent; /** * An event emitted on a "mouseleave". * @event */ - onMouseLeave(listener: (e: IPartialEditorMouseEvent) => void): IDisposable; + onMouseLeave: IEvent; /** * An event emitted on a "keyup". * @event */ - onKeyUp(listener: (e: IKeyboardEvent) => void): IDisposable; + onKeyUp: IEvent; /** * An event emitted on a "keydown". * @event */ - onKeyDown(listener: (e: IKeyboardEvent) => void): IDisposable; + onKeyDown: IEvent; /** * An event emitted when the layout of the editor has changed. * @event */ - onDidLayoutChange(listener: (e: EditorLayoutInfo) => void): IDisposable; + onDidLayoutChange: IEvent; /** * An event emitted when the content width or content height in the editor has changed. * @event */ - onDidContentSizeChange(listener: (e: IContentSizeChangedEvent) => void): IDisposable; + onDidContentSizeChange: IEvent; /** * An event emitted when the scroll in the editor has changed. * @event */ - onDidScrollChange(listener: (e: IScrollEvent) => void): IDisposable; + onDidScrollChange: IEvent; + /** + * An event emitted when hidden areas change in the editor (e.g. due to folding). + * @event + */ + onDidChangeHiddenAreas: IEvent; /** * Saves current view state of the editor in a serializable object. */ @@ -5358,7 +5393,7 @@ declare namespace monaco.languages { /** * Register a code action provider (used by e.g. quick fix). */ - export function registerCodeActionProvider(languageId: string, provider: CodeActionProvider): IDisposable; + export function registerCodeActionProvider(languageId: string, provider: CodeActionProvider, metadata?: CodeActionProviderMetadata): IDisposable; /** * Register a formatter that can handle only entire models. @@ -5449,6 +5484,25 @@ declare namespace monaco.languages { * Provide commands for the given document and range. */ provideCodeActions(model: editor.ITextModel, range: Range, context: CodeActionContext, token: CancellationToken): ProviderResult; + /** + * Given a code action fill in the edit. Will only invoked when missing. + */ + resolveCodeAction?(codeAction: CodeAction, token: CancellationToken): ProviderResult; + } + + /** + * Metadata about the type of code actions that a {@link CodeActionProvider} provides. + */ + export interface CodeActionProviderMetadata { + /** + * List of code action kinds 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 `["quickfix", "refactor", "source"]`, or list out every kind provided, + * such as `["quickfix.removeLine", "source.fixAll" ...]`. + */ + readonly providedCodeActionKinds?: readonly string[]; } /** @@ -5506,6 +5560,11 @@ declare namespace monaco.languages { * settings will be used. */ surroundingPairs?: IAutoClosingPair[]; + /** + * Defines a list of bracket pairs that are colorized depending on their nesting level. + * If not set, the configured brackets will be used. + */ + colorizedBracketPairs?: CharacterPair[]; /** * Defines what characters must be after the cursor for bracket or quote autoclosing to occur when using the \'languageDefined\' autoclosing setting. * @@ -5770,6 +5829,11 @@ declare namespace monaco.languages { InsertAsSnippet = 4 } + export interface CompletionItemRanges { + insert: IRange; + replace: IRange; + } + /** * A completion item represents a text snippet that is * proposed to complete text that is being typed. @@ -5838,10 +5902,7 @@ declare namespace monaco.languages { * *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; - }; + range: IRange | CompletionItemRanges; /** * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous @@ -5939,6 +6000,12 @@ declare namespace monaco.languages { * How the completion was triggered. */ readonly triggerKind: InlineCompletionTriggerKind; + readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined; + } + + export interface SelectedSuggestionInfo { + range: IRange; + text: string; } export interface InlineCompletion { @@ -6633,7 +6700,7 @@ declare namespace monaco.languages { } export interface InlayHintsProvider { - onDidChangeInlayHints?: IEvent | undefined; + onDidChangeInlayHints?: IEvent; provideInlayHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; } diff --git a/src/vs/nls.js b/src/vs/nls.js index 9996108163..6b2b03d48d 100644 --- a/src/vs/nls.js +++ b/src/vs/nls.js @@ -33,7 +33,7 @@ var NLSLoaderPlugin; this._detect(); return this._isPseudo; }, - enumerable: true, + enumerable: false, configurable: true }); Environment.prototype._detect = function () { diff --git a/src/vs/platform/accessibility/common/accessibilityService.ts b/src/vs/platform/accessibility/browser/accessibilityService.ts similarity index 96% rename from src/vs/platform/accessibility/common/accessibilityService.ts rename to src/vs/platform/accessibility/browser/accessibilityService.ts index 626e03511c..79f1ff6680 100644 --- a/src/vs/platform/accessibility/common/accessibilityService.ts +++ b/src/vs/platform/accessibility/browser/accessibilityService.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { alert } from 'vs/base/browser/ui/aria/aria'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { AccessibilitySupport, CONTEXT_ACCESSIBILITY_MODE_ENABLED, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -58,4 +59,8 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe this._accessibilitySupport = accessibilitySupport; this._onDidChangeScreenReaderOptimized.fire(); } + + alert(message: string): void { + alert(message); + } } diff --git a/src/vs/platform/accessibility/common/accessibility.ts b/src/vs/platform/accessibility/common/accessibility.ts index 5b15cd7d80..e31fe03d25 100644 --- a/src/vs/platform/accessibility/common/accessibility.ts +++ b/src/vs/platform/accessibility/common/accessibility.ts @@ -18,6 +18,7 @@ export interface IAccessibilityService { isScreenReaderOptimized(): boolean; getAccessibilitySupport(): AccessibilitySupport; setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void; + alert(message: string): void; } export const enum AccessibilitySupport { diff --git a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts index 96be5ba1f2..68283a1278 100644 --- a/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts +++ b/src/vs/platform/actions/browser/dropdownWithPrimaryActionViewItem.ts @@ -10,7 +10,8 @@ import { ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { IAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; -import { KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index d5ff09f91c..799938d702 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -285,7 +285,7 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { @IContextMenuService contextMenuService: IContextMenuService ) { const dropdownOptions = Object.assign({}, options ?? Object.create(null), { - menuAsChild: options?.menuAsChild ?? true, + menuAsChild: options?.menuAsChild ?? false, classNames: options?.classNames ?? (ThemeIcon.isThemeIcon(action.item.icon) ? ThemeIcon.asClassName(action.item.icon) : undefined), }); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index aaa8525d14..9aa70183ff 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -137,6 +137,7 @@ export class MenuId { static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TunnelContext = new MenuId('TunnelContext'); + static readonly TunnelPrivacy = new MenuId('TunnelPrivacy'); static readonly TunnelProtocol = new MenuId('TunnelProtocol'); static readonly TunnelPortInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); @@ -161,6 +162,7 @@ export class MenuId { static readonly NotebookCellBetween = new MenuId('NotebookCellBetween'); static readonly NotebookCellListTop = new MenuId('NotebookCellTop'); static readonly NotebookCellExecute = new MenuId('NotebookCellExecute'); + static readonly NotebookCellExecutePrimary = new MenuId('NotebookCellExecutePrimary'); static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle'); static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle'); static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle'); @@ -183,6 +185,7 @@ export class MenuId { static readonly AccountsContext = new MenuId('AccountsContext'); static readonly PanelTitle = new MenuId('PanelTitle'); static readonly TerminalInstanceContext = new MenuId('TerminalInstanceContext'); + static readonly TerminalEditorInstanceContext = new MenuId('TerminalEditorInstanceContext'); static readonly TerminalNewDropdownContext = new MenuId('TerminalNewDropdownContext'); static readonly TerminalTabContext = new MenuId('TerminalTabContext'); static readonly TerminalTabEmptyAreaContext = new MenuId('TerminalTabEmptyAreaContext'); @@ -213,11 +216,16 @@ export interface IMenu extends IDisposable { export const IMenuService = createDecorator('menuService'); +export interface IMenuCreateOptions { + emitEventsForSubmenuChanges?: boolean; + eventDebounceDelay?: number; +} + export interface IMenuService { readonly _serviceBrand: undefined; - createMenu(id: MenuId, contextKeyService: IContextKeyService, emitEventsForSubmenuChanges?: boolean): IMenu; + createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu; } export type ICommandsMap = Map; @@ -404,7 +412,7 @@ export class MenuItemAction implements IAction { tooltip: string; readonly class: string | undefined; readonly enabled: boolean; - readonly checked: boolean; + readonly checked?: boolean; readonly expanded: boolean = false; // {{SQL CARBON EDIT}} constructor( @@ -420,7 +428,7 @@ export class MenuItemAction implements IAction { : (typeof item.title === 'string' ? item.title : item.title.value); this.tooltip = (typeof item.tooltip === 'string' ? item.tooltip : item.tooltip?.value) ?? ''; this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); - this.checked = false; + this.checked = undefined; if (item.toggled) { const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as { diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 41596cd60e..1ca4e371d4 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -6,7 +6,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ILocalizedString, IMenu, IMenuActionOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { ILocalizedString, IMenu, IMenuActionOptions, IMenuCreateOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -26,8 +26,8 @@ export class MenuService implements IMenuService { * sub menu entries. That is more expensive and must be explicitly enabled with the * `emitEventsForSubmenuChanges` flag. */ - createMenu(id: MenuId, contextKeyService: IContextKeyService, emitEventsForSubmenuChanges: boolean = false): IMenu { - return new Menu(id, emitEventsForSubmenuChanges, this._commandService, contextKeyService, this); + createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu { + return new Menu(id, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, contextKeyService, this); } } @@ -46,7 +46,7 @@ class Menu implements IMenu { constructor( private readonly _id: MenuId, - private readonly _fireEventsForSubmenuChanges: boolean, + private readonly _options: Required, @ICommandService private readonly _commandService: ICommandService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IMenuService private readonly _menuService: IMenuService @@ -59,7 +59,7 @@ class Menu implements IMenu { const rebuildMenuSoon = new RunOnceScheduler(() => { this._build(); this._onDidChange.fire(this); - }, 50); + }, _options.eventDebounceDelay); this._disposables.add(rebuildMenuSoon); this._disposables.add(MenuRegistry.onDidChangeMenu(e => { if (e.has(_id)) { @@ -72,7 +72,7 @@ class Menu implements IMenu { // firing often and (2) menu are often leaked const contextKeyListener = this._disposables.add(new DisposableStore()); const startContextKeyListener = () => { - const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), 50); + const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), _options.eventDebounceDelay); contextKeyListener.add(fireChangeSoon); contextKeyListener.add(_contextKeyService.onDidChangeContext(e => { if (e.affectsSome(this._contextKeys)) { @@ -135,7 +135,7 @@ class Menu implements IMenu { Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); } - } else if (this._fireEventsForSubmenuChanges) { + } else if (this._options.emitEventsForSubmenuChanges) { // recursively collect context keys from submenus so that this // menu fires events when context key changes affect submenus MenuRegistry.getMenuItems(item.submenu).forEach(this._collectContextKeys, this); diff --git a/src/vs/platform/browser/contextScopedHistoryWidget.ts b/src/vs/platform/browser/contextScopedHistoryWidget.ts index 33ea613584..477d24d548 100644 --- a/src/vs/platform/browser/contextScopedHistoryWidget.ts +++ b/src/vs/platform/browser/contextScopedHistoryWidget.ts @@ -11,6 +11,7 @@ import { HistoryInputBox, IHistoryInputOptions } from 'vs/base/browser/ui/inputb import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr, IContextKey, IContextKeyService, IContextKeyServiceTarget, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { Context as SuggestContext } from 'vs/editor/contrib/suggest/suggest'; export const HistoryNavigationWidgetContext = 'historyNavigationWidget'; const HistoryNavigationForwardsEnablementContext = 'historyNavigationForwardsEnabled'; @@ -89,7 +90,11 @@ export class ContextScopedReplaceInput extends ReplaceInput { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'history.showPrevious', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationBackwardsEnablementContext, true)), + when: ContextKeyExpr.and( + ContextKeyExpr.has(HistoryNavigationWidgetContext), + ContextKeyExpr.equals(HistoryNavigationBackwardsEnablementContext, true), + SuggestContext.Visible.isEqualTo(false), + ), primary: KeyCode.UpArrow, secondary: [KeyMod.Alt | KeyCode.UpArrow], handler: (accessor) => { @@ -104,7 +109,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'history.showNext', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationForwardsEnablementContext, true)), + when: ContextKeyExpr.and( + ContextKeyExpr.has(HistoryNavigationWidgetContext), + ContextKeyExpr.equals(HistoryNavigationForwardsEnablementContext, true), + SuggestContext.Visible.isEqualTo(false), + ), primary: KeyCode.DownArrow, secondary: [KeyMod.Alt | KeyCode.DownArrow], handler: (accessor) => { diff --git a/src/vs/platform/browser/historyWidgetKeybindingHint.ts b/src/vs/platform/browser/historyWidgetKeybindingHint.ts new file mode 100644 index 0000000000..33f81973ae --- /dev/null +++ b/src/vs/platform/browser/historyWidgetKeybindingHint.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +export function showHistoryKeybindingHint(keybindingService: IKeybindingService): boolean { + return keybindingService.lookupKeybinding('history.showPrevious')?.getElectronAccelerator() === 'Up' && keybindingService.lookupKeybinding('history.showNext')?.getElectronAccelerator() === 'Down'; +} diff --git a/src/vs/platform/checksum/node/checksumService.ts b/src/vs/platform/checksum/node/checksumService.ts index 1de53dfe88..2be73664e8 100644 --- a/src/vs/platform/checksum/node/checksumService.ts +++ b/src/vs/platform/checksum/node/checksumService.ts @@ -15,10 +15,10 @@ export class ChecksumService implements IChecksumService { constructor(@IFileService private readonly fileService: IFileService) { } - checksum(resource: URI): Promise { - return new Promise(async (resolve, reject) => { + async checksum(resource: URI): Promise { + const stream = (await this.fileService.readFileStream(resource)).value; + return new Promise((resolve, reject) => { const hash = createHash('md5'); - const stream = (await this.fileService.readFileStream(resource)).value; listenStream(stream, { onData: data => hash.update(data.buffer), diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index b59773bc51..b27f64295f 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -21,11 +21,12 @@ import { Workspace } from 'vs/platform/workspace/common/workspace'; export class ConfigurationModel implements IConfigurationModel { private isFrozen: boolean = false; + private readonly overrideConfigurations = new Map(); constructor( - private _contents: any = {}, - private _keys: string[] = [], - private _overrides: IOverrides[] = [] + private readonly _contents: any = {}, + private readonly _keys: string[] = [], + private readonly _overrides: IOverrides[] = [] ) { } @@ -66,34 +67,12 @@ export class ConfigurationModel implements IConfigurationModel { } override(identifier: string): ConfigurationModel { - const overrideContents = this.getContentsForOverrideIdentifer(identifier); - - if (!overrideContents || typeof overrideContents !== 'object' || !Object.keys(overrideContents).length) { - // If there are no valid overrides, return self - return this; + let overrideConfigurationModel = this.overrideConfigurations.get(identifier); + if (!overrideConfigurationModel) { + overrideConfigurationModel = this.createOverrideConfigurationModel(identifier); + this.overrideConfigurations.set(identifier, overrideConfigurationModel); } - - let contents: any = {}; - for (const key of arrays.distinct([...Object.keys(this.contents), ...Object.keys(overrideContents)])) { - - let contentsForKey = this.contents[key]; - let overrideContentsForKey = overrideContents[key]; - - // If there are override contents for the key, clone and merge otherwise use base contents - if (overrideContentsForKey) { - // Clone and merge only if base contents and override contents are of type object otherwise just override - if (typeof contentsForKey === 'object' && typeof overrideContentsForKey === 'object') { - contentsForKey = objects.deepClone(contentsForKey); - this.mergeContents(contentsForKey, overrideContentsForKey); - } else { - contentsForKey = overrideContentsForKey; - } - } - - contents[key] = contentsForKey; - } - - return new ConfigurationModel(contents, this.keys, this.overrides); + return overrideConfigurationModel; } merge(...others: ConfigurationModel[]): ConfigurationModel { @@ -126,6 +105,37 @@ export class ConfigurationModel implements IConfigurationModel { return this; } + private createOverrideConfigurationModel(identifier: string): ConfigurationModel { + const overrideContents = this.getContentsForOverrideIdentifer(identifier); + + if (!overrideContents || typeof overrideContents !== 'object' || !Object.keys(overrideContents).length) { + // If there are no valid overrides, return self + return this; + } + + let contents: any = {}; + for (const key of arrays.distinct([...Object.keys(this.contents), ...Object.keys(overrideContents)])) { + + let contentsForKey = this.contents[key]; + let overrideContentsForKey = overrideContents[key]; + + // If there are override contents for the key, clone and merge otherwise use base contents + if (overrideContentsForKey) { + // Clone and merge only if base contents and override contents are of type object otherwise just override + if (typeof contentsForKey === 'object' && typeof overrideContentsForKey === 'object') { + contentsForKey = objects.deepClone(contentsForKey); + this.mergeContents(contentsForKey, overrideContentsForKey); + } else { + contentsForKey = overrideContentsForKey; + } + } + + contents[key] = contentsForKey; + } + + return new ConfigurationModel(contents, this.keys, this.overrides); + } + private mergeContents(source: any, target: any): void { for (const key of Object.keys(target)) { if (key in source) { diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index b35a4407f9..518228adc9 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { distinct } from 'vs/base/common/arrays'; import { IStringDictionary } from 'vs/base/common/collections'; import { Emitter, Event } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; @@ -37,6 +38,13 @@ export interface IConfigurationRegistry { */ deregisterConfigurations(configurations: IConfigurationNode[]): void; + /** + * update the configuration registry by + * - registering the configurations to add + * - dereigstering the configurations to remove + */ + updateConfigurations(configurations: { add: IConfigurationNode[], remove: IConfigurationNode[] }): void; + /** * Register multiple default configurations to the registry. */ @@ -210,12 +218,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { } public registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): void { - const properties: string[] = []; - configurations.forEach(configuration => { - properties.push(...this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo)); // fills in defaults - this.configurationContributors.push(configuration); - this.registerJSONConfiguration(configuration); - }); + const properties = this.doRegisterConfigurations(configurations, validate); contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema); this._onDidSchemaChange.fire(); @@ -223,32 +226,23 @@ class ConfigurationRegistry implements IConfigurationRegistry { } public deregisterConfigurations(configurations: IConfigurationNode[]): void { - const properties: string[] = []; - const deregisterConfiguration = (configuration: IConfigurationNode) => { - if (configuration.properties) { - for (const key in configuration.properties) { - properties.push(key); - delete this.configurationProperties[key]; - this.removeFromSchema(key, configuration.properties[key]); - } - } - if (configuration.allOf) { - configuration.allOf.forEach(node => deregisterConfiguration(node)); - } - }; - for (const configuration of configurations) { - deregisterConfiguration(configuration); - const index = this.configurationContributors.indexOf(configuration); - if (index !== -1) { - this.configurationContributors.splice(index, 1); - } - } + const properties = this.doDeregisterConfigurations(configurations); contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema); this._onDidSchemaChange.fire(); this._onDidUpdateConfiguration.fire(properties); } + public updateConfigurations({ add, remove }: { add: IConfigurationNode[], remove: IConfigurationNode[] }): void { + const properties = []; + properties.push(...this.doDeregisterConfigurations(remove)); + properties.push(...this.doRegisterConfigurations(add, false)); + + contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema); + this._onDidSchemaChange.fire(); + this._onDidUpdateConfiguration.fire(distinct(properties)); + } + public registerDefaultConfigurations(defaultConfigurations: IStringDictionary[]): void { const properties: string[] = []; const overrideIdentifiers: string[] = []; @@ -319,6 +313,40 @@ class ConfigurationRegistry implements IConfigurationRegistry { this.updateOverridePropertyPatternKey(); } + private doRegisterConfigurations(configurations: IConfigurationNode[], validate: boolean): string[] { + const properties: string[] = []; + configurations.forEach(configuration => { + properties.push(...this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo)); // fills in defaults + this.configurationContributors.push(configuration); + this.registerJSONConfiguration(configuration); + }); + return properties; + } + + private doDeregisterConfigurations(configurations: IConfigurationNode[]): string[] { + const properties: string[] = []; + const deregisterConfiguration = (configuration: IConfigurationNode) => { + if (configuration.properties) { + for (const key in configuration.properties) { + properties.push(key); + delete this.configurationProperties[key]; + this.removeFromSchema(key, configuration.properties[key]); + } + } + if (configuration.allOf) { + configuration.allOf.forEach(node => deregisterConfiguration(node)); + } + }; + for (const configuration of configurations) { + deregisterConfiguration(configuration); + const index = this.configurationContributors.indexOf(configuration); + if (index !== -1) { + this.configurationContributors.splice(index, 1); + } + } + return properties; + } + private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, extensionInfo?: IConfigurationExtensionInfo, scope: ConfigurationScope = ConfigurationScope.WINDOW): string[] { scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope; let propertyKeys: string[] = []; diff --git a/src/vs/platform/configuration/common/userConfigurationFileService.ts b/src/vs/platform/configuration/common/userConfigurationFileService.ts index 27dcd7b1f4..564e79041d 100644 --- a/src/vs/platform/configuration/common/userConfigurationFileService.ts +++ b/src/vs/platform/configuration/common/userConfigurationFileService.ts @@ -8,9 +8,8 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { JSONPath, parse, ParseError } from 'vs/base/common/json'; import { setProperty } from 'vs/base/common/jsonEdit'; import { Edit, FormattingOptions } from 'vs/base/common/jsonFormatter'; -import { URI } from 'vs/base/common/uri'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, IFileService, IWriteFileOptions } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -31,6 +30,7 @@ export interface IUserConfigurationFileService { readonly _serviceBrand: undefined; updateSettings(value: IJSONValue, formattingOptions: FormattingOptions): Promise; + write(value: VSBuffer, options?: IWriteFileOptions): Promise; } export class UserConfigurationFileService implements IUserConfigurationFileService { @@ -48,12 +48,12 @@ export class UserConfigurationFileService implements IUserConfigurationFileServi } async updateSettings(value: IJSONValue, formattingOptions: FormattingOptions): Promise { - return this.queue.queue(() => this.doWrite(this.environmentService.settingsResource, value, formattingOptions)); // queue up writes to prevent race conditions + return this.queue.queue(() => this.doWrite(value, formattingOptions)); // queue up writes to prevent race conditions } - private async doWrite(resource: URI, jsonValue: IJSONValue, formattingOptions: FormattingOptions): Promise { - this.logService.trace(`${UserConfigurationFileServiceId}#write`, resource.toString(), jsonValue); - const { value, mtime, etag } = await this.fileService.readFile(resource, { atomic: true }); + private async doWrite(jsonValue: IJSONValue, formattingOptions: FormattingOptions): Promise { + this.logService.trace(`${UserConfigurationFileServiceId}#write`, this.environmentService.settingsResource.toString(), jsonValue); + const { value, mtime, etag } = await this.fileService.readFile(this.environmentService.settingsResource, { atomic: true }); let content = value.toString(); const parseErrors: ParseError[] = []; @@ -66,7 +66,7 @@ export class UserConfigurationFileService implements IUserConfigurationFileServi if (edit) { content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length); try { - await this.fileService.writeFile(resource, VSBuffer.fromString(content), { etag, mtime }); + await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(content), { etag, mtime }); } catch (error) { if ((error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { throw new Error(UserConfigurationErrorCode.ERROR_FILE_MODIFIED_SINCE); @@ -75,6 +75,13 @@ export class UserConfigurationFileService implements IUserConfigurationFileServi } } + async write(content: VSBuffer, options?: IWriteFileOptions): Promise { + // queue up writes to prevent race conditions + return this.queue.queue(async () => { + await this.fileService.writeFile(this.environmentService.settingsResource, content, options); + }); + } + private getEdits({ value, path }: IJSONValue, modelContent: string, formattingOptions: FormattingOptions): Edit[] { if (path.length) { return setProperty(modelContent, path, value, formattingOptions); diff --git a/src/vs/platform/configuration/test/common/configurationService.test.ts b/src/vs/platform/configuration/test/common/configurationService.test.ts index c3e90a9b3e..44cee740d5 100644 --- a/src/vs/platform/configuration/test/common/configurationService.test.ts +++ b/src/vs/platform/configuration/test/common/configurationService.test.ts @@ -89,12 +89,12 @@ suite('ConfigurationService', () => { test('trigger configuration change event when file does not exist', async () => { const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); await testObject.initialize(); - return new Promise(async (c) => { + return new Promise((c, e) => { disposables.add(Event.filter(testObject.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(() => { assert.strictEqual(testObject.getValue('foo'), 'bar'); c(); })); - await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }')); + fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }')).catch(e); }); }); diff --git a/src/vs/platform/configuration/test/common/testConfigurationService.ts b/src/vs/platform/configuration/test/common/testConfigurationService.ts index 57ced93974..b8d1256415 100644 --- a/src/vs/platform/configuration/test/common/testConfigurationService.ts +++ b/src/vs/platform/configuration/test/common/testConfigurationService.ts @@ -34,7 +34,7 @@ export class TestConfigurationService implements IConfigurationService { } configuration = configuration ? configuration : this.configuration; if (arg1 && typeof arg1 === 'string') { - return getConfigurationValue(configuration, arg1); + return configuration[arg1] ?? getConfigurationValue(configuration, arg1); } return configuration; } diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 4aa04e6e92..e07bcc0278 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -9,18 +9,18 @@ import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; let _userAgent = userAgent || ''; -const STATIC_VALUES = new Map(); -STATIC_VALUES.set('false', false); -STATIC_VALUES.set('true', true); -STATIC_VALUES.set('isMac', isMacintosh); -STATIC_VALUES.set('isLinux', isLinux); -STATIC_VALUES.set('isWindows', isWindows); -STATIC_VALUES.set('isWeb', isWeb); -STATIC_VALUES.set('isMacNative', isMacintosh && !isWeb); -STATIC_VALUES.set('isEdge', _userAgent.indexOf('Edg/') >= 0); -STATIC_VALUES.set('isFirefox', _userAgent.indexOf('Firefox') >= 0); -STATIC_VALUES.set('isChrome', _userAgent.indexOf('Chrome') >= 0); -STATIC_VALUES.set('isSafari', _userAgent.indexOf('Safari') >= 0); +const CONSTANT_VALUES = new Map(); +CONSTANT_VALUES.set('false', false); +CONSTANT_VALUES.set('true', true); +CONSTANT_VALUES.set('isMac', isMacintosh); +CONSTANT_VALUES.set('isLinux', isLinux); +CONSTANT_VALUES.set('isWindows', isWindows); +CONSTANT_VALUES.set('isWeb', isWeb); +CONSTANT_VALUES.set('isMacNative', isMacintosh && !isWeb); +CONSTANT_VALUES.set('isEdge', _userAgent.indexOf('Edg/') >= 0); +CONSTANT_VALUES.set('isFirefox', _userAgent.indexOf('Firefox') >= 0); +CONSTANT_VALUES.set('isChrome', _userAgent.indexOf('Chrome') >= 0); +CONSTANT_VALUES.set('isSafari', _userAgent.indexOf('Safari') >= 0); const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -59,6 +59,7 @@ export interface IContextKeyExprMapper { export interface IContextKeyExpression { cmp(other: ContextKeyExpression): number; equals(other: ContextKeyExpression): boolean; + substituteConstants(): ContextKeyExpression | undefined; evaluate(context: IContext): boolean; serialize(): string; keys(): string[]; @@ -80,50 +81,45 @@ export abstract class ContextKeyExpr { public static false(): ContextKeyExpression { return ContextKeyFalseExpr.INSTANCE; } - public static true(): ContextKeyExpression { return ContextKeyTrueExpr.INSTANCE; } - public static has(key: string): ContextKeyExpression { return ContextKeyDefinedExpr.create(key); } - public static equals(key: string, value: any): ContextKeyExpression { return ContextKeyEqualsExpr.create(key, value); } - public static notEquals(key: string, value: any): ContextKeyExpression { return ContextKeyNotEqualsExpr.create(key, value); } - public static regex(key: string, value: RegExp): ContextKeyExpression { return ContextKeyRegexExpr.create(key, value); } - public static in(key: string, value: string): ContextKeyExpression { return ContextKeyInExpr.create(key, value); } - public static not(key: string): ContextKeyExpression { return ContextKeyNotExpr.create(key); } - public static and(...expr: Array): ContextKeyExpression | undefined { - return ContextKeyAndExpr.create(expr); + return ContextKeyAndExpr.create(expr, null); } - public static or(...expr: Array): ContextKeyExpression | undefined { - return ContextKeyOrExpr.create(expr); + return ContextKeyOrExpr.create(expr, null, true); } - - public static greater(key: string, value: any): ContextKeyExpression { + public static greater(key: string, value: number): ContextKeyExpression { return ContextKeyGreaterExpr.create(key, value); } - - public static less(key: string, value: any): ContextKeyExpression { + public static greaterEquals(key: string, value: number): ContextKeyExpression { + return ContextKeyGreaterEqualsExpr.create(key, value); + } + public static smaller(key: string, value: number): ContextKeyExpression { return ContextKeySmallerExpr.create(key, value); } + public static smallerEquals(key: string, value: number): ContextKeyExpression { + return ContextKeySmallerEqualsExpr.create(key, value); + } public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpression | undefined { if (!serialized) { @@ -135,12 +131,12 @@ export abstract class ContextKeyExpr { private static _deserializeOrExpression(serialized: string, strict: boolean): ContextKeyExpression | undefined { let pieces = serialized.split('||'); - return ContextKeyOrExpr.create(pieces.map(p => this._deserializeAndExpression(p, strict))); + return ContextKeyOrExpr.create(pieces.map(p => this._deserializeAndExpression(p, strict)), null, true); } private static _deserializeAndExpression(serialized: string, strict: boolean): ContextKeyExpression | undefined { let pieces = serialized.split('&&'); - return ContextKeyAndExpr.create(pieces.map(p => this._deserializeOne(p, strict))); + return ContextKeyAndExpr.create(pieces.map(p => this._deserializeOne(p, strict)), null); } private static _deserializeOne(serializedOne: string, strict: boolean): ContextKeyExpression { @@ -249,6 +245,18 @@ export abstract class ContextKeyExpr { } } +export function expressionsAreEqualWithConstantSubstitution(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean { + const aExpr = a ? a.substituteConstants() : undefined; + const bExpr = b ? b.substituteConstants() : undefined; + if (!aExpr && !bExpr) { + return true; + } + if (!aExpr || !bExpr) { + return false; + } + return aExpr.equals(bExpr); +} + function cmp(a: ContextKeyExpression, b: ContextKeyExpression): number { return a.cmp(b); } @@ -269,6 +277,10 @@ export class ContextKeyFalseExpr implements IContextKeyExpression { return (other.type === this.type); } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { return false; } @@ -306,6 +318,10 @@ export class ContextKeyTrueExpr implements IContextKeyExpression { return (other.type === this.type); } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { return true; } @@ -328,17 +344,20 @@ export class ContextKeyTrueExpr implements IContextKeyExpression { } export class ContextKeyDefinedExpr implements IContextKeyExpression { - public static create(key: string): ContextKeyExpression { - const staticValue = STATIC_VALUES.get(key); - if (typeof staticValue === 'boolean') { - return staticValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE; + public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression { + const constantValue = CONSTANT_VALUES.get(key); + if (typeof constantValue === 'boolean') { + return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE; } - return new ContextKeyDefinedExpr(key); + return new ContextKeyDefinedExpr(key, negated); } public readonly type = ContextKeyExprType.Defined; - protected constructor(readonly key: string) { + protected constructor( + readonly key: string, + private negated: ContextKeyExpression | null + ) { } public cmp(other: ContextKeyExpression): number { @@ -355,6 +374,14 @@ export class ContextKeyDefinedExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + const constantValue = CONSTANT_VALUES.get(this.key); + if (typeof constantValue === 'boolean') { + return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE; + } + return this; + } + public evaluate(context: IContext): boolean { return (!!context.getValue(this.key)); } @@ -372,27 +399,34 @@ export class ContextKeyDefinedExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeyNotExpr.create(this.key); + if (!this.negated) { + this.negated = ContextKeyNotExpr.create(this.key, this); + } + return this.negated; } } export class ContextKeyEqualsExpr implements IContextKeyExpression { - public static create(key: string, value: any): ContextKeyExpression { + public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { if (typeof value === 'boolean') { - return (value ? ContextKeyDefinedExpr.create(key) : ContextKeyNotExpr.create(key)); + return (value ? ContextKeyDefinedExpr.create(key, negated) : ContextKeyNotExpr.create(key, negated)); } - const staticValue = STATIC_VALUES.get(key); - if (typeof staticValue === 'boolean') { - const trueValue = staticValue ? 'true' : 'false'; + const constantValue = CONSTANT_VALUES.get(key); + if (typeof constantValue === 'boolean') { + const trueValue = constantValue ? 'true' : 'false'; return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE); } - return new ContextKeyEqualsExpr(key, value); + return new ContextKeyEqualsExpr(key, value, negated); } public readonly type = ContextKeyExprType.Equals; - private constructor(private readonly key: string, private readonly value: any) { + private constructor( + private readonly key: string, + private readonly value: any, + private negated: ContextKeyExpression | null + ) { } public cmp(other: ContextKeyExpression): number { @@ -409,6 +443,15 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + const constantValue = CONSTANT_VALUES.get(this.key); + if (typeof constantValue === 'boolean') { + const trueValue = constantValue ? 'true' : 'false'; + return (this.value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE); + } + return this; + } + public evaluate(context: IContext): boolean { // Intentional == // eslint-disable-next-line eqeqeq @@ -428,7 +471,10 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeyNotEqualsExpr.create(this.key, this.value); + if (!this.negated) { + this.negated = ContextKeyNotEqualsExpr.create(this.key, this.value, this); + } + return this.negated; } } @@ -439,8 +485,12 @@ export class ContextKeyInExpr implements IContextKeyExpression { } public readonly type = ContextKeyExprType.In; + private negated: ContextKeyExpression | null = null; - private constructor(private readonly key: string, private readonly valueKey: string) { + private constructor( + private readonly key: string, + private readonly valueKey: string, + ) { } public cmp(other: ContextKeyExpression): number { @@ -457,6 +507,10 @@ export class ContextKeyInExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { const source = context.getValue(this.valueKey); @@ -485,7 +539,10 @@ export class ContextKeyInExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeyNotInExpr.create(this); + if (!this.negated) { + this.negated = ContextKeyNotInExpr.create(this); + } + return this.negated; } } @@ -515,6 +572,10 @@ export class ContextKeyNotInExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { return !this._actual.evaluate(context); } @@ -538,24 +599,28 @@ export class ContextKeyNotInExpr implements IContextKeyExpression { export class ContextKeyNotEqualsExpr implements IContextKeyExpression { - public static create(key: string, value: any): ContextKeyExpression { + public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { if (typeof value === 'boolean') { if (value) { - return ContextKeyNotExpr.create(key); + return ContextKeyNotExpr.create(key, negated); } - return ContextKeyDefinedExpr.create(key); + return ContextKeyDefinedExpr.create(key, negated); } - const staticValue = STATIC_VALUES.get(key); - if (typeof staticValue === 'boolean') { - const falseValue = staticValue ? 'true' : 'false'; + const constantValue = CONSTANT_VALUES.get(key); + if (typeof constantValue === 'boolean') { + const falseValue = constantValue ? 'true' : 'false'; return (value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); } - return new ContextKeyNotEqualsExpr(key, value); + return new ContextKeyNotEqualsExpr(key, value, negated); } public readonly type = ContextKeyExprType.NotEquals; - private constructor(private readonly key: string, private readonly value: any) { + private constructor( + private readonly key: string, + private readonly value: any, + private negated: ContextKeyExpression | null + ) { } public cmp(other: ContextKeyExpression): number { @@ -572,6 +637,15 @@ export class ContextKeyNotEqualsExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + const constantValue = CONSTANT_VALUES.get(this.key); + if (typeof constantValue === 'boolean') { + const falseValue = constantValue ? 'true' : 'false'; + return (this.value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); + } + return this; + } + public evaluate(context: IContext): boolean { // Intentional != // eslint-disable-next-line eqeqeq @@ -591,23 +665,29 @@ export class ContextKeyNotEqualsExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeyEqualsExpr.create(this.key, this.value); + if (!this.negated) { + this.negated = ContextKeyEqualsExpr.create(this.key, this.value, this); + } + return this.negated; } } export class ContextKeyNotExpr implements IContextKeyExpression { - public static create(key: string): ContextKeyExpression { - const staticValue = STATIC_VALUES.get(key); - if (typeof staticValue === 'boolean') { - return (staticValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); + public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression { + const constantValue = CONSTANT_VALUES.get(key); + if (typeof constantValue === 'boolean') { + return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); } - return new ContextKeyNotExpr(key); + return new ContextKeyNotExpr(key, negated); } public readonly type = ContextKeyExprType.Not; - private constructor(private readonly key: string) { + private constructor( + private readonly key: string, + private negated: ContextKeyExpression | null + ) { } public cmp(other: ContextKeyExpression): number { @@ -624,6 +704,14 @@ export class ContextKeyNotExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + const constantValue = CONSTANT_VALUES.get(this.key); + if (typeof constantValue === 'boolean') { + return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); + } + return this; + } + public evaluate(context: IContext): boolean { return (!context.getValue(this.key)); } @@ -641,21 +729,38 @@ export class ContextKeyNotExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeyDefinedExpr.create(this.key); + if (!this.negated) { + this.negated = ContextKeyDefinedExpr.create(this.key, this); + } + return this.negated; } } +function withFloatOrStr(value: any, callback: (value: number | string) => T): T | ContextKeyFalseExpr { + if (typeof value === 'string') { + const n = parseFloat(value); + if (!isNaN(n)) { + value = n; + } + } + if (typeof value === 'string' || typeof value === 'number') { + return callback(value); + } + return ContextKeyFalseExpr.INSTANCE; +} + export class ContextKeyGreaterExpr implements IContextKeyExpression { - public static create(key: string, value: any): ContextKeyExpression { - return new ContextKeyGreaterExpr(key, value); + public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { + return withFloatOrStr(_value, (value) => new ContextKeyGreaterExpr(key, value, negated)); } public readonly type = ContextKeyExprType.Greater; private constructor( private readonly key: string, - private readonly value: any + private readonly value: number | string, + private negated: ContextKeyExpression | null ) { } public cmp(other: ContextKeyExpression): number { @@ -672,8 +777,15 @@ export class ContextKeyGreaterExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { - return (parseFloat(context.getValue(this.key)) > parseFloat(this.value)); + if (typeof this.value === 'string') { + return false; + } + return (parseFloat(context.getValue(this.key)) > this.value); } public serialize(): string { @@ -689,21 +801,25 @@ export class ContextKeyGreaterExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeySmallerEqualsExpr.create(this.key, this.value); + if (!this.negated) { + this.negated = ContextKeySmallerEqualsExpr.create(this.key, this.value, this); + } + return this.negated; } } export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression { - public static create(key: string, value: any): ContextKeyExpression { - return new ContextKeyGreaterEqualsExpr(key, value); + public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { + return withFloatOrStr(_value, (value) => new ContextKeyGreaterEqualsExpr(key, value, negated)); } public readonly type = ContextKeyExprType.GreaterEquals; private constructor( private readonly key: string, - private readonly value: any + private readonly value: number | string, + private negated: ContextKeyExpression | null ) { } public cmp(other: ContextKeyExpression): number { @@ -720,8 +836,15 @@ export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { - return (parseFloat(context.getValue(this.key)) >= parseFloat(this.value)); + if (typeof this.value === 'string') { + return false; + } + return (parseFloat(context.getValue(this.key)) >= this.value); } public serialize(): string { @@ -737,21 +860,25 @@ export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeySmallerExpr.create(this.key, this.value); + if (!this.negated) { + this.negated = ContextKeySmallerExpr.create(this.key, this.value, this); + } + return this.negated; } } export class ContextKeySmallerExpr implements IContextKeyExpression { - public static create(key: string, value: any): ContextKeyExpression { - return new ContextKeySmallerExpr(key, value); + public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { + return withFloatOrStr(_value, (value) => new ContextKeySmallerExpr(key, value, negated)); } public readonly type = ContextKeyExprType.Smaller; private constructor( private readonly key: string, - private readonly value: any + private readonly value: number | string, + private negated: ContextKeyExpression | null ) { } @@ -769,8 +896,15 @@ export class ContextKeySmallerExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { - return (parseFloat(context.getValue(this.key)) < parseFloat(this.value)); + if (typeof this.value === 'string') { + return false; + } + return (parseFloat(context.getValue(this.key)) < this.value); } public serialize(): string { @@ -786,21 +920,25 @@ export class ContextKeySmallerExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeyGreaterEqualsExpr.create(this.key, this.value); + if (!this.negated) { + this.negated = ContextKeyGreaterEqualsExpr.create(this.key, this.value, this); + } + return this.negated; } } export class ContextKeySmallerEqualsExpr implements IContextKeyExpression { - public static create(key: string, value: any): ContextKeyExpression { - return new ContextKeySmallerEqualsExpr(key, value); + public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression { + return withFloatOrStr(_value, (value) => new ContextKeySmallerEqualsExpr(key, value, negated)); } public readonly type = ContextKeyExprType.SmallerEquals; private constructor( private readonly key: string, - private readonly value: any + private readonly value: number | string, + private negated: ContextKeyExpression | null ) { } @@ -818,8 +956,15 @@ export class ContextKeySmallerEqualsExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { - return (parseFloat(context.getValue(this.key)) <= parseFloat(this.value)); + if (typeof this.value === 'string') { + return false; + } + return (parseFloat(context.getValue(this.key)) <= this.value); } public serialize(): string { @@ -835,7 +980,10 @@ export class ContextKeySmallerEqualsExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeyGreaterExpr.create(this.key, this.value); + if (!this.negated) { + this.negated = ContextKeyGreaterExpr.create(this.key, this.value, this); + } + return this.negated; } } @@ -846,8 +994,12 @@ export class ContextKeyRegexExpr implements IContextKeyExpression { } public readonly type = ContextKeyExprType.Regex; + private negated: ContextKeyExpression | null = null; - private constructor(private readonly key: string, private readonly regexp: RegExp | null) { + private constructor( + private readonly key: string, + private readonly regexp: RegExp | null + ) { // } @@ -881,6 +1033,10 @@ export class ContextKeyRegexExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { let value = context.getValue(this.key); return this.regexp ? this.regexp.test(value) : false; @@ -902,7 +1058,10 @@ export class ContextKeyRegexExpr implements IContextKeyExpression { } public negate(): ContextKeyExpression { - return ContextKeyNotRegexExpr.create(this); + if (!this.negated) { + this.negated = ContextKeyNotRegexExpr.create(this); + } + return this.negated; } } @@ -932,6 +1091,10 @@ export class ContextKeyNotRegexExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + return this; + } + public evaluate(context: IContext): boolean { return !this._actual.evaluate(context); } @@ -953,15 +1116,50 @@ export class ContextKeyNotRegexExpr implements IContextKeyExpression { } } -export class ContextKeyAndExpr implements IContextKeyExpression { +/** + * @returns the same instance if nothing changed. + */ +function eliminateConstantsInArray(arr: ContextKeyExpression[]): (ContextKeyExpression | undefined)[] { + // Allocate array only if there is a difference + let newArr: (ContextKeyExpression | undefined)[] | null = null; + for (let i = 0, len = arr.length; i < len; i++) { + const newExpr = arr[i].substituteConstants(); - public static create(_expr: ReadonlyArray): ContextKeyExpression | undefined { - return ContextKeyAndExpr._normalizeArr(_expr); + if (arr[i] !== newExpr) { + // something has changed! + + // allocate array on first difference + if (newArr === null) { + newArr = []; + for (let j = 0; j < i; j++) { + newArr[j] = arr[j]; + } + } + } + + if (newArr !== null) { + newArr[i] = newExpr; + } + } + + if (newArr === null) { + return arr; + } + return newArr; +} + +class ContextKeyAndExpr implements IContextKeyExpression { + + public static create(_expr: ReadonlyArray, negated: ContextKeyExpression | null): ContextKeyExpression | undefined { + return ContextKeyAndExpr._normalizeArr(_expr, negated); } public readonly type = ContextKeyExprType.And; - private constructor(public readonly expr: ContextKeyExpression[]) { + private constructor( + public readonly expr: ContextKeyExpression[], + private negated: ContextKeyExpression | null + ) { } public cmp(other: ContextKeyExpression): number { @@ -998,6 +1196,15 @@ export class ContextKeyAndExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + const exprArr = eliminateConstantsInArray(this.expr); + if (exprArr === this.expr) { + // no change + return this; + } + return ContextKeyAndExpr.create(exprArr, this.negated); + } + public evaluate(context: IContext): boolean { for (let i = 0, len = this.expr.length; i < len; i++) { if (!this.expr[i].evaluate(context)) { @@ -1007,7 +1214,7 @@ export class ContextKeyAndExpr implements IContextKeyExpression { return true; } - private static _normalizeArr(arr: ReadonlyArray): ContextKeyExpression | undefined { + private static _normalizeArr(arr: ReadonlyArray, negated: ContextKeyExpression | null): ContextKeyExpression | undefined { const expr: ContextKeyExpression[] = []; let hasTrue = false; @@ -1049,6 +1256,18 @@ export class ContextKeyAndExpr implements IContextKeyExpression { expr.sort(cmp); + // eliminate duplicate terms + for (let i = 1; i < expr.length; i++) { + if (expr[i - 1].equals(expr[i])) { + expr.splice(i, 1); + i--; + } + } + + if (expr.length === 1) { + return expr[0]; + } + // We must distribute any OR expression because we don't support parens // OR extensions will be at the end (due to sorting rules) while (expr.length > 1) { @@ -1062,9 +1281,13 @@ export class ContextKeyAndExpr implements IContextKeyExpression { // pop the second to last element const secondToLastElement = expr.pop()!; + const isFinished = (expr.length === 0); + // distribute `lastElement` over `secondToLastElement` const resultElement = ContextKeyOrExpr.create( - lastElement.expr.map(el => ContextKeyAndExpr.create([el, secondToLastElement])) + lastElement.expr.map(el => ContextKeyAndExpr.create([el, secondToLastElement], null)), + null, + isFinished ); if (resultElement) { @@ -1077,7 +1300,7 @@ export class ContextKeyAndExpr implements IContextKeyExpression { return expr[0]; } - return new ContextKeyAndExpr(expr); + return new ContextKeyAndExpr(expr, negated); } public serialize(): string { @@ -1093,36 +1316,33 @@ export class ContextKeyAndExpr implements IContextKeyExpression { } public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { - return new ContextKeyAndExpr(this.expr.map(expr => expr.map(mapFnc))); + return new ContextKeyAndExpr(this.expr.map(expr => expr.map(mapFnc)), null); } public negate(): ContextKeyExpression { - let result: ContextKeyExpression[] = []; - for (let expr of this.expr) { - result.push(expr.negate()); + if (!this.negated) { + const result: ContextKeyExpression[] = []; + for (let expr of this.expr) { + result.push(expr.negate()); + } + this.negated = ContextKeyOrExpr.create(result, this, true)!; } - return ContextKeyOrExpr.create(result)!; + return this.negated; } } -export class ContextKeyOrExpr implements IContextKeyExpression { +class ContextKeyOrExpr implements IContextKeyExpression { - public static create(_expr: ReadonlyArray): ContextKeyExpression | undefined { - const expr = ContextKeyOrExpr._normalizeArr(_expr); - if (expr.length === 0) { - return undefined; - } - - if (expr.length === 1) { - return expr[0]; - } - - return new ContextKeyOrExpr(expr); + public static create(_expr: ReadonlyArray, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined { + return ContextKeyOrExpr._normalizeArr(_expr, negated, extraRedundantCheck); } public readonly type = ContextKeyExprType.Or; - private constructor(public readonly expr: ContextKeyExpression[]) { + private constructor( + public readonly expr: ContextKeyExpression[], + private negated: ContextKeyExpression | null + ) { } public cmp(other: ContextKeyExpression): number { @@ -1159,6 +1379,15 @@ export class ContextKeyOrExpr implements IContextKeyExpression { return false; } + public substituteConstants(): ContextKeyExpression | undefined { + const exprArr = eliminateConstantsInArray(this.expr); + if (exprArr === this.expr) { + // no change + return this; + } + return ContextKeyOrExpr.create(exprArr, this.negated, false); + } + public evaluate(context: IContext): boolean { for (let i = 0, len = this.expr.length; i < len; i++) { if (this.expr[i].evaluate(context)) { @@ -1168,7 +1397,7 @@ export class ContextKeyOrExpr implements IContextKeyExpression { return false; } - private static _normalizeArr(arr: ReadonlyArray): ContextKeyExpression[] { + private static _normalizeArr(arr: ReadonlyArray, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined { let expr: ContextKeyExpression[] = []; let hasFalse = false; @@ -1187,7 +1416,7 @@ export class ContextKeyOrExpr implements IContextKeyExpression { if (e.type === ContextKeyExprType.True) { // anything || true ==> true - return [ContextKeyTrueExpr.INSTANCE]; + return ContextKeyTrueExpr.INSTANCE; } if (e.type === ContextKeyExprType.Or) { @@ -1199,13 +1428,49 @@ export class ContextKeyOrExpr implements IContextKeyExpression { } if (expr.length === 0 && hasFalse) { - return [ContextKeyFalseExpr.INSTANCE]; + return ContextKeyFalseExpr.INSTANCE; } expr.sort(cmp); } - return expr; + if (expr.length === 0) { + return undefined; + } + + if (expr.length === 1) { + return expr[0]; + } + + // eliminate duplicate terms + for (let i = 1; i < expr.length; i++) { + if (expr[i - 1].equals(expr[i])) { + expr.splice(i, 1); + i--; + } + } + + if (expr.length === 1) { + return expr[0]; + } + + // eliminate redundant terms + if (extraRedundantCheck) { + for (let i = 0; i < expr.length; i++) { + for (let j = i + 1; j < expr.length; j++) { + if (implies(expr[i], expr[j])) { + expr.splice(j, 1); + j--; + } + } + } + + if (expr.length === 1) { + return expr[0]; + } + } + + return new ContextKeyOrExpr(expr, negated); } public serialize(): string { @@ -1221,38 +1486,36 @@ export class ContextKeyOrExpr implements IContextKeyExpression { } public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { - return new ContextKeyOrExpr(this.expr.map(expr => expr.map(mapFnc))); + return new ContextKeyOrExpr(this.expr.map(expr => expr.map(mapFnc)), null); } public negate(): ContextKeyExpression { - let result: ContextKeyExpression[] = []; - for (let expr of this.expr) { - result.push(expr.negate()); - } - - const terminals = (node: ContextKeyExpression) => { - if (node.type === ContextKeyExprType.Or) { - return node.expr; + if (!this.negated) { + let result: ContextKeyExpression[] = []; + for (let expr of this.expr) { + result.push(expr.negate()); } - return [node]; - }; - // We don't support parens, so here we distribute the AND over the OR terminals - // We always take the first 2 AND pairs and distribute them - while (result.length > 1) { - const LEFT = result.shift()!; - const RIGHT = result.shift()!; + // We don't support parens, so here we distribute the AND over the OR terminals + // We always take the first 2 AND pairs and distribute them + while (result.length > 1) { + const LEFT = result.shift()!; + const RIGHT = result.shift()!; - const all: ContextKeyExpression[] = []; - for (const left of terminals(LEFT)) { - for (const right of terminals(RIGHT)) { - all.push(ContextKeyExpr.and(left, right)!); + const all: ContextKeyExpression[] = []; + for (const left of getTerminals(LEFT)) { + for (const right of getTerminals(RIGHT)) { + all.push(ContextKeyAndExpr.create([left, right], null)!); + } } - } - result.unshift(ContextKeyExpr.or(...all)!); - } - return result[0]; + const isFinished = (result.length === 0); + result.unshift(ContextKeyOrExpr.create(all, null, isFinished)!); + } + + this.negated = result[0]; + } + return this.negated; } } @@ -1273,7 +1536,7 @@ export class RawContextKey extends ContextKeyDefinedExpr { private readonly _defaultValue: T | undefined; constructor(key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string, description: string }) { - super(key); + super(key, null); this._defaultValue = defaultValue; // collect all context keys into a central place @@ -1293,15 +1556,15 @@ export class RawContextKey extends ContextKeyDefinedExpr { } public toNegated(): ContextKeyExpression { - return ContextKeyExpr.not(this.key); + return this.negate(); } public isEqualTo(value: any): ContextKeyExpression { - return ContextKeyExpr.equals(this.key, value); + return ContextKeyEqualsExpr.create(this.key, value); } public notEqualsTo(value: any): ContextKeyExpression { - return ContextKeyExpr.notEquals(this.key, value); + return ContextKeyNotEqualsExpr.create(this.key, value); } } @@ -1378,3 +1641,42 @@ function cmp2(key1: string, value1: any, key2: string, value2: any): number { } return 0; } + +/** + * Returns true if it is provable `p` implies `q`. + */ +export function implies(p: ContextKeyExpression, q: ContextKeyExpression): boolean { + + if (q.type === ContextKeyExprType.And && (p.type !== ContextKeyExprType.Or && p.type !== ContextKeyExprType.And)) { + // covers the case: A implies A && B + for (const qTerm of q.expr) { + if (p.equals(qTerm)) { + return true; + } + } + } + + const notP = p.negate(); + const expr = getTerminals(notP).concat(getTerminals(q)); + expr.sort(cmp); + + for (let i = 0; i < expr.length; i++) { + const a = expr[i]; + const notA = a.negate(); + for (let j = i + 1; j < expr.length; j++) { + const b = expr[j]; + if (notA.equals(b)) { + return true; + } + } + } + + return false; +} + +function getTerminals(node: ContextKeyExpression) { + if (node.type === ContextKeyExprType.Or) { + return node.expr; + } + return [node]; +} diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index f35db96aeb..3be36dd5f2 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression, implies } from 'vs/platform/contextkey/common/contextkey'; function createContext(ctx: any) { return { @@ -45,6 +45,19 @@ suite('ContextKeyExpr', () => { assert(a.equals(b), 'expressions should be equal'); }); + test('issue #134942: Equals in comparator expressions', () => { + function testEquals(expr: ContextKeyExpression | undefined, str: string): void { + const deserialized = ContextKeyExpr.deserialize(str); + assert.ok(expr); + assert.ok(deserialized); + assert.strictEqual(expr.equals(deserialized), true, str); + } + testEquals(ContextKeyExpr.greater('value', 0), 'value > 0'); + testEquals(ContextKeyExpr.greaterEquals('value', 0), 'value >= 0'); + testEquals(ContextKeyExpr.smaller('value', 0), 'value < 0'); + testEquals(ContextKeyExpr.smallerEquals('value', 0), 'value <= 0'); + }); + test('normalize', () => { let key1IsTrue = ContextKeyExpr.equals('key1', true); let key1IsNotFalse = ContextKeyExpr.notEquals('key1', false); @@ -191,6 +204,47 @@ suite('ContextKeyExpr', () => { assert.strictEqual(actual!.equals(expected!), true); }); + test('issue #129625: Removes duplicated terms in OR expressions', () => { + const expr = ContextKeyExpr.or( + ContextKeyExpr.has('A'), + ContextKeyExpr.has('B'), + ContextKeyExpr.has('A') + )!; + assert.strictEqual(expr.serialize(), 'A || B'); + }); + + test('issue #129625: Removes duplicated terms in AND expressions', () => { + const expr = ContextKeyExpr.and( + ContextKeyExpr.has('A'), + ContextKeyExpr.has('B'), + ContextKeyExpr.has('A') + )!; + assert.strictEqual(expr.serialize(), 'A && B'); + }); + + test('issue #129625: Remove duplicated terms when negating', () => { + const expr = ContextKeyExpr.and( + ContextKeyExpr.has('A'), + ContextKeyExpr.or( + ContextKeyExpr.has('B1'), + ContextKeyExpr.has('B2'), + ) + )!; + assert.strictEqual(expr.serialize(), 'A && B1 || A && B2'); + assert.strictEqual(expr.negate()!.serialize(), '!A || !B1 && !B2'); + assert.strictEqual(expr.negate()!.negate()!.serialize(), 'A && B1 || A && B2'); + assert.strictEqual(expr.negate()!.negate()!.negate()!.serialize(), '!A || !B1 && !B2'); + }); + + test('issue #129625: remove redundant terms in OR expressions', () => { + function strImplies(p0: string, q0: string): boolean { + const p = ContextKeyExpr.deserialize(p0)!; + const q = ContextKeyExpr.deserialize(q0)!; + return implies(p, q); + } + assert.strictEqual(strImplies('a', 'a && b'), true); + }); + test('Greater, GreaterEquals, Smaller, SmallerEquals evaluate', () => { function checkEvaluate(expr: string, ctx: any, expected: any): void { const _expr = ContextKeyExpr.deserialize(expr)!; diff --git a/src/vs/platform/debug/common/extensionHostDebug.ts b/src/vs/platform/debug/common/extensionHostDebug.ts index 6849cdd54b..2a1978b135 100644 --- a/src/vs/platform/debug/common/extensionHostDebug.ts +++ b/src/vs/platform/debug/common/extensionHostDebug.ts @@ -54,5 +54,5 @@ export interface IExtensionHostDebugService { terminateSession(sessionId: string, subId?: string): void; readonly onTerminateSession: Event; - openExtensionDevelopmentHostWindow(args: string[], env: INullableProcessEnvironment | undefined, debugRenderer: boolean): Promise; + openExtensionDevelopmentHostWindow(args: string[], debugRenderer: boolean): Promise; } diff --git a/src/vs/platform/debug/common/extensionHostDebugIpc.ts b/src/vs/platform/debug/common/extensionHostDebugIpc.ts index e8e7b5aad1..fb964c6648 100644 --- a/src/vs/platform/debug/common/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/common/extensionHostDebugIpc.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IAttachSessionEvent, ICloseSessionEvent, IExtensionHostDebugService, INullableProcessEnvironment, IOpenExtensionWindowResult, IReloadSessionEvent, ITerminateSessionEvent } from 'vs/platform/debug/common/extensionHostDebug'; +import { IAttachSessionEvent, ICloseSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult, IReloadSessionEvent, ITerminateSessionEvent } from 'vs/platform/debug/common/extensionHostDebug'; export class ExtensionHostDebugBroadcastChannel implements IServerChannel { @@ -86,7 +86,7 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte return this.channel.listen('terminate'); } - openExtensionDevelopmentHostWindow(args: string[], env: INullableProcessEnvironment | undefined, debugRenderer: boolean): Promise { - return this.channel.call('openExtensionDevelopmentHostWindow', [args, env || {}, debugRenderer]); + openExtensionDevelopmentHostWindow(args: string[], debugRenderer: boolean): Promise { + return this.channel.call('openExtensionDevelopmentHostWindow', [args, debugRenderer]); } } diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index de274bbb30..6e23620032 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AddressInfo, createServer } from 'net'; -import { IProcessEnvironment } from 'vs/base/common/platform'; -import { INullableProcessEnvironment, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; +import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; @@ -18,13 +17,13 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens override call(ctx: TContext, command: string, arg?: any): Promise { if (command === 'openExtensionDevelopmentHostWindow') { - return this.openExtensionDevelopmentHostWindow(arg[0], arg[1], arg[2]); + return this.openExtensionDevelopmentHostWindow(arg[0], arg[1]); } else { return super.call(ctx, command, arg); } } - private async openExtensionDevelopmentHostWindow(args: string[], env: INullableProcessEnvironment, debugRenderer: boolean): Promise { + private async openExtensionDevelopmentHostWindow(args: string[], debugRenderer: boolean): Promise { const pargs = parseArgs(args, OPTIONS); pargs.debugRenderer = debugRenderer; @@ -33,27 +32,9 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens return { success: false }; } - // split INullableProcessEnvironment into a IProcessEnvironment and an array of keys to be deleted - // TODO: support to delete env vars; currently the "deletes" are ignored - let userEnv: IProcessEnvironment | undefined; - //let userEnvDeletes: string[] = []; - const keys = Object.keys(env); - for (let k of keys) { - let value = env[k]; - if (value === null) { - //userEnvDeletes.push(k); - } else { - if (!userEnv) { - userEnv = Object.create(null) as IProcessEnvironment; - } - userEnv[k] = value; - } - } - const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, { context: OpenContext.API, cli: pargs, - userEnv: userEnv }); if (!debugRenderer) { diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 2f727b8f3e..b35a5b09c9 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as osLib from 'os'; import { Iterable } from 'vs/base/common/iterator'; +import { Promises } from 'vs/base/common/async'; import { getNodeType, parse, ParseError } from 'vs/base/common/json'; import { Schemas } from 'vs/base/common/network'; import { basename, join } from 'vs/base/common/path'; @@ -11,7 +12,7 @@ import { isLinux, isWindows } from 'vs/base/common/platform'; import { ProcessItem } from 'vs/base/common/processes'; import { URI } from 'vs/base/common/uri'; import { virtualMachineHint } from 'vs/base/node/id'; -import { IDirent, Promises } from 'vs/base/node/pfs'; +import { IDirent, Promises as pfs } from 'vs/base/node/pfs'; import { listProcesses } from 'vs/base/node/ps'; import { IDiagnosticsService, IMachineInfo, IRemoteDiagnosticError, IRemoteDiagnosticInfo, isRemoteDiagnosticError, IWorkspaceInformation, PerformanceInfo, SystemInfo, WorkspaceStatItem, WorkspaceStats } from 'vs/platform/diagnostics/common/diagnostics'; import { ByteSize } from 'vs/platform/files/common/files'; @@ -55,7 +56,9 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P { tag: 'sln', filePattern: /^.+\.sln$/i }, { tag: 'csproj', filePattern: /^.+\.csproj$/i }, { tag: 'cmake', filePattern: /^.+\.cmake$/i }, - { tag: 'github-actions', filePattern: /^.+\.yml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i } + { tag: 'github-actions', filePattern: /^.+\.yml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i }, + { tag: 'devcontainer.json', filePattern: /^devcontainer\.json$/i }, + { tag: 'dockerfile', filePattern: /^(dockerfile|docker\-compose\.ya?ml)$/i } ]; const fileTypes = new Map(); @@ -66,10 +69,10 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P function collect(root: string, dir: string, filter: string[], token: { count: number, maxReached: boolean }): Promise { const relativePath = dir.substring(root.length + 1); - return new Promise(async resolve => { + return Promises.withAsyncBody(async resolve => { let files: IDirent[]; try { - files = await Promises.readdir(dir, { withFileTypes: true }); + files = await pfs.readdir(dir, { withFileTypes: true }); } catch (error) { // Ignore folders that can't be read resolve(); @@ -172,7 +175,7 @@ export async function collectLaunchConfigs(folder: string): Promise(); const launchConfig = join(folder, '.vscode', 'launch.json'); - const contents = await Promises.readFile(launchConfig); + const contents = await pfs.readFile(launchConfig); const errors: ParseError[] = []; const json = parse(contents.toString(), errors); @@ -240,8 +243,8 @@ export class DiagnosticsService implements IDiagnosticsService { } public async getPerformanceInfo(info: IMainProcessInfo, remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise { - return Promise.all([listProcesses(info.mainPID), this.formatWorkspaceMetadata(info)]).then(async result => { - let [rootProcess, workspaceInfo] = result; + return Promise.all([listProcesses(info.mainPID), this.formatWorkspaceMetadata(info)]).then(async result => { + let [rootProcess, workspaceInfo] = result; let processInfo = this.formatProcessList(info, rootProcess); remoteData.forEach(diagnostics => { diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index f17c7d2c9f..e3ea4c750d 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -16,6 +16,7 @@ import { Promises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; @@ -55,7 +56,8 @@ export class DialogMainService implements IDialogMainService { private readonly noWindowDialogueQueue = new Queue(); constructor( - @IStateMainService private readonly stateMainService: IStateMainService + @IStateMainService private readonly stateMainService: IStateMainService, + @ILogService private readonly logService: ILogService ) { } @@ -72,7 +74,7 @@ export class DialogMainService implements IDialogMainService { } pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { - const title = localize('openWorkspaceTitle', "Open Workspace"); + const title = localize('openWorkspaceTitle', "Open Workspace from File"); const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")); const filters = WORKSPACE_FILTER; @@ -155,7 +157,9 @@ export class DialogMainService implements IDialogMainService { // prevent duplicates of the same dialog queueing at the same time const fileDialogLock = this.acquireFileDialogLock(options, window); if (!fileDialogLock) { - throw new Error('A file save dialog is already or will be showing for the window with the same configuration'); + this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration'); + + return { canceled: true }; } try { @@ -203,7 +207,9 @@ export class DialogMainService implements IDialogMainService { // prevent duplicates of the same dialog queueing at the same time const fileDialogLock = this.acquireFileDialogLock(options, window); if (!fileDialogLock) { - throw new Error('A file open dialog is already or will be showing for the window with the same configuration'); + this.logService.error('[DialogMainService]: file open dialog is already or will be showing for the window with the same configuration'); + + return { canceled: true, filePaths: [] }; } try { diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index 3da5a75541..f60bb15ac0 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -6,10 +6,10 @@ import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; -import { KeyCode, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings'; import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { OS } from 'vs/base/common/platform'; -import { ScanCodeBinding } from 'vs/base/common/scanCode'; import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; import { IDriver, IDriverOptions, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; @@ -22,7 +22,7 @@ import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifec import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; function isSilentKeyCode(keyCode: KeyCode) { - return keyCode < KeyCode.KEY_0; + return keyCode < KeyCode.Digit0; } export class Driver implements IDriver, IWindowDriverRegistry { diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 2c9c45d1a6..feddff53bc 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -34,7 +34,7 @@ export interface IEditorModel { dispose(): void; } -export interface IBaseResourceEditorInput { +export interface IBaseUntypedEditorInput { /** * Optional options to use when opening the input. @@ -50,15 +50,9 @@ export interface IBaseResourceEditorInput { * Description to show for the input. */ readonly description?: string; +} - /** - * Hint to indicate that this input should be treated as a file - * that opens in an editor capable of showing file content. - * - * Without this hint, the editor service will make a guess by - * looking at the scheme of the resource(s). - */ - readonly forceFile?: boolean; +export interface IBaseResourceEditorInput extends IBaseUntypedEditorInput { /** * Hint to indicate that this input should be treated as a @@ -66,6 +60,10 @@ export interface IBaseResourceEditorInput { * * Without this hint, the editor service will make a guess by * looking at the scheme of the resource(s). + * + * Use `forceUntitled: true` when you pass in a `resource` that + * does not use the `untitled` scheme. The `resource` will then + * be used as associated path when saving the untitled file. */ readonly forceUntitled?: boolean; } @@ -288,6 +286,12 @@ export interface IEditorOptions { * not as a modal dialog. */ context?: EditorOpenContext; + + /** + * An optional property to signal that certain view state should be + * applied when opening the editor. + */ + viewState?: object; } export interface ITextEditorSelection { @@ -327,11 +331,6 @@ export interface ITextEditorOptions extends IEditorOptions { */ selection?: ITextEditorSelection; - /** - * Text editor view state. - */ - viewState?: object; - /** * Option to control the text editor selection reveal type. * Defaults to TextEditorSelectionRevealType.Center diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 9f45a2b03b..11d652a334 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -43,12 +43,15 @@ export interface NativeParsedArgs { extensionDevelopmentPath?: string[]; // undefined or array of 1 or more local paths or URIs extensionTestsPath?: string; // either a local path or a URI extensionDevelopmentKind?: string[]; + extensionEnvironment?: string; // JSON-stringified Record object 'inspect-extensions'?: string; 'inspect-brk-extensions'?: string; debugId?: string; debugRenderer?: boolean; // whether we expect a debugger (js-debug) to attach to the renderer, incl webviews+webworker 'inspect-search'?: string; 'inspect-brk-search'?: string; + 'inspect-ptyhost'?: string; + 'inspect-brk-ptyhost'?: string; 'disable-extensions'?: boolean; 'disable-extension'?: string[]; // undefined or array of 1 or more 'list-extensions'?: boolean; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 42ec90bc97..6d7696f087 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -18,6 +18,7 @@ export interface IDebugParams { export interface IExtensionHostDebugParams extends IDebugParams { debugId?: string; + env?: Record; } /** diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 90fd734de3..d552b4bb84 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -244,17 +244,29 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron } export function parseExtensionHostPort(args: NativeParsedArgs, isBuild: boolean): IExtensionHostDebugParams { - return parseDebugPort(args['inspect-extensions'], args['inspect-brk-extensions'], 5870, isBuild, args.debugId); + return parseDebugParams(args['inspect-extensions'], args['inspect-brk-extensions'], 5870, isBuild, args.debugId, args.extensionEnvironment); } export function parseSearchPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams { - return parseDebugPort(args['inspect-search'], args['inspect-brk-search'], 5876, isBuild); + return parseDebugParams(args['inspect-search'], args['inspect-brk-search'], 5876, isBuild, args.extensionEnvironment); } -function parseDebugPort(debugArg: string | undefined, debugBrkArg: string | undefined, defaultBuildPort: number, isBuild: boolean, debugId?: string): IExtensionHostDebugParams { +export function parsePtyHostPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams { + return parseDebugParams(args['inspect-ptyhost'], args['inspect-brk-ptyhost'], 5877, isBuild, args.extensionEnvironment); +} + +function parseDebugParams(debugArg: string | undefined, debugBrkArg: string | undefined, defaultBuildPort: number, isBuild: boolean, debugId?: string, environmentString?: string): IExtensionHostDebugParams { const portStr = debugBrkArg || debugArg; const port = Number(portStr) || (!isBuild ? defaultBuildPort : null); const brk = port ? Boolean(!!debugBrkArg) : false; + let env: Record | undefined; + if (environmentString) { + try { + env = JSON.parse(environmentString); + } catch { + // ignore + } + } - return { port, break: brk, debugId }; + return { port, break: brk, debugId, env }; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 7b8dbfaa86..58bc1d1438 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -86,8 +86,11 @@ export const OPTIONS: OptionDescriptions> = { 'extensionDevelopmentPath': { type: 'string[]' }, 'extensionDevelopmentKind': { type: 'string[]' }, 'extensionTestsPath': { type: 'string' }, + 'extensionEnvironment': { type: 'string' }, 'debugId': { type: 'string' }, 'debugRenderer': { type: 'boolean' }, + 'inspect-ptyhost': { type: 'string' }, + 'inspect-brk-ptyhost': { type: 'string' }, 'inspect-search': { type: 'string', deprecates: 'debugSearch' }, 'inspect-brk-search': { type: 'string', deprecates: 'debugBrkSearch' }, 'export-default-configuration': { type: 'string' }, diff --git a/src/vs/platform/environment/node/shellEnv.ts b/src/vs/platform/environment/node/shellEnv.ts index 7c2196de28..92ebb25e19 100644 --- a/src/vs/platform/environment/node/shellEnv.ts +++ b/src/vs/platform/environment/node/shellEnv.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { spawn } from 'child_process'; -import * as path from 'path'; +import { basename } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; @@ -14,11 +15,25 @@ import { getSystemShell } from 'vs/base/node/shell'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { ILogService } from 'vs/platform/log/common/log'; +import { Promises } from 'vs/base/common/async'; + +/** + * The maximum of time we accept to wait on resolving the shell + * environment before giving up. This ensures we are not blocking + * other tasks from running for a too long time period. + */ +const MAX_SHELL_RESOLVE_TIME = 10000; + +let unixShellEnvPromise: Promise | undefined = undefined; /** * We need to get the environment from a user's shell. * This should only be done when Code itself is not launched * from within a shell. + * + * Will throw an error if: + * - we hit a timeout of `MAX_SHELL_RESOLVE_TIME` + * - any other error from spawning a shell to figure out the environment */ export async function resolveShellEnv(logService: ILogService, args: NativeParsedArgs, env: IProcessEnvironment): Promise { @@ -55,28 +70,24 @@ export async function resolveShellEnv(logService: ILogService, args: NativeParse // subsequent calls since this operation can be // expensive (spawns a process). if (!unixShellEnvPromise) { - unixShellEnvPromise = new Promise(async resolve => { + unixShellEnvPromise = Promises.withAsyncBody(async (resolve, reject) => { const cts = new CancellationTokenSource(); - // Give up resolving shell env after 10 seconds + // Give up resolving shell env after some time const timeout = setTimeout(() => { - logService.error(`[resolve shell env] Could not resolve shell environment within 10 seconds. Proceeding without shell environment...`); - cts.dispose(true); - resolve({}); - }, 10000); + reject(new Error(localize('resolveShellEnvTimeout', "Unable to resolve your shell environment in a reasonable time. Please review your shell configuration."))); + }, MAX_SHELL_RESOLVE_TIME); // Resolve shell env and handle errors try { - const shellEnv = await doResolveUnixShellEnv(logService, cts.token); - - resolve(shellEnv); + resolve(await doResolveUnixShellEnv(logService, cts.token)); } catch (error) { - if (!isPromiseCanceledError(error)) { - logService.error(`[resolve shell env] Unable to resolve shell environment (${error}). Proceeding without shell environment...`); + if (!isPromiseCanceledError(error) && !cts.token.isCancellationRequested) { + reject(new Error(localize('resolveShellEnvError', "Unable to resolve your shell environment: {0}", toErrorMessage(error)))); + } else { + resolve({}); } - - resolve({}); } finally { clearTimeout(timeout); cts.dispose(); @@ -88,35 +99,33 @@ export async function resolveShellEnv(logService: ILogService, args: NativeParse } } -let unixShellEnvPromise: Promise | undefined = undefined; - async function doResolveUnixShellEnv(logService: ILogService, token: CancellationToken): Promise { - const promise = new Promise(async (resolve, reject) => { - const runAsNode = process.env['ELECTRON_RUN_AS_NODE']; - logService.trace('getUnixShellEnvironment#runAsNode', runAsNode); + const runAsNode = process.env['ELECTRON_RUN_AS_NODE']; + logService.trace('getUnixShellEnvironment#runAsNode', runAsNode); - const noAttach = process.env['ELECTRON_NO_ATTACH_CONSOLE']; - logService.trace('getUnixShellEnvironment#noAttach', noAttach); + const noAttach = process.env['ELECTRON_NO_ATTACH_CONSOLE']; + logService.trace('getUnixShellEnvironment#noAttach', noAttach); - const mark = generateUuid().replace(/-/g, '').substr(0, 12); - const regex = new RegExp(mark + '(.*)' + mark); + const mark = generateUuid().replace(/-/g, '').substr(0, 12); + const regex = new RegExp(mark + '(.*)' + mark); - const env = { - ...process.env, - ELECTRON_RUN_AS_NODE: '1', - ELECTRON_NO_ATTACH_CONSOLE: '1' - }; + const env = { + ...process.env, + ELECTRON_RUN_AS_NODE: '1', + ELECTRON_NO_ATTACH_CONSOLE: '1' + }; - logService.trace('getUnixShellEnvironment#env', env); - const systemShellUnix = await getSystemShell(OS, env); - logService.trace('getUnixShellEnvironment#shell', systemShellUnix); + logService.trace('getUnixShellEnvironment#env', env); + const systemShellUnix = await getSystemShell(OS, env); + logService.trace('getUnixShellEnvironment#shell', systemShellUnix); + return new Promise((resolve, reject) => { if (token.isCancellationRequested) { - return reject(canceled); + return reject(canceled()); } // handle popular non-POSIX shells - const name = path.basename(systemShellUnix); + const name = basename(systemShellUnix); let command: string, shellArgs: Array; if (/^pwsh(-preview)?$/.test(name)) { // Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how @@ -125,7 +134,12 @@ async function doResolveUnixShellEnv(logService: ILogService, token: Cancellatio shellArgs = ['-Login', '-Command']; } else { command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`; - shellArgs = ['-ilc']; + + if (name === 'tcsh') { + shellArgs = ['-ic']; + } else { + shellArgs = ['-ilc']; + } } logService.trace('getUnixShellEnvironment#spawn', JSON.stringify(shellArgs), command); @@ -139,12 +153,12 @@ async function doResolveUnixShellEnv(logService: ILogService, token: Cancellatio token.onCancellationRequested(() => { child.kill(); - return reject(canceled); + return reject(canceled()); }); child.on('error', err => { logService.error('getUnixShellEnvironment#errorChildProcess', toErrorMessage(err)); - resolve({}); + reject(err); }); const buffers: Buffer[] = []; @@ -163,7 +177,7 @@ async function doResolveUnixShellEnv(logService: ILogService, token: Cancellatio } if (code || signal) { - return reject(new Error(`Failed to get environment (code ${code}, signal ${signal})`)); + return reject(new Error(localize('resolveShellEnvExitError', "Unexpected exit code from spawned shell (code {0}, signal {1})", code, signal))); } const match = regex.exec(raw); @@ -195,12 +209,4 @@ async function doResolveUnixShellEnv(logService: ILogService, token: Cancellatio } }); }); - - try { - return await promise; - } catch (error) { - logService.error('getUnixShellEnvironment#error', toErrorMessage(error)); - - return {}; // ignore any errors - } } diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index 6287559fca..a079b4664c 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -14,35 +14,36 @@ suite('EnvironmentService', () => { test('parseExtensionHostPort when built', () => { const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), true); - assert.deepStrictEqual(parse([]), { port: null, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse([]), { port: null, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost']), { port: null, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, env: undefined, debugId: '7' }); - assert.deepStrictEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions']), { port: null, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, env: undefined, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--extensionEnvironment={"COOL":"1"}']), { port: 5678, break: true, env: { COOL: '1' }, debugId: undefined }); }); test('parseExtensionHostPort when unbuilt', () => { const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), false); - assert.deepStrictEqual(parse([]), { port: 5870, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse([]), { port: 5870, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost']), { port: 5870, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, env: undefined, debugId: '7' }); - assert.deepStrictEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined }); - assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions']), { port: 5870, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, env: undefined, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, env: undefined, debugId: '7' }); }); // https://github.com/microsoft/vscode/issues/78440 diff --git a/src/vs/platform/environment/test/node/nativeModules.test.ts b/src/vs/platform/environment/test/node/nativeModules.test.ts index 6b94909996..8e4dd2a9fb 100644 --- a/src/vs/platform/environment/test/node/nativeModules.test.ts +++ b/src/vs/platform/environment/test/node/nativeModules.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isWindows } from 'vs/base/common/platform'; function testErrorMessage(module: string): string { return `Unable to load "${module}" dependency. It was probably not compiled for the right operating system architecture or had missing build tools.`; @@ -38,27 +38,21 @@ suite('Native Modules (all platforms)', () => { }); test('nsfw', async () => { - const nsfWatcher = await import('nsfw'); + const nsfWatcher = await import('vscode-nsfw'); assert.ok(typeof nsfWatcher === 'function', testErrorMessage('nsfw')); }); + test('parcel', async () => { + const parcelWatcher = await import('@parcel/watcher'); + assert.ok(typeof parcelWatcher.subscribe === 'function', testErrorMessage('parcel')); + }); + test('sqlite3', async () => { const sqlite3 = await import('@vscode/sqlite3'); assert.ok(typeof sqlite3.Database === 'function', testErrorMessage('@vscode/sqlite3')); }); }); -(!isMacintosh ? suite.skip : suite)('Native Modules (macOS)', () => { - - test('chokidar (fsevents)', async () => { - const chokidar = await import('chokidar'); - const watcher = chokidar.watch(__dirname); - assert.ok(watcher.options.useFsEvents, testErrorMessage('chokidar (fsevents)')); - - return watcher.close(); - }); -}); - (!isWindows ? suite.skip : suite)('Native Modules (Windows)', () => { test('windows-mutex', async () => { diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 8d66de263a..13f3ede71b 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Barrier, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { canceled, getErrorMessage } from 'vs/base/common/errors'; @@ -13,18 +14,14 @@ import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { DidUninstallExtensionEvent, ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOperation, InstallOptions, - InstallVSIXOptions, INSTALL_ERROR_INCOMPATIBLE, INSTALL_ERROR_MALICIOUS, IReportedExtension, StatisticType, UninstallOptions + InstallVSIXOptions, IReportedExtension, StatisticType, UninstallOptions, TargetPlatform, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -export const INSTALL_ERROR_VALIDATING = 'validating'; -export const ERROR_UNKNOWN = 'unknown'; -export const INSTALL_ERROR_LOCAL = 'local'; - export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; @@ -70,6 +67,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl @IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService, @ITelemetryService protected readonly telemetryService: ITelemetryService, @ILogService protected readonly logService: ILogService, + @IProductService protected readonly productService: IProductService ) { super(); this._register(toDisposable(() => { @@ -80,30 +78,43 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); } + async canInstall(extension: IGalleryExtension): Promise { + const currentTargetPlatform = await this.getTargetPlatform(); + return extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform)); + } + async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise { if (!this.galleryService.isEnabled()) { - throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled")); + throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal); + } + + if (!await this.canInstall(extension)) { + const targetPlatform = await this.getTargetPlatform(); + const error = new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.Incompatible); + this.logService.error(`Cannot install extension.`, extension.identifier.id, error.message); + reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error); + throw error; } try { - extension = await this.checkAndGetCompatibleVersion(extension); + extension = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion); } catch (error) { this.logService.error(getErrorMessage(error)); reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error); throw error; } - if (!await this.canInstall(extension)) { - const error = new ExtensionManagementError(`Not supported`, INSTALL_ERROR_VALIDATING); - this.logService.error(`Canno install extension as it is not supported.`, extension.identifier.id, error.message); + const manifest = await this.galleryService.getManifest(extension, CancellationToken.None); + if (manifest === null) { + const error = new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid); + this.logService.error(`Failed to install extension:`, extension.identifier.id, error.message); reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error); throw error; } - const manifest = await this.galleryService.getManifest(extension, CancellationToken.None); - if (manifest === null) { - const error = new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, INSTALL_ERROR_VALIDATING); - this.logService.error(`Failed to install extension:`, extension.identifier.id, error.message); + if (manifest.version !== extension.version) { + const error = new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid); + this.logService.error(error.message); reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error); throw error; } @@ -187,9 +198,20 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } } } catch (error) { - this.logService.error('Error while preparing to install dependencies and extension packs of the extension:', installExtensionTask.identifier.id); - this.logService.error(error); - throw error; + // Installing through VSIX + if (URI.isUri(installExtensionTask.source)) { + // Ignore installing dependencies and packs + if (isNonEmptyArray(manifest.extensionDependencies)) { + this.logService.warn(`Cannot install dependencies of extension:`, installExtensionTask.identifier.id, error.message); + } + if (isNonEmptyArray(manifest.extensionPack)) { + this.logService.warn(`Cannot install packed extensions of extension:`, installExtensionTask.identifier.id, error.message); + } + } else { + this.logService.error('Error while preparing to install dependencies and extension packs of the extension:', installExtensionTask.identifier.id); + this.logService.error(error); + throw error; + } } } @@ -269,7 +291,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source }))); if (error instanceof Error) { - error.name = error && (error).code ? (error).code : ERROR_UNKNOWN; + error.name = error && (error).code ? (error).code : ExtensionManagementErrorCode.Internal; } throw error; } finally { @@ -311,7 +333,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const allDependenciesAndPacks: { gallery: IGalleryExtension, manifest: IExtensionManifest }[] = []; const collectDependenciesAndPackExtensionsToInstall = async (extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest): Promise => { - const dependenciesAndPackExtensions: string[] = manifest.extensionDependencies || []; + const dependecies: string[] = manifest.extensionDependencies || []; + const dependenciesAndPackExtensions = [...dependecies]; if (manifest.extensionPack) { const existing = getOnlyNewlyAddedFromExtensionPack ? installed.find(e => areSameExtensions(e.identifier, extensionIdentifier)) : undefined; for (const extension of manifest.extensionPack) { @@ -334,14 +357,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl if (identifiers.find(identifier => areSameExtensions(identifier, galleryExtension.identifier))) { continue; } - const compatibleExtension = await this.checkAndGetCompatibleVersion(galleryExtension); - if (!await this.canInstall(compatibleExtension)) { - this.logService.info('Skipping the extension as it cannot be installed', compatibleExtension.identifier.id); + const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier)); + if (!isDependency && !await this.canInstall(galleryExtension)) { + this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id); continue; } + const compatibleExtension = await this.checkAndGetCompatibleVersion(galleryExtension, true); const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None); if (manifest === null) { - throw new ExtensionManagementError(`Missing manifest for extension ${compatibleExtension.identifier.id}`, INSTALL_ERROR_VALIDATING); + throw new ExtensionManagementError(`Missing manifest for extension ${compatibleExtension.identifier.id}`, ExtensionManagementErrorCode.Invalid); } allDependenciesAndPacks.push({ gallery: compatibleExtension, manifest }); await collectDependenciesAndPackExtensionsToInstall(compatibleExtension.identifier, manifest); @@ -355,14 +379,28 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return allDependenciesAndPacks.filter(e => !installed.some(i => areSameExtensions(i.identifier, e.gallery.identifier))); } - private async checkAndGetCompatibleVersion(extension: IGalleryExtension): Promise { + private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise { if (await this.isMalicious(extension)) { - throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), INSTALL_ERROR_MALICIOUS); + throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious); } - const compatibleExtension = await this.galleryService.getCompatibleExtension(extension); + const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion); if (!compatibleExtension) { - throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of VS Code (version {1}).", extension.identifier.id, product.version), INSTALL_ERROR_INCOMPATIBLE); + throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible); + } + + return compatibleExtension; + } + + protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise { + const targetPlatform = await this.getTargetPlatform(); + let compatibleExtension: IGalleryExtension | null = null; + if (await this.galleryService.isExtensionCompatible(extension, targetPlatform)) { + compatibleExtension = extension; + } + + if (!compatibleExtension && fetchCompatibleVersion) { + compatibleExtension = await this.galleryService.getCompatibleExtension(extension, targetPlatform); } return compatibleExtension; @@ -437,7 +475,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } postUninstallExtension(task.extension); } catch (e) { - const error = e instanceof ExtensionManagementError ? e : new ExtensionManagementError(getErrorMessage(e), ERROR_UNKNOWN); + const error = e instanceof ExtensionManagementError ? e : new ExtensionManagementError(getErrorMessage(e), ExtensionManagementErrorCode.Internal); postUninstallExtension(task.extension, error); throw error; } finally { @@ -446,7 +484,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); } catch (e) { - const error = e instanceof ExtensionManagementError ? e : new ExtensionManagementError(getErrorMessage(e), ERROR_UNKNOWN); + const error = e instanceof ExtensionManagementError ? e : new ExtensionManagementError(getErrorMessage(e), ExtensionManagementErrorCode.Internal); for (const task of allTasks) { // cancel the tasks try { task.cancel(); } catch (error) { /* ignore */ } @@ -557,11 +595,11 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } } + abstract getTargetPlatform(): Promise; abstract zip(extension: ILocalExtension): Promise; abstract unzip(zipLocation: URI): Promise; abstract getManifest(vsix: URI): Promise; abstract install(vsix: URI, options?: InstallVSIXOptions): Promise; - abstract canInstall(extension: IGalleryExtension): Promise; abstract getInstalled(type?: ExtensionType): Promise; abstract updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise; @@ -582,7 +620,7 @@ export function joinErrors(errorOrErrors: (Error | string) | (Array 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; @@ -288,8 +301,8 @@ function getDownloadAsset(version: IRawGalleryExtensionVersion): IGalleryExtensi // {{SQL CARBON EDIT}} - End return { - uri: `${version.fallbackAssetUri}/${AssetType.VSIX}?redirect=true`, - fallbackUri: `${version.fallbackAssetUri}/${AssetType.VSIX}` + uri: `${version.fallbackAssetUri}/${AssetType.VSIX}?redirect=true${version.targetPlatform ? `&targetPlatform=${version.targetPlatform}` : ''}`, + fallbackUri: `${version.fallbackAssetUri}/${AssetType.VSIX}${version.targetPlatform ? `?targetPlatform=${version.targetPlatform}` : ''}` }; } @@ -355,7 +368,66 @@ function getIsPreview(flags: string): boolean { return flags.indexOf('preview') !== -1; } -function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, index: number, query: Query, querySource?: string): IGalleryExtension { +function getTargetPlatformForExtensionVersion(version: IRawGalleryExtensionVersion): TargetPlatform { + return version.targetPlatform ? toTargetPlatform(version.targetPlatform) : TargetPlatform.UNDEFINED; +} + +function getAllTargetPlatforms(rawGalleryExtension: IRawGalleryExtension): TargetPlatform[] { + const allTargetPlatforms = distinct(rawGalleryExtension.versions.map(getTargetPlatformForExtensionVersion)); + + // Is a web extension only if it has WEB_EXTENSION_TAG + const isWebExtension = !!rawGalleryExtension.tags?.includes(WEB_EXTENSION_TAG); + + // Include Web Target Platform only if it is a web extension + const webTargetPlatformIndex = allTargetPlatforms.indexOf(TargetPlatform.WEB); + if (isWebExtension) { + if (webTargetPlatformIndex === -1) { + // Web extension but does not has web target platform -> add it + allTargetPlatforms.push(TargetPlatform.WEB); + } + } else { + if (webTargetPlatformIndex !== -1) { + // Not a web extension but has web target platform -> remove it + allTargetPlatforms.splice(webTargetPlatformIndex, 1); + } + } + + return allTargetPlatforms; +} + +export function sortExtensionVersions(versions: IRawGalleryExtensionVersion[], preferredTargetPlatform: TargetPlatform): IRawGalleryExtensionVersion[] { + /* It is expected that versions from Marketplace are sorted by version. So we are just sorting by preferred targetPlatform */ + const fallbackTargetPlatforms = getFallbackTargetPlarforms(preferredTargetPlatform); + for (let index = 0; index < versions.length; index++) { + const version = versions[index]; + if (version.version === versions[index - 1]?.version) { + let insertionIndex = index; + const versionTargetPlatform = getTargetPlatformForExtensionVersion(version); + /* put it at the beginning */ + if (versionTargetPlatform === preferredTargetPlatform) { + while (insertionIndex > 0 && versions[insertionIndex - 1].version === version.version) { insertionIndex--; } + } + /* put it after version with preferred targetPlatform or at the beginning */ + else if (fallbackTargetPlatforms.includes(versionTargetPlatform)) { + while (insertionIndex > 0 && versions[insertionIndex - 1].version === version.version && getTargetPlatformForExtensionVersion(versions[insertionIndex - 1]) !== preferredTargetPlatform) { insertionIndex--; } + } + if (insertionIndex !== index) { + versions.splice(index, 1); + versions.splice(insertionIndex, 0, version); + } + } + } + return versions; +} + +function toExtensionWithLatestVersion(galleryExtension: IRawGalleryExtension, index: number, query: Query, querySource: string | undefined, targetPlatform: TargetPlatform): IGalleryExtension { + const allTargetPlatforms = getAllTargetPlatforms(galleryExtension); + let latestVersion = galleryExtension.versions[0]; + latestVersion = galleryExtension.versions.find(version => version.version === latestVersion.version && isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(version), allTargetPlatforms, targetPlatform)) || latestVersion; + return toExtension(galleryExtension, latestVersion, allTargetPlatforms, index, query, querySource); +} + +function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], index: number, query: Query, querySource?: string): IGalleryExtension { const assets = { manifest: getVersionAsset(version, AssetType.Manifest), readme: getVersionAsset(version, AssetType.Details), @@ -380,6 +452,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller publisherId: galleryExtension.publisher.publisherId, publisher: galleryExtension.publisher.publisherName, publisherDisplayName: galleryExtension.publisher.displayName, + publisherDomain: galleryExtension.publisher.domain ? { link: galleryExtension.publisher.domain, verified: !!galleryExtension.publisher.isDomainVerified } : undefined, description: galleryExtension.shortDescription || '', installCount: getStatistic(galleryExtension.statistics, 'install'), rating: getStatistic(galleryExtension.statistics, 'averagerating'), @@ -388,7 +461,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller tags: galleryExtension.tags || [], releaseDate: Date.parse(galleryExtension.releaseDate), lastUpdated: Date.parse(version.lastUpdated), // {{SQL CARBON EDIT}} We don't have the lastUpdated at the top level currently - webExtension: !!galleryExtension.tags?.includes(WEB_EXTENSION_TAG), + allTargetPlatforms, assets, properties: { dependencies: getExtensions(version, PropertyType.Dependency), @@ -397,7 +470,9 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller // {{SQL CARBON EDIT}} azDataEngine: getAzureDataStudioEngine(version), localizedLanguages: getLocalizedLanguages(version), + targetPlatform: getTargetPlatformForExtensionVersion(version), }, + preview: getIsPreview(galleryExtension.flags), /* __GDPR__FRAGMENT__ "GalleryExtensionTelemetryData2" : { "index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -408,7 +483,6 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller index: ((query.pageNumber - 1) * query.pageSize) + index, querySource }, - preview: getIsPreview(galleryExtension.flags) }; } @@ -417,7 +491,7 @@ interface IRawExtensionsReport { slow: string[]; } -export class ExtensionGalleryService implements IExtensionGalleryService { +abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { declare readonly _serviceBrand: undefined; @@ -427,19 +501,19 @@ export class ExtensionGalleryService implements IExtensionGalleryService { private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>; constructor( + storageService: IStorageService | undefined, @IRequestService private readonly requestService: IRequestService, @ILogService private readonly logService: ILogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IConfigurationService private configurationService: IConfigurationService, // {{SQL CARBON EDIT}} @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService, - @optional(IStorageService) storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { const config = productService.extensionsGallery; this.extensionsGalleryUrl = config && config.serviceUrl; this.extensionsControlUrl = config && config.controlUrl; - this.commonHeadersPromise = resolveMarketplaceHeaders(productService.version, this.environmentService, this.fileService, storageService); + this.commonHeadersPromise = resolveMarketplaceHeaders(productService.version, productService, this.environmentService, this.configurationService, this.fileService, storageService); } private api(path = ''): string { @@ -451,18 +525,36 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return !!this.extensionsGalleryUrl; } - async getExtensions(names: string[], token: CancellationToken): Promise { + async getExtensions(identifiers: ReadonlyArray, token: CancellationToken): Promise { const result: IGalleryExtension[] = []; - let { total, firstPage: pageResult, getPage } = await this.query({ names, pageSize: names.length }, token); - result.push(...pageResult); - for (let pageIndex = 1; result.length < total; pageIndex++) { - pageResult = await getPage(pageIndex, token); - if (pageResult.length) { - result.push(...pageResult); + let query = new Query() + .withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties) + .withPage(1, identifiers.length) + .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code') + .withFilter(FilterType.ExtensionName, ...identifiers.map(({ id }) => id.toLowerCase())); + + if (identifiers.every(identifier => !(identifier).version)) { + query = query.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.IncludeLatestVersionOnly); + } + + const { galleryExtensions } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, CancellationToken.None); + for (let index = 0; index < galleryExtensions.length; index++) { + const galleryExtension = galleryExtensions[index]; + if (!galleryExtension.versions.length) { + continue; + } + const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName); + const version = (identifiers.find(identifier => areSameExtensions(identifier, { id })))?.version; + if (version) { + const versionAsset = galleryExtension.versions.find(v => v.version === version); + if (versionAsset) { + result.push(toExtension(galleryExtension, versionAsset, getAllTargetPlatforms(galleryExtension), index, query)); + } } else { - break; + result.push(toExtensionWithLatestVersion(galleryExtension, index, query, undefined, CURRENT_TARGET_PLATFORM)); } } + return result; } @@ -492,16 +584,18 @@ export class ExtensionGalleryService implements IExtensionGalleryService { query = query.withFilter(FilterType.ExtensionName, id); } - const { galleryExtensions } = await this.queryGallery(query, CancellationToken.None); + const { galleryExtensions } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, CancellationToken.None); const [rawExtension] = galleryExtensions; if (!rawExtension || !rawExtension.versions.length) { return null; } + const allTargetPlatforms = getAllTargetPlatforms(rawExtension); + if (version) { const versionAsset = rawExtension.versions.filter(v => v.version === version)[0]; if (versionAsset) { - const extension = toExtension(rawExtension, versionAsset, 0, query); + const extension = toExtension(rawExtension, versionAsset, allTargetPlatforms, 0, query); if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) { return extension; } @@ -511,11 +605,36 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const rawVersion = await this.getLastValidExtensionVersion(rawExtension, rawExtension.versions); if (rawVersion) { - return toExtension(rawExtension, rawVersion, 0, query); + return toExtension(rawExtension, rawVersion, allTargetPlatforms, 0, query); } return null; } + async isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise { + if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) { + return false; + } + + let engine = extension.properties.engine; + if (!engine) { + const manifest = await this.getManifest(extension, CancellationToken.None); + if (!manifest) { + throw new Error('Manifest was not found'); + } + engine = manifest.engines.vscode; + } + return isEngineValid(engine, this.productService.version, this.productService.date); + } + + private async isRawExtensionVersionCompatible(rawExtensionVersion: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise { + if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawExtensionVersion), allTargetPlatforms, targetPlatform)) { + return false; + } + + const engine = await this.getEngine(rawExtensionVersion); + return isEngineValid(engine, this.productService.version, this.productService.date); + } + query(token: CancellationToken): Promise>; query(options: IQueryOptions, token: CancellationToken): Promise>; async query(arg1: any, arg2?: any): Promise> { @@ -592,15 +711,15 @@ export class ExtensionGalleryService implements IExtensionGalleryService { query = query.withSortOrder(options.sortOrder); } - const { galleryExtensions, total } = await this.queryGallery(query, token); - const extensions = galleryExtensions.map((e, index) => toExtension(e, e.versions[0], index, query, options.source)); + const { galleryExtensions, total } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, token); + const extensions = galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, query, options.source, CURRENT_TARGET_PLATFORM)); const getPage = async (pageIndex: number, ct: CancellationToken) => { if (ct.isCancellationRequested) { throw canceled(); } const nextPageQuery = query.withPage(pageIndex + 1); - const { galleryExtensions } = await this.queryGallery(nextPageQuery, ct); - return galleryExtensions.map((e, index) => toExtension(e, e.versions[0], index, nextPageQuery, options.source)); + const { galleryExtensions } = await this.queryGallery(nextPageQuery, CURRENT_TARGET_PLATFORM, ct); + return galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, nextPageQuery, options.source, CURRENT_TARGET_PLATFORM)); }; // {{SQL CARBON EDIT}} @@ -632,7 +751,8 @@ export class ExtensionGalleryService implements IExtensionGalleryService { filteredExtensions = filteredExtensions.filter(e => { // we only have 1 version for our extensions in the gallery file, so this should always be the case if (e.versions.length === 1) { - const extension = toExtension(e, e.versions[0], 0, query); + const allTargetPlatforms = getAllTargetPlatforms(e); + const extension = toExtension(e, e.versions[0], allTargetPlatforms, 0, query); return extension.properties.localizedLanguages && extension.properties.localizedLanguages.length > 0; } return false; @@ -721,7 +841,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return a[fieldName] < b[fieldName] ? -1 : 1; } - private async queryGallery(query: Query, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> { + private async queryGallery(query: Query, targetPlatform: TargetPlatform, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> { if (!this.isEnabled()) { throw new Error('No extension gallery service configured.'); } @@ -758,6 +878,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (result) { const r = result.results[0]; const galleryExtensions = r.extensions; + galleryExtensions.forEach(e => sortExtensionVersions(e.versions, targetPlatform)); // 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 @@ -807,8 +928,8 @@ export class ExtensionGalleryService implements IExtensionGalleryService { // const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; const operationParam = undefined; const downloadAsset = operationParam ? { - uri: `${extension.assets.download.uri}&${operationParam}=true`, - fallbackUri: `${extension.assets.download.fallbackUri}?${operationParam}=true` + uri: `${extension.assets.download.uri}${URI.parse(extension.assets.download.uri).query ? '&' : '?'}${operationParam}=true`, + fallbackUri: `${extension.assets.download.fallbackUri}${URI.parse(extension.assets.download.fallbackUri).query ? '&' : '?'}${operationParam}=true` } : extension.assets.download; const context = await this.getAsset(downloadAsset); @@ -834,6 +955,16 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return null; } + private async getManifestFromRawExtensionVersion(rawExtensionVersion: IRawGalleryExtensionVersion, token: CancellationToken): Promise { + const manifestAsset = getVersionAsset(rawExtensionVersion, AssetType.Manifest); + if (!manifestAsset) { + throw new Error('Manifest was not found'); + } + const headers = { 'Accept-Encoding': 'gzip' }; + const context = await this.getAsset(manifestAsset, { headers }); + return await asJson(context); + } + async getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise { const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0]; if (asset) { @@ -853,7 +984,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return ''; } - async getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise { + async getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise { let query = new Query() .withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, 1) @@ -865,22 +996,23 @@ export class ExtensionGalleryService implements IExtensionGalleryService { query = query.withFilter(FilterType.ExtensionName, extension.identifier.id); } + const { galleryExtensions } = await this.queryGallery(query, targetPlatform, CancellationToken.None); + if (!galleryExtensions.length) { + return []; + } + + const allTargetPlatforms = getAllTargetPlatforms(galleryExtensions[0]); + if (isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, targetPlatform)) { + return []; + } + const result: IGalleryExtensionVersion[] = []; - const { galleryExtensions } = await this.queryGallery(query, CancellationToken.None); - if (galleryExtensions.length) { - if (compatible) { - await Promise.all(galleryExtensions[0].versions.map(async v => { - let engine: string | undefined; - try { - engine = await this.getEngine(v); - } catch (error) { /* Ignore error and skip version */ } - if (engine && isEngineValid(engine, this.productService.version, this.productService.date)) { - result.push({ version: v!.version, date: v!.lastUpdated }); - } - })); - } else { - result.push(...galleryExtensions[0].versions.map(v => ({ version: v.version, date: v.lastUpdated }))); - } + for (const version of galleryExtensions[0].versions) { + try { + if (result[result.length - 1]?.version !== version.version && await this.isRawExtensionVersionCompatible(version, allTargetPlatforms, targetPlatform)) { + result.push({ version: version.version, date: version.lastUpdated }); + } + } catch (error) { /* Ignore error and skip version */ } } return result; } @@ -922,7 +1054,6 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return this.requestService.request(fallbackOptions, token); } } - private async getLastValidExtensionVersion(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise { const version = this.getLastValidExtensionVersionFromProperties(extension, versions); if (version) { @@ -949,25 +1080,16 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return null; } - private async getEngine(version: IRawGalleryExtensionVersion): Promise { - const engine = getEngine(version); - if (engine) { - return engine; + private async getEngine(rawExtensionVersion: IRawGalleryExtensionVersion): Promise { + let engine = getEngine(rawExtensionVersion); + if (!engine) { + const manifest = await this.getManifestFromRawExtensionVersion(rawExtensionVersion, CancellationToken.None); + if (!manifest) { + throw new Error('Manifest was not found'); + } + engine = manifest.engines.vscode; } - - const manifestAsset = getVersionAsset(version, AssetType.Manifest); - if (!manifestAsset) { - throw new Error('Manifest was not found'); - } - - const headers = { 'Accept-Encoding': 'gzip' }; - const context = await this.getAsset(manifestAsset, { headers }); - const manifest = await asJson(context); - if (manifest) { - return manifest.engines.vscode; - } - - throw new Error('Error while reading manifest'); + return engine; } private async getLastValidExtensionVersionRecursively(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise { @@ -1016,7 +1138,38 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } } -export async function resolveMarketplaceHeaders(version: string, environmentService: IEnvironmentService, fileService: IFileService, storageService: { +export class ExtensionGalleryService extends AbstractExtensionGalleryService { + + constructor( + @IStorageService storageService: IStorageService, + @IRequestService requestService: IRequestService, + @ILogService logService: ILogService, + @IEnvironmentService environmentService: IEnvironmentService, + @ITelemetryService telemetryService: ITelemetryService, + @IFileService fileService: IFileService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(storageService, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService); + } +} + +export class ExtensionGalleryServiceWithNoStorageService extends AbstractExtensionGalleryService { + + constructor( + @IRequestService requestService: IRequestService, + @ILogService logService: ILogService, + @IEnvironmentService environmentService: IEnvironmentService, + @ITelemetryService telemetryService: ITelemetryService, + @IFileService fileService: IFileService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(undefined, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService); + } +} + +export async function resolveMarketplaceHeaders(version: string, productService: IProductService, environmentService: IEnvironmentService, configurationService: IConfigurationService, fileService: IFileService, storageService: { get: (key: string, scope: StorageScope) => string | undefined, store: (key: string, value: string, scope: StorageScope, target: StorageTarget) => void } | undefined): Promise<{ [key: string]: string; }> { @@ -1025,6 +1178,8 @@ export async function resolveMarketplaceHeaders(version: string, environmentServ 'User-Agent': `VSCode ${version}` }; const uuid = await getServiceMachineId(environmentService, fileService, storageService); - headers['X-Market-User-Id'] = uuid; + if (supportsTelemetry(productService, environmentService) && getTelemetryLevel(configurationService) === TelemetryLevel.USAGE) { + headers['X-Market-User-Id'] = uuid; + } return headers; } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index c0aa2c1006..57f8ffff31 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -7,6 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { FileAccess } from 'vs/base/common/network'; import { IPager } from 'vs/base/common/paging'; +import { Platform } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ExtensionType, IExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; @@ -16,6 +17,167 @@ export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); export const WEB_EXTENSION_TAG = '__web_extension'; +export const enum TargetPlatform { + WIN32_X64 = 'win32-x64', + WIN32_IA32 = 'win32-ia32', + WIN32_ARM64 = 'win32-arm64', + + LINUX_X64 = 'linux-x64', + LINUX_ARM64 = 'linux-arm64', + LINUX_ARMHF = 'linux-armhf', + + ALPINE_X64 = 'alpine-x64', + ALPINE_ARM64 = 'alpine-arm64', + + DARWIN_X64 = 'darwin-x64', + DARWIN_ARM64 = 'darwin-arm64', + + WEB = 'web', + + UNIVERSAL = 'universal', + UNKNOWN = 'unknown', + UNDEFINED = 'undefined', +} + +export function TargetPlatformToString(targetPlatform: TargetPlatform) { + switch (targetPlatform) { + case TargetPlatform.WIN32_X64: return 'Windows 64 bit'; + case TargetPlatform.WIN32_IA32: return 'Windows 32 bit'; + case TargetPlatform.WIN32_ARM64: return 'Windows ARM'; + + case TargetPlatform.LINUX_X64: return 'Linux 64 bit'; + case TargetPlatform.LINUX_ARM64: return 'Linux ARM 64'; + case TargetPlatform.LINUX_ARMHF: return 'Linux ARM'; + + case TargetPlatform.ALPINE_X64: return 'Alpine Linux 64 bit'; + case TargetPlatform.ALPINE_ARM64: return 'Alpine ARM 64'; + + case TargetPlatform.DARWIN_X64: return 'Mac'; + case TargetPlatform.DARWIN_ARM64: return 'Mac Silicon'; + + case TargetPlatform.WEB: return 'Web'; + + case TargetPlatform.UNIVERSAL: return TargetPlatform.UNIVERSAL; + case TargetPlatform.UNKNOWN: return TargetPlatform.UNKNOWN; + case TargetPlatform.UNDEFINED: return TargetPlatform.UNDEFINED; + } +} + +export function toTargetPlatform(targetPlatform: string): TargetPlatform { + switch (targetPlatform) { + case TargetPlatform.WIN32_X64: return TargetPlatform.WIN32_X64; + case TargetPlatform.WIN32_IA32: return TargetPlatform.WIN32_IA32; + case TargetPlatform.WIN32_ARM64: return TargetPlatform.WIN32_ARM64; + + case TargetPlatform.LINUX_X64: return TargetPlatform.LINUX_X64; + case TargetPlatform.LINUX_ARM64: return TargetPlatform.LINUX_ARM64; + case TargetPlatform.LINUX_ARMHF: return TargetPlatform.LINUX_ARMHF; + + case TargetPlatform.ALPINE_X64: return TargetPlatform.ALPINE_X64; + case TargetPlatform.ALPINE_ARM64: return TargetPlatform.ALPINE_ARM64; + + case TargetPlatform.DARWIN_X64: return TargetPlatform.DARWIN_X64; + case TargetPlatform.DARWIN_ARM64: return TargetPlatform.DARWIN_ARM64; + + case TargetPlatform.WEB: return TargetPlatform.WEB; + + case TargetPlatform.UNIVERSAL: return TargetPlatform.UNIVERSAL; + default: return TargetPlatform.UNKNOWN; + } +} + +export function getTargetPlatform(platform: Platform | 'alpine', arch: string | undefined): TargetPlatform { + switch (platform) { + case Platform.Windows: + if (arch === 'x64') { + return TargetPlatform.WIN32_X64; + } + if (arch === 'ia32') { + return TargetPlatform.WIN32_IA32; + } + if (arch === 'arm64') { + return TargetPlatform.WIN32_ARM64; + } + return TargetPlatform.UNKNOWN; + + case Platform.Linux: + if (arch === 'x64') { + return TargetPlatform.LINUX_X64; + } + if (arch === 'arm64') { + return TargetPlatform.LINUX_ARM64; + } + if (arch === 'arm') { + return TargetPlatform.LINUX_ARMHF; + } + return TargetPlatform.UNKNOWN; + + case 'alpine': + if (arch === 'x64') { + return TargetPlatform.ALPINE_X64; + } + if (arch === 'arm64') { + return TargetPlatform.ALPINE_ARM64; + } + return TargetPlatform.UNKNOWN; + + case Platform.Mac: + if (arch === 'x64') { + return TargetPlatform.DARWIN_X64; + } + if (arch === 'arm64') { + return TargetPlatform.DARWIN_ARM64; + } + return TargetPlatform.UNKNOWN; + + case Platform.Web: return TargetPlatform.WEB; + } +} + +export function isNotWebExtensionInWebTargetPlatform(allTargetPlatforms: TargetPlatform[], productTargetPlatform: TargetPlatform): boolean { + // Not a web extension in web target platform + return productTargetPlatform === TargetPlatform.WEB && !allTargetPlatforms.includes(TargetPlatform.WEB); +} + +export function isTargetPlatformCompatible(extensionTargetPlatform: TargetPlatform, allTargetPlatforms: TargetPlatform[], productTargetPlatform: TargetPlatform): boolean { + // Not compatible when extension is not a web extension in web target platform + if (isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, productTargetPlatform)) { + return false; + } + + // Compatible when extension target platform is not defined + if (extensionTargetPlatform === TargetPlatform.UNDEFINED) { + return true; + } + + // Compatible when extension target platform is universal + if (extensionTargetPlatform === TargetPlatform.UNIVERSAL) { + return true; + } + + // Not compatible when extension target platform is unknown + if (extensionTargetPlatform === TargetPlatform.UNKNOWN) { + return false; + } + + // Compatible when extension and product target platforms matches + if (extensionTargetPlatform === productTargetPlatform) { + return true; + } + + // Fallback + const fallbackTargetPlatforms = getFallbackTargetPlarforms(productTargetPlatform); + return fallbackTargetPlatforms.includes(extensionTargetPlatform); +} + +export function getFallbackTargetPlarforms(targetPlatform: TargetPlatform): TargetPlatform[] { + switch (targetPlatform) { + case TargetPlatform.WIN32_X64: return [TargetPlatform.WIN32_IA32]; + case TargetPlatform.WIN32_ARM64: return [TargetPlatform.WIN32_IA32]; + } + return []; +} + export interface IGalleryExtensionProperties { dependencies?: string[]; extensionPack?: string[]; @@ -23,6 +185,7 @@ export interface IGalleryExtensionProperties { // {{SQL CARBON EDIT}} azDataEngine?: string; localizedLanguages?: string[]; + targetPlatform: TargetPlatform; } export interface IGalleryExtensionAsset { @@ -84,6 +247,7 @@ export interface IGalleryExtension { publisherId: string; publisher: string; publisherDisplayName: string; + publisherDomain?: { link: string, verified: boolean }; description: string; installCount: number; rating: number; @@ -92,11 +256,11 @@ export interface IGalleryExtension { tags: readonly string[]; releaseDate: number; lastUpdated: number; + preview: boolean; + allTargetPlatforms: TargetPlatform[]; assets: IGalleryExtensionAssets; properties: IGalleryExtensionProperties; telemetryData: any; - preview: boolean; - webExtension: boolean; } export interface IGalleryMetadata { @@ -118,7 +282,7 @@ export const enum SortBy { Title = 2, PublisherName = 3, InstallCount = 4, - PublishedDate = 5, + PublishedDate = 10, AverageRating = 6, WeightedRating = 12 } @@ -170,17 +334,18 @@ export interface IExtensionGalleryService { isEnabled(): boolean; query(token: CancellationToken): Promise>; query(options: IQueryOptions, token: CancellationToken): Promise>; - getExtensions(ids: string[], token: CancellationToken): Promise; + getExtensions(identifiers: ReadonlyArray, token: CancellationToken): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise; getReadme(extension: IGalleryExtension, token: CancellationToken): Promise; getManifest(extension: IGalleryExtension, token: CancellationToken): Promise; getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise; getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise; - getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise; getExtensionsReport(): Promise; - getCompatibleExtension(extension: IGalleryExtension): Promise; - getCompatibleExtension(id: IExtensionIdentifier, version?: string): Promise; + isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise; + getCompatibleExtension(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise; + getCompatibleExtension(id: IExtensionIdentifier, targetPlatform: TargetPlatform): Promise; + getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise; } export interface InstallExtensionEvent { @@ -200,19 +365,29 @@ export interface DidUninstallExtensionEvent { error?: string; } -export const INSTALL_ERROR_NOT_SUPPORTED = 'notsupported'; -export const INSTALL_ERROR_MALICIOUS = 'malicious'; -export const INSTALL_ERROR_INCOMPATIBLE = 'incompatible'; +export enum ExtensionManagementErrorCode { + Unsupported = 'Unsupported', + Malicious = 'Malicious', + Incompatible = 'Incompatible', + Invalid = 'Invalid', + Download = 'Download', + Extract = 'Extract', + Delete = 'Delete', + Rename = 'Rename', + CorruptZip = 'CorruptZip', + IncompleteZip = 'IncompleteZip', + Internal = 'Internal', +} export class ExtensionManagementError extends Error { - constructor(message: string, readonly code: string) { + constructor(message: string, readonly code: ExtensionManagementErrorCode) { super(message); this.name = code; } } -export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean }; -export type InstallVSIXOptions = InstallOptions & { installOnlyNewlyAddedFromExtensionPack?: boolean }; +export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean, installGivenVersion?: boolean }; +export type InstallVSIXOptions = Omit & { installOnlyNewlyAddedFromExtensionPack?: boolean }; export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean }; export interface IExtensionManagementParticipant { @@ -237,13 +412,14 @@ export interface IExtensionManagementService { installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; - getInstalled(type?: ExtensionType): Promise; + getInstalled(type?: ExtensionType, donotIgnoreInvalidExtensions?: boolean): Promise; getExtensionsReport(): Promise; updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise; updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise; registerParticipant(pariticipant: IExtensionManagementParticipant): void; + getTargetPlatform(): Promise; } export const DISABLED_EXTENSIONS_STORAGE_PATH = 'extensionsIdentifiers/disabled'; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts index 910706e6a3..ac010a8a0e 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts @@ -199,23 +199,11 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer } private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { - const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); - const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); - const galleryExtensions = new Map(); - await Promise.all([ - (async () => { - const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); - result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); - })(), - Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { - const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); - if (extension) { - galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); - } - })) - ]); - + const result = await this.extensionGalleryService.getExtensions(extensions, CancellationToken.None); + for (const extension of result) { + galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); + } return galleryExtensions; } @@ -241,7 +229,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer output.log(version ? localize('installing with version', "Installing extension '{0}' v{1}...", id, version) : localize('installing', "Installing extension '{0}'...", id)); } - await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); + await this.extensionManagementService.installFromGallery(galleryExtension, { ...installOptions, installGivenVersion: !!version }); output.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); return manifest; } catch (error) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index e311d230c0..8467265de7 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IReportedExtension, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IReportedExtension, isTargetPlatformCompatible, TargetPlatform, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI { @@ -64,6 +64,7 @@ export class ExtensionManagementChannel implements IServerChannel { case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer), args[1]); case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); + case 'getTargetPlatform': return this.service.getTargetPlatform(); case 'canInstall': return this.service.canInstall(args[0]); case 'installFromGallery': return this.service.installFromGallery(args[0], args[1]); case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), args[1]); @@ -112,6 +113,19 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt typeof (thing).scheme === 'string'; } + private _targetPlatformPromise: Promise | undefined; + getTargetPlatform(): Promise { + if (!this._targetPlatformPromise) { + this._targetPlatformPromise = this.channel.call('getTargetPlatform'); + } + return this._targetPlatformPromise; + } + + async canInstall(extension: IGalleryExtension): Promise { + const currentTargetPlatform = await this.getTargetPlatform(); + return extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform)); + } + zip(extension: ILocalExtension): Promise { return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(result))); } @@ -128,10 +142,6 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('getManifest', [vsix])); } - async canInstall(extension: IGalleryExtension): Promise { - return true; - } - installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { return Promise.resolve(this.channel.call('installFromGallery', [extension, installOptions])).then(local => transformIncomingExtension(local, null)); } diff --git a/src/vs/platform/extensionManagement/common/extensionTipsService.ts b/src/vs/platform/extensionManagement/common/extensionTipsService.ts index fb962e40a2..beb576934c 100644 --- a/src/vs/platform/extensionManagement/common/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/common/extensionTipsService.ts @@ -54,6 +54,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe private async getValidConfigBasedTips(folder: URI): Promise { const result: IConfigBasedExtensionTip[] = []; for (const [configPath, tip] of this.allConfigBasedTips) { + if (tip.configScheme && tip.configScheme !== folder.scheme) { + continue; + } try { const content = await this.fileService.readFile(joinPath(folder, configPath)); const recommendationByRemote: Map = new Map(); diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 2f3988c435..aec5f8eba2 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { Promises as FSPromises } from 'vs/base/node/pfs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IGalleryExtension, InstallOperation, TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionIdentifierWithVersion, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; @@ -72,7 +72,8 @@ export class ExtensionsDownloader extends Disposable { } async delete(location: URI): Promise { - // noop as caching is enabled always + await this.cleanUpPromise; + await this.fileService.del(location); } private async rename(from: URI, to: URI, retryUntil: number): Promise { @@ -123,7 +124,7 @@ export class ExtensionsDownloader extends Disposable { } private getName(extension: IGalleryExtension): string { - return this.cache ? new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key().toLowerCase() : generateUuid(); + return this.cache ? `${new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key().toLowerCase()}${extension.properties.targetPlatform !== TargetPlatform.UNDEFINED ? `-${extension.properties.targetPlatform}` : ''}` : generateUuid(); } private parse(name: string): ExtensionIdentifierWithVersion | null { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 53331a7ad6..238d17b2da 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -6,9 +6,11 @@ import { extensionsWorkbenchServiceIncompatible } from 'sql/base/common/locConstants'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { getErrorMessage } from 'vs/base/common/errors'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isLinux, isMacintosh, platform } from 'vs/base/common/platform'; +import { arch } from 'vs/base/common/process'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; import { URI } from 'vs/base/common/uri'; @@ -18,11 +20,10 @@ import { IFile, zip } from 'vs/base/node/zip'; import * as nls from 'vs/nls'; import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, INSTALL_ERROR_VALIDATING, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { - ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions, - InstallVSIXOptions, - INSTALL_ERROR_INCOMPATIBLE + ExtensionManagementError, ExtensionManagementErrorCode, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions, + InstallVSIXOptions, TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; @@ -34,14 +35,11 @@ import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensio import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IFileService } from 'vs/platform/files/common/files'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled'; -const INSTALL_ERROR_DOWNLOADING = 'downloading'; - interface InstallableExtension { zipPath: string; identifierWithVersion: ExtensionIdentifierWithVersion; @@ -59,11 +57,12 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @optional(IDownloadService) private downloadService: IDownloadService, + @IDownloadService private downloadService: IDownloadService, @IInstantiationService instantiationService: IInstantiationService, - @IFileService fileService: IFileService, + @IFileService private readonly fileService: IFileService, + @IProductService productService: IProductService ) { - super(galleryService, telemetryService, logService); + super(galleryService, telemetryService, logService, productService); const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle)); this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension))); this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this)); @@ -78,6 +77,39 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi })); } + private _targetPlatformPromise: Promise | undefined; + getTargetPlatform(): Promise { + if (!this._targetPlatformPromise) { + this._targetPlatformPromise = (async () => { + const isAlpineLinux = await this.isAlpineLinux(); + const targetPlatform = getTargetPlatform(isAlpineLinux ? 'alpine' : platform, arch); + this.logService.debug('ExtensionManagementService#TargetPlatform:', targetPlatform); + return targetPlatform; + })(); + } + return this._targetPlatformPromise; + } + + private async isAlpineLinux(): Promise { + if (!isLinux) { + return false; + } + let content: string | undefined; + try { + const fileContent = await this.fileService.readFile(URI.file('/etc/os-release')); + content = fileContent.value.toString(); + } catch (error) { + try { + const fileContent = await this.fileService.readFile(URI.file('/usr/lib/os-release')); + content = fileContent.value.toString(); + } catch (error) { + /* Ignore */ + this.logService.debug(`Error while getting the os-release file.`, getErrorMessage(error)); + } + } + return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine'; + } + async zip(extension: ILocalExtension): Promise { this.logService.trace('ExtensionManagementService#zip', extension.identifier.id); const files = await this.collectFiles(extension); @@ -101,10 +133,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.scanExtensions(type); } - async canInstall(extension: IGalleryExtension): Promise { - return true; - } - async install(vsix: URI, options: InstallVSIXOptions = {}): Promise { this.logService.trace('ExtensionManagementService#install', vsix.toString()); @@ -112,15 +140,15 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const manifest = await getManifest(path.resolve(downloadLocation.fsPath)); // {{SQL CARBON EDIT}} Do our own engine checks const id = getGalleryExtensionId(manifest.publisher, manifest.name); - 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}'.", id, product.vscodeVersion)); + if (manifest.engines?.vscode && !isEngineValid(manifest.engines.vscode, this.productService.vscodeVersion, this.productService.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}'.", id, this.productService.vscodeVersion)); } - if (manifest.engines?.azdata && !isEngineValid(manifest.engines.azdata, product.version, product.date)) { - throw new ExtensionManagementError(extensionsWorkbenchServiceIncompatible(id, manifest.version, product.version, manifest.engines.azdata), INSTALL_ERROR_INCOMPATIBLE); + if (manifest.engines?.azdata && !isEngineValid(manifest.engines.azdata, this.productService.version, this.productService.date)) { + throw new ExtensionManagementError(extensionsWorkbenchServiceIncompatible(id, manifest.version, this.productService.version, manifest.engines.azdata), ExtensionManagementErrorCode.Incompatible); } /* - if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, product.version, product.date)) { - throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", getGalleryExtensionId(manifest.publisher, manifest.name), product.version)); + if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, this.productService.version, this.productService.date)) { + throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", getGalleryExtensionId(manifest.publisher, manifest.name), this.productService.version)); } */ @@ -149,10 +177,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi if (vsix.scheme === Schemas.file) { return vsix; } - if (!this.downloadService) { - throw new Error('Download service is not available'); - } - const downloadedLocation = joinPath(this.environmentService.tmpDir, generateUuid()); await this.downloadService.download(vsix, downloadedLocation); return downloadedLocation; @@ -215,9 +239,9 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask { - let local = await this.extensionsScanner.extractUserExtension(identifierWithVersion, zipPath, token); + let local = await this.extensionsScanner.extractUserExtension(identifierWithVersion, zipPath, metadata, token); this.logService.info('Extracting completed.', identifierWithVersion.id); - if (metadata) { - local = await this.extensionsScanner.saveMetadataForLocalExtension(local, metadata); - } return local; } @@ -276,12 +297,25 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask { installableExtension.metadata.isMachineScoped = this.options.isMachineScoped || existingExtension?.isMachineScoped; installableExtension.metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin; - const local = await this.installExtension(installableExtension, token); - if (existingExtension && semver.neq(existingExtension.manifest.version, this.gallery.version)) { - await this.extensionsScanner.setUninstalled(existingExtension); + try { + const local = await this.installExtension(installableExtension, token); + if (existingExtension && semver.neq(existingExtension.manifest.version, this.gallery.version)) { + await this.extensionsScanner.setUninstalled(existingExtension); + } + return local; + } catch (error) { + await this.deleteDownloadedVSIX(installableExtension.zipPath); + throw error; + } + } + + private async deleteDownloadedVSIX(vsix: string): Promise { + try { + await this.extensionsDownloader.delete(URI.file(vsix)); + } catch (error) { + /* Ignore */ + this.logService.warn('Error while deleting the downloaded vsix', vsix.toString(), getErrorMessage(error)); } - try { await this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)); } catch (error) { /* Ignore */ } - return local; } private async downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): Promise> { @@ -297,14 +331,15 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask { zipPath = (await this.extensionsDownloader.downloadExtension(extension, operation)).fsPath; this.logService.info('Downloaded extension:', extension.identifier.id, zipPath); } catch (error) { - throw new ExtensionManagementError(joinErrors(error).message, INSTALL_ERROR_DOWNLOADING); + throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Download); } try { const manifest = await getManifest(zipPath); return (>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata }); } catch (error) { - throw new ExtensionManagementError(joinErrors(error).message, INSTALL_ERROR_VALIDATING); + await this.deleteDownloadedVSIX(zipPath); + throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Invalid); } } } @@ -327,10 +362,10 @@ class InstallVSIXTask extends AbstractInstallExtensionTask { const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User); const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier)); const metadata = await this.getMetadata(this.identifier.id, token); + metadata.isMachineScoped = this.options.isMachineScoped || existing?.isMachineScoped; + metadata.isBuiltin = this.options.isBuiltin || existing?.isBuiltin; if (existing) { - metadata.isMachineScoped = this.options.isMachineScoped || existing.isMachineScoped; - metadata.isBuiltin = this.options.isBuiltin || existing.isBuiltin; this._operation = InstallOperation.Update; if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) { try { diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index 84e3be06b2..954377378f 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -19,7 +19,7 @@ import * as pfs from 'vs/base/node/pfs'; import { extract, ExtractError } from 'vs/base/node/zip'; import { localize } from 'vs/nls'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionManagementError, IGalleryMetadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementError, ExtensionManagementErrorCode, IGalleryMetadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { ExtensionType, IExtensionIdentifier, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; @@ -28,12 +28,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { CancellationToken } from 'vscode'; -const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem'; -const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser'; -const INSTALL_ERROR_EXTRACTING = 'extracting'; -const INSTALL_ERROR_DELETING = 'deleting'; -const INSTALL_ERROR_RENAMING = 'renaming'; - export type IMetadata = Partial; type IStoredMetadata = IMetadata & { installedTimestamp: number | undefined }; export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata }; @@ -69,11 +63,11 @@ export class ExtensionsScanner extends Disposable { const promises: Promise[] = []; if (type === null || type === ExtensionType.System) { - promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_SYS_EXTENSIONS)))); + promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Internal)))); } if (type === null || type === ExtensionType.User) { - promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_USER_EXTENSIONS)))); + promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Internal)))); } try { @@ -100,7 +94,7 @@ export class ExtensionsScanner extends Disposable { return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User); } - async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise { + async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, token: CancellationToken): Promise { const folderName = identifierWithVersion.key(); const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`); const extensionPath = path.join(this.extensionsPath, folderName); @@ -111,7 +105,7 @@ export class ExtensionsScanner extends Disposable { try { 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); + 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), ExtensionManagementErrorCode.Delete); } await this.extractAtLocation(identifierWithVersion, zipPath, tempPath, token); @@ -119,7 +113,7 @@ export class ExtensionsScanner extends Disposable { if (!local) { throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath)); } - await this.storeMetadata(local, { installedTimestamp: Date.now() }); + await this.storeMetadata(local, { ...metadata, installedTimestamp: Date.now() }); try { await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); @@ -236,7 +230,7 @@ export class ExtensionsScanner extends Disposable { try { await pfs.Promises.rm(location); } catch (e) { - throw new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING); + throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete); } try { @@ -244,7 +238,15 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Extracted extension to ${location}:`, identifier.id); } catch (e) { try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ } - throw new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING); + let errorCode = ExtensionManagementErrorCode.Extract; + if (e instanceof ExtractError) { + if (e.type === 'CorruptZip') { + errorCode = ExtensionManagementErrorCode.CorruptZip; + } else if (e.type === 'Incomplete') { + errorCode = ExtensionManagementErrorCode.IncompleteZip; + } + } + throw new ExtensionManagementError(e.message, errorCode); } } @@ -256,7 +258,7 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); return this.rename(identifier, extractPath, renamePath, retryUntil); } - throw new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING); + throw new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); } } diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index 826a091410..3fd081630d 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -9,26 +9,32 @@ import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { isUUID } from 'vs/base/common/uuid'; import { mock } from 'vs/base/test/common/mock'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { IRawGalleryExtensionVersion, resolveMarketplaceHeaders, sortExtensionVersions } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { NullLogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; +import { TelemetryConfiguration, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; class EnvironmentServiceMock extends mock() { override readonly serviceMachineIdResource: URI; constructor(serviceMachineIdResource: URI) { super(); this.serviceMachineIdResource = serviceMachineIdResource; + this.isBuilt = true; } } suite('Extension Gallery Service', () => { const disposables: DisposableStore = new DisposableStore(); - let fileService: IFileService, environmentService: IEnvironmentService, storageService: IStorageService; + let fileService: IFileService, environmentService: IEnvironmentService, storageService: IStorageService, productService: IProductService, configurationService: IConfigurationService; setup(() => { const serviceMachineIdResource = joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'machineid'); @@ -37,14 +43,112 @@ suite('Extension Gallery Service', () => { const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); fileService.registerProvider(serviceMachineIdResource.scheme, fileSystemProvider); storageService = new InMemoryStorageService(); + configurationService = new TestConfigurationService({ [TELEMETRY_SETTING_ID]: TelemetryConfiguration.ON }); + configurationService.updateValue(TELEMETRY_SETTING_ID, TelemetryConfiguration.ON); + productService = { _serviceBrand: undefined, ...product, enableTelemetry: true }; }); teardown(() => disposables.clear()); test('marketplace machine id', async () => { - const headers = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService); + const headers = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService); assert.ok(isUUID(headers['X-Market-User-Id'])); - const headers2 = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService); + const headers2 = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService); assert.strictEqual(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); }); + + test('sorting single extension version without target platform', async () => { + const actual = [aExtensionVersion('1.1.2')]; + const expected = [...actual]; + sortExtensionVersions(actual, TargetPlatform.DARWIN_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting single extension version with preferred target platform', async () => { + const actual = [aExtensionVersion('1.1.2', TargetPlatform.DARWIN_X64)]; + const expected = [...actual]; + sortExtensionVersions(actual, TargetPlatform.DARWIN_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting single extension version with fallback target platform', async () => { + const actual = [aExtensionVersion('1.1.2', TargetPlatform.WIN32_IA32)]; + const expected = [...actual]; + sortExtensionVersions(actual, TargetPlatform.WIN32_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting single extension version with not compatible target platform', async () => { + const actual = [aExtensionVersion('1.1.2', TargetPlatform.DARWIN_ARM64)]; + const expected = [...actual]; + sortExtensionVersions(actual, TargetPlatform.WIN32_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting single extension version with multiple target platforms and preferred at first', async () => { + const actual = [aExtensionVersion('1.1.2', TargetPlatform.WIN32_X64), aExtensionVersion('1.1.2', TargetPlatform.WIN32_IA32), aExtensionVersion('1.1.2')]; + const expected = [...actual]; + sortExtensionVersions(actual, TargetPlatform.WIN32_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting single extension version with multiple target platforms and preferred at first with no fallbacks', async () => { + const actual = [aExtensionVersion('1.1.2', TargetPlatform.DARWIN_X64), aExtensionVersion('1.1.2'), aExtensionVersion('1.1.2', TargetPlatform.WIN32_IA32)]; + const expected = [...actual]; + sortExtensionVersions(actual, TargetPlatform.DARWIN_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting single extension version with multiple target platforms and preferred at first and fallback at last', async () => { + const actual = [aExtensionVersion('1.1.2', TargetPlatform.WIN32_X64), aExtensionVersion('1.1.2'), aExtensionVersion('1.1.2', TargetPlatform.WIN32_IA32)]; + const expected = [actual[0], actual[2], actual[1]]; + sortExtensionVersions(actual, TargetPlatform.WIN32_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting single extension version with multiple target platforms and preferred is not first', async () => { + const actual = [aExtensionVersion('1.1.2', TargetPlatform.WIN32_IA32), aExtensionVersion('1.1.2', TargetPlatform.WIN32_X64), aExtensionVersion('1.1.2')]; + const expected = [actual[1], actual[0], actual[2]]; + sortExtensionVersions(actual, TargetPlatform.WIN32_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting single extension version with multiple target platforms and preferred is at the end', async () => { + const actual = [aExtensionVersion('1.1.2', TargetPlatform.WIN32_IA32), aExtensionVersion('1.1.2'), aExtensionVersion('1.1.2', TargetPlatform.WIN32_X64)]; + const expected = [actual[2], actual[0], actual[1]]; + sortExtensionVersions(actual, TargetPlatform.WIN32_X64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting multiple extension versions without target platforms', async () => { + const actual = [aExtensionVersion('1.2.4'), aExtensionVersion('1.1.3'), aExtensionVersion('1.1.2'), aExtensionVersion('1.1.1')]; + const expected = [...actual]; + sortExtensionVersions(actual, TargetPlatform.WIN32_ARM64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting multiple extension versions with target platforms - 1', async () => { + const actual = [aExtensionVersion('1.2.4', TargetPlatform.DARWIN_ARM64), aExtensionVersion('1.2.4', TargetPlatform.WIN32_ARM64), aExtensionVersion('1.2.4', TargetPlatform.LINUX_ARM64), aExtensionVersion('1.1.3'), aExtensionVersion('1.1.2'), aExtensionVersion('1.1.1')]; + const expected = [actual[1], actual[0], actual[2], actual[3], actual[4], actual[5]]; + sortExtensionVersions(actual, TargetPlatform.WIN32_ARM64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting multiple extension versions with target platforms - 2', async () => { + const actual = [aExtensionVersion('1.2.4'), aExtensionVersion('1.2.3', TargetPlatform.DARWIN_ARM64), aExtensionVersion('1.2.3', TargetPlatform.WIN32_ARM64), aExtensionVersion('1.2.3', TargetPlatform.LINUX_ARM64), aExtensionVersion('1.1.2'), aExtensionVersion('1.1.1')]; + const expected = [actual[0], actual[3], actual[1], actual[2], actual[4], actual[5]]; + sortExtensionVersions(actual, TargetPlatform.LINUX_ARM64); + assert.deepStrictEqual(actual, expected); + }); + + test('sorting multiple extension versions with target platforms - 3', async () => { + const actual = [aExtensionVersion('1.2.4'), aExtensionVersion('1.1.2'), aExtensionVersion('1.1.1'), aExtensionVersion('1.0.0', TargetPlatform.DARWIN_ARM64), aExtensionVersion('1.0.0', TargetPlatform.WIN32_IA32), aExtensionVersion('1.0.0', TargetPlatform.WIN32_ARM64)]; + const expected = [actual[0], actual[1], actual[2], actual[5], actual[4], actual[3]]; + sortExtensionVersions(actual, TargetPlatform.WIN32_ARM64); + assert.deepStrictEqual(actual, expected); + }); + + function aExtensionVersion(version: string, targetPlatform?: TargetPlatform): IRawGalleryExtensionVersion { + return { version, targetPlatform } as IRawGalleryExtensionVersion; + } }); diff --git a/src/vs/platform/extensions/common/extensionHostStarter.ts b/src/vs/platform/extensions/common/extensionHostStarter.ts new file mode 100644 index 0000000000..0f1c356bb6 --- /dev/null +++ b/src/vs/platform/extensions/common/extensionHostStarter.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SerializedError } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IExtensionHostStarter = createDecorator('extensionHostStarter'); + +export const ipcExtensionHostStarterChannelName = 'extensionHostStarter'; + +export interface IExtensionHostProcessOptions { + env: { [key: string]: string | undefined; }; + detached: boolean; + execArgv: string[] | undefined; + silent: boolean; +} + +export interface IExtensionHostStarter { + readonly _serviceBrand: undefined; + + onDynamicStdout(id: string): Event; + onDynamicStderr(id: string): Event; + onDynamicMessage(id: string): Event; + onDynamicError(id: string): Event<{ error: SerializedError; }>; + onDynamicExit(id: string): Event<{ code: number; signal: string }>; + + createExtensionHost(): Promise<{ id: string; }>; + start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number; }>; + enableInspectPort(id: string): Promise; + kill(id: string): Promise; + +} diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index cb8039e676..d5de3ab70a 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -7,6 +7,7 @@ import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILocalization } from 'vs/platform/localizations/common/localizations'; +import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; export const MANIFEST_CACHE_FOLDER = 'CachedExtensions'; export const USER_MANIFEST_CACHE_FILE = 'user'; @@ -32,6 +33,9 @@ export interface IConfigurationProperty { } export interface IConfiguration { + id?: string, + order?: number, + title?: string, properties: { [key: string]: IConfigurationProperty; }; } @@ -125,8 +129,9 @@ export interface IWalkthroughStep { readonly title: string; readonly description: string | undefined; readonly media: - | { image: string | { dark: string, light: string, hc: string }, altText: string, markdown?: never } - | { markdown: string, image?: never } + | { image: string | { dark: string, light: string, hc: string }, altText: string, markdown?: never, svg?: never } + | { markdown: string, image?: never, svg?: never } + | { svg: string, altText: string, markdown?: never, image?: never } readonly completionEvents?: string[]; /** @deprecated use `completionEvents: 'onCommand:...'` */ readonly doneOn?: { command: string }; @@ -350,10 +355,18 @@ export function isLanguagePackExtension(manifest: IExtensionManifest): boolean { return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false; } -export function isAuthenticaionProviderExtension(manifest: IExtensionManifest): boolean { +export function isAuthenticationProviderExtension(manifest: IExtensionManifest): boolean { return manifest.contributes && manifest.contributes.authentication ? manifest.contributes.authentication.length > 0 : false; } +export function isResolverExtension(manifest: IExtensionManifest, remoteAuthority: string | undefined): boolean { + if (remoteAuthority && manifest.enableProposedApi) { + const activationEvent = `onResolveRemoteAuthority:${getRemoteName(remoteAuthority)}`; + return manifest.activationEvents?.indexOf(activationEvent) !== -1; + } + return false; +} + export const IBuiltinExtensionsScannerService = createDecorator('IBuiltinExtensionsScannerService'); export interface IBuiltinExtensionsScannerService { readonly _serviceBrand: undefined; diff --git a/src/vs/platform/extensions/node/extensionHostStarter.ts b/src/vs/platform/extensions/node/extensionHostStarter.ts new file mode 100644 index 0000000000..1325df8761 --- /dev/null +++ b/src/vs/platform/extensions/node/extensionHostStarter.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 { SerializedError, transformErrorForSerialization } from 'vs/base/common/errors'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ChildProcess, fork } from 'child_process'; +import { FileAccess } from 'vs/base/common/network'; +import { StringDecoder } from 'string_decoder'; +import * as platform from 'vs/base/common/platform'; +import { ILogService } from 'vs/platform/log/common/log'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { mixin } from 'vs/base/common/objects'; +import { cwd } from 'vs/base/common/process'; + +class ExtensionHostProcess extends Disposable { + + readonly _onStdout = this._register(new Emitter()); + readonly onStdout = this._onStdout.event; + + readonly _onStderr = this._register(new Emitter()); + readonly onStderr = this._onStderr.event; + + readonly _onMessage = this._register(new Emitter()); + readonly onMessage = this._onMessage.event; + + readonly _onError = this._register(new Emitter<{ error: SerializedError; }>()); + readonly onError = this._onError.event; + + readonly _onExit = this._register(new Emitter<{ pid: number; code: number; signal: string }>()); + readonly onExit = this._onExit.event; + + private _process: ChildProcess | null = null; + + constructor( + public readonly id: string, + @ILogService private readonly _logService: ILogService + ) { + super(); + } + + register(disposable: IDisposable) { + this._register(disposable); + } + + start(opts: IExtensionHostProcessOptions): { pid: number; } { + this._process = fork( + FileAccess.asFileUri('bootstrap-fork', require).fsPath, + ['--type=extensionHost', '--skipWorkspaceStorageLock'], + mixin({ cwd: cwd() }, opts), + ); + const pid = this._process.pid; + + this._logService.info(`Starting extension host with pid ${pid}.`); + + const stdoutDecoder = new StringDecoder('utf-8'); + this._process.stdout?.on('data', (chunk) => { + const strChunk = typeof chunk === 'string' ? chunk : stdoutDecoder.write(chunk); + this._onStdout.fire(strChunk); + }); + + const stderrDecoder = new StringDecoder('utf-8'); + this._process.stderr?.on('data', (chunk) => { + const strChunk = typeof chunk === 'string' ? chunk : stderrDecoder.write(chunk); + this._onStderr.fire(strChunk); + }); + + this._process.on('message', msg => { + this._onMessage.fire(msg); + }); + + this._process.on('error', (err) => { + this._onError.fire({ error: transformErrorForSerialization(err) }); + }); + + this._process.on('exit', (code: number, signal: string) => { + this._onExit.fire({ pid, code, signal }); + }); + + return { pid }; + } + + enableInspectPort(): boolean { + if (!this._process) { + return false; + } + + this._logService.info(`Enabling inspect port on extension host with pid ${this._process.pid}.`); + + interface ProcessExt { + _debugProcess?(n: number): any; + } + + if (typeof (process)._debugProcess === 'function') { + // use (undocumented) _debugProcess feature of node + (process)._debugProcess!(this._process.pid); + return true; + } else if (!platform.isWindows) { + // use KILL USR1 on non-windows platforms (fallback) + this._process.kill('SIGUSR1'); + return true; + } else { + // not supported... + return false; + } + } + + kill(): void { + if (!this._process) { + return; + } + this._logService.info(`Killing extension host with pid ${this._process.pid}.`); + this._process.kill(); + } +} + +export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter { + _serviceBrand: undefined; + + private static _lastId: number = 0; + + private readonly _extHosts: Map; + + constructor( + @ILogService private readonly _logService: ILogService + ) { + this._extHosts = new Map(); + } + + dispose(): void { + // Intentionally not killing the extension host processes + } + + private _getExtHost(id: string): ExtensionHostProcess { + const extHostProcess = this._extHosts.get(id); + if (!extHostProcess) { + throw new Error(`Unknown extension host!`); + } + return extHostProcess; + } + + onDynamicStdout(id: string): Event { + return this._getExtHost(id).onStdout; + } + + onDynamicStderr(id: string): Event { + return this._getExtHost(id).onStderr; + } + + onDynamicMessage(id: string): Event { + return this._getExtHost(id).onMessage; + } + + onDynamicError(id: string): Event<{ error: SerializedError; }> { + return this._getExtHost(id).onError; + } + + onDynamicExit(id: string): Event<{ code: number; signal: string; }> { + return this._getExtHost(id).onExit; + } + + async createExtensionHost(): Promise<{ id: string; }> { + const id = String(++ExtensionHostStarter._lastId); + const extHost = new ExtensionHostProcess(id, this._logService); + this._extHosts.set(id, extHost); + extHost.onExit(({ pid, code, signal }) => { + this._logService.info(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`); + setTimeout(() => { + extHost.dispose(); + this._extHosts.delete(id); + }); + }); + return { id }; + } + + async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number; }> { + return this._getExtHost(id).start(opts); + } + + async enableInspectPort(id: string): Promise { + const extHostProcess = this._extHosts.get(id); + if (!extHostProcess) { + return false; + } + return extHostProcess.enableInspectPort(); + } + + async kill(id: string): Promise { + const extHostProcess = this._extHosts.get(id); + if (!extHostProcess) { + // already gone! + return; + } + extHostProcess.kill(); + } +} + +registerSingleton(IExtensionHostStarter, ExtensionHostStarter, true); diff --git a/src/vs/platform/externalTerminal/node/externalTerminalService.ts b/src/vs/platform/externalTerminal/node/externalTerminalService.ts index 56342e1d99..7a83267cfa 100644 --- a/src/vs/platform/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/platform/externalTerminal/node/externalTerminalService.ts @@ -273,9 +273,11 @@ export class LinuxExternalTerminalService extends ExternalTerminalService implem 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.Promises.exists('/etc/debian_version'); + if (!env.isLinux) { + LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = Promise.resolve('xterm'); + } else { + const isDebian = await pfs.Promises.exists('/etc/debian_version'); + LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(r => { if (isDebian) { r('x-terminal-emulator'); } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { @@ -289,10 +291,8 @@ export class LinuxExternalTerminalService extends ExternalTerminalService implem } else { r('xterm'); } - } else { - r('xterm'); - } - }); + }); + } } return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY; } diff --git a/src/vs/platform/files/browser/htmlFileSystemProvider.ts b/src/vs/platform/files/browser/htmlFileSystemProvider.ts index 3cc82b49f7..426831dadc 100644 --- a/src/vs/platform/files/browser/htmlFileSystemProvider.ts +++ b/src/vs/platform/files/browser/htmlFileSystemProvider.ts @@ -14,7 +14,6 @@ import { normalize } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; import { extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; -import { generateUuid } from 'vs/base/common/uuid'; import { createFileSystemProviderError, FileDeleteOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability { @@ -52,7 +51,7 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr async stat(resource: URI): Promise { try { - const handle = await this.getHandle(resource); + const handle: any = await this.getHandle(resource); if (!handle) { throw this.createFileSystemProviderError(resource, 'No such file or directory, stat', FileSystemProviderErrorCode.FileNotFound); } @@ -81,7 +80,7 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr async readdir(resource: URI): Promise<[string, FileType][]> { try { - const handle = await this.getDirectoryHandle(resource); + const handle: any = await this.getDirectoryHandle(resource); if (!handle) { throw this.createFileSystemProviderError(resource, 'No such file or directory, readdir', FileSystemProviderErrorCode.FileNotFound); } @@ -182,7 +181,7 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { try { - let handle = await this.getFileHandle(resource); + let handle: any = await this.getFileHandle(resource); // Validate target unless { create: true, overwrite: true } if (!opts.create || !opts.overwrite) { @@ -290,24 +289,30 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr private readonly directories = new Map(); registerFileHandle(handle: FileSystemFileHandle): URI { - const handleId = generateUuid(); - this.files.set(handleId, handle); - - return this.toHandleUri(handle, handleId); + return this.registerHandle(handle, this.files); } registerDirectoryHandle(handle: FileSystemDirectoryHandle): URI { - const handleId = generateUuid(); - this.directories.set(handleId, handle); - - return this.toHandleUri(handle, handleId); + return this.registerHandle(handle, this.directories); } - private toHandleUri(handle: FileSystemHandle, handleId: string): URI { - return URI.from({ scheme: Schemas.file, path: `/${handle.name}`, query: handleId }); + private registerHandle(handle: FileSystemHandle, map: Map): URI { + let handleId = `/${handle.name}`; + + // Compute a valid handle ID in case this exists already + if (map.has(handleId)) { + let handleIdCounter = 2; + do { + handleId = `/${handle.name}-${handleIdCounter++}`; + } while (map.has(handleId)); + } + + map.set(handleId, handle); + + return URI.from({ scheme: Schemas.file, path: handleId }); } - private async getHandle(resource: URI): Promise { + async getHandle(resource: URI): Promise { // First: try to find a well known handle first let handle = this.getHandleSync(resource); @@ -340,9 +345,9 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr return undefined; } - const handleId = resource.query; + const handleId = resource.path.replace(/\/$/, ''); // remove potential slash from the end of the path + const handle = this.files.get(handleId) ?? this.directories.get(handleId); - const handle = this.files.get(handleId) || this.directories.get(handleId); if (!handle) { throw this.createFileSystemProviderError(resource, 'No file system handle registered', FileSystemProviderErrorCode.Unavailable); } diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index 73b403f67f..b108782ba6 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -5,9 +5,12 @@ import { Throttler } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; +import { getErrorMessage } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { mark } from 'vs/base/common/performance'; import { joinPath } from 'vs/base/common/resources'; +import { isString } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { createFileSystemProviderError, FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; @@ -25,12 +28,20 @@ const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty' // Arbitrary Internal Errors (should never be thrown in production) const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occurred in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); +class MissingStoresError extends Error { + constructor(readonly db: IDBDatabase) { + super('Missing stores'); + } +} + export class IndexedDB { private indexedDBPromise: Promise; constructor() { + mark('code/willOpenWebDB'); this.indexedDBPromise = this.openIndexedDB(INDEXEDDB_VSCODE_DB, 2, [INDEXEDDB_USERDATA_OBJECT_STORE, INDEXEDDB_LOGS_OBJECT_STORE]); + this.indexedDBPromise.finally(() => mark('code/didOpenWebDB')); } async createFileSystemProvider(scheme: string, store: string, watchCrossWindowChanges: boolean): Promise { @@ -46,16 +57,38 @@ export class IndexedDB { return fsp; } - private openIndexedDB(name: string, version: number, stores: string[]): Promise { + private async openIndexedDB(name: string, version: number, stores: string[]): Promise { + try { + return await this.createIndexedDB(name, version, stores); + } catch (err) { + if (err instanceof MissingStoresError) { + console.info(`Attempting to recreate the indexedDB once.`, name); + + try { + // Try to delete the db + await this.deleteIndexedDB(err.db); + } catch (error) { + console.error(`Error while deleting the indexedDB`, getErrorMessage(error)); + throw error; + } + + return await this.createIndexedDB(name, version, stores); + } + + throw err; + } + } + + private createIndexedDB(name: string, version: number, stores: string[]): Promise { return new Promise((c, e) => { const request = window.indexedDB.open(name, version); - request.onerror = (err) => e(request.error); + request.onerror = () => e(request.error); request.onsuccess = () => { const db = request.result; for (const store of stores) { if (!db.objectStoreNames.contains(store)) { - console.error(`Error while creating indexedDB. Could not create ${store} object store`); - c(null); + console.error(`Error while opening indexedDB. Could not find ${store} object store`); + e(new MissingStoresError(db)); return; } } @@ -71,6 +104,18 @@ export class IndexedDB { }; }); } + + private deleteIndexedDB(indexedDB: IDBDatabase): Promise { + return new Promise((c, e) => { + // Close any opened connections + indexedDB.close(); + + // Delete the db + const deleteRequest = window.indexedDB.deleteDatabase(indexedDB.name); + deleteRequest.onerror = (err) => e(deleteRequest.error); + deleteRequest.onsuccess = () => c(); + }); + } } export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability { @@ -210,6 +255,73 @@ type FileChangeDto = { readonly resource: UriComponents; }; +class IndexedDBChangesBroadcastChannel extends Disposable { + + private broadcastChannel: BroadcastChannel | undefined; + + private readonly _onDidFileChanges = this._register(new Emitter()); + readonly onDidFileChanges: Event = this._onDidFileChanges.event; + + constructor(private readonly changesKey: string) { + super(); + + // Use BroadcastChannel + if ('BroadcastChannel' in window) { + try { + this.broadcastChannel = new BroadcastChannel(changesKey); + const listener = (event: MessageEvent) => { + if (isString(event.data)) { + this.onDidReceiveChanges(event.data); + } + }; + this.broadcastChannel.addEventListener('message', listener); + this._register(toDisposable(() => { + if (this.broadcastChannel) { + this.broadcastChannel.removeEventListener('message', listener); + this.broadcastChannel.close(); + } + })); + } catch (error) { + console.warn('Error while creating broadcast channel. Falling back to localStorage.', getErrorMessage(error)); + this.createStorageBroadcastChannel(changesKey); + } + } + + // BroadcastChannel is not supported. Use storage. + else { + this.createStorageBroadcastChannel(changesKey); + } + } + + private createStorageBroadcastChannel(changesKey: string): void { + const listener = (event: StorageEvent) => { + if (event.key === changesKey && event.newValue) { + this.onDidReceiveChanges(event.newValue); + } + }; + window.addEventListener('storage', listener); + this._register(toDisposable(() => window.removeEventListener('storage', listener))); + } + + private onDidReceiveChanges(data: string): void { + try { + const changesDto: FileChangeDto[] = JSON.parse(data); + this._onDidFileChanges.fire(changesDto.map(c => ({ type: c.type, resource: URI.revive(c.resource) }))); + } catch (error) {/* ignore*/ } + } + + postChanges(changes: IFileChange[]): void { + if (this.broadcastChannel) { + this.broadcastChannel.postMessage(JSON.stringify(changes)); + } else { + // remove previous changes so that event is triggered even if new changes are same as old changes + window.localStorage.removeItem(this.changesKey); + window.localStorage.setItem(this.changesKey, JSON.stringify(changes)); + } + } + +} + class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities = @@ -217,7 +329,7 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy | FileSystemProviderCapabilities.PathCaseSensitive; readonly onDidChangeCapabilities: Event = Event.None; - private readonly changesKey: string; + private readonly changesBroadcastChannel: IndexedDBChangesBroadcastChannel | undefined; private readonly _onDidChangeFile = this._register(new Emitter()); readonly onDidChangeFile: Event = this._onDidChangeFile.event; @@ -226,24 +338,13 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy private cachedFiletree: Promise | undefined; private writeManyThrottler: Throttler; - constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string, private readonly watchCrossWindowChanges: boolean) { + constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string, watchCrossWindowChanges: boolean) { super(); this.writeManyThrottler = new Throttler(); - this.changesKey = `vscode.indexedDB.${scheme}.changes`; if (watchCrossWindowChanges) { - const storageListener = (event: StorageEvent) => this.onDidStorageChange(event); - window.addEventListener('storage', storageListener); - this._register(toDisposable(() => window.removeEventListener('storage', storageListener))); - } - } - - private onDidStorageChange(event: StorageEvent): void { - if (event.key === this.changesKey && event.newValue) { - try { - const changesDto: FileChangeDto[] = JSON.parse(event.newValue); - this._onDidChangeFile.fire(changesDto.map(c => ({ type: c.type, resource: URI.revive(c.resource) }))); - } catch (error) {/* ignore*/ } + this.changesBroadcastChannel = this._register(new IndexedDBChangesBroadcastChannel(`vscode.indexedDB.${scheme}.changes`)); + this._register(this.changesBroadcastChannel.onDidFileChanges(changes => this._onDidChangeFile.fire(changes))); } } @@ -396,10 +497,8 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy if (changes.length) { this._onDidChangeFile.fire(changes); - if (this.watchCrossWindowChanges) { - // remove previous changes so that event is triggered even if new changes are same as old changes - window.localStorage.removeItem(this.changesKey); - window.localStorage.setItem(this.changesKey, JSON.stringify(changes)); + if (this.changesBroadcastChannel) { + this.changesBroadcastChannel.postChanges(changes); } } } @@ -447,7 +546,7 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy } private deleteKeys(keys: string[]): Promise { - return new Promise(async (c, e) => { + return new Promise((c, e) => { if (keys.length === 0) { return c(); } @@ -463,7 +562,7 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy } reset(): Promise { - return new Promise(async (c, e) => { + return new Promise((c, e) => { const transaction = this.database.transaction([this.store], 'readwrite'); transaction.oncomplete = () => c(); transaction.onerror = () => e(transaction.error); diff --git a/src/vs/platform/files/common/diskFileSystemProvider.ts b/src/vs/platform/files/common/diskFileSystemProvider.ts new file mode 100644 index 0000000000..9756b175ed --- /dev/null +++ b/src/vs/platform/files/common/diskFileSystemProvider.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 { insert } from 'vs/base/common/arrays'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter } from 'vs/base/common/event'; +import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { normalize } from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { IFileChange, IWatchOptions } from 'vs/platform/files/common/files'; +import { IDiskFileChange, ILogMessage, IWatchRequest, toFileChanges, WatcherService } from 'vs/platform/files/common/watcher'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; + +export abstract class AbstractDiskFileSystemProvider extends Disposable { + + constructor( + protected readonly logService: ILogService + ) { + super(); + } + + //#region File Watching + + protected readonly _onDidErrorOccur = this._register(new Emitter()); + readonly onDidErrorOccur = this._onDidErrorOccur.event; + + protected readonly _onDidChangeFile = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChangeFile.event; + + private recursiveWatcher: WatcherService | undefined; + private readonly recursiveFoldersToWatch: IWatchRequest[] = []; + private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer(0)); + + watch(resource: URI, opts: IWatchOptions): IDisposable { + if (opts.recursive) { + return this.watchRecursive(resource, opts); + } + + return this.watchNonRecursive(resource); + } + + private watchRecursive(resource: URI, opts: IWatchOptions): IDisposable { + + // Add to list of folders to watch recursively + const folderToWatch: IWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes }; + const remove = insert(this.recursiveFoldersToWatch, folderToWatch); + + // Trigger update + this.refreshRecursiveWatchers(); + + return toDisposable(() => { + + // Remove from list of folders to watch recursively + remove(); + + // Trigger update + this.refreshRecursiveWatchers(); + }); + } + + private refreshRecursiveWatchers(): void { + + // Buffer requests for recursive watching to decide on right watcher + // that supports potentially watching more than one folder at once + this.recursiveWatchRequestDelayer.trigger(() => { + return this.doRefreshRecursiveWatchers(); + }).catch(error => onUnexpectedError(error)); + } + + private doRefreshRecursiveWatchers(): Promise { + + // Create watcher if this is the first time + if (!this.recursiveWatcher) { + this.recursiveWatcher = this._register(this.createRecursiveWatcher( + this.recursiveFoldersToWatch.length, + changes => this._onDidChangeFile.fire(toFileChanges(changes)), + msg => this.onWatcherLogMessage(msg), + this.logService.getLevel() === LogLevel.Trace + )); + + // Apply log levels dynamically + this._register(this.logService.onDidChangeLogLevel(() => { + this.recursiveWatcher?.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); + })); + } + + // Ask to watch the provided folders + return this.doWatch(this.recursiveWatcher, this.recursiveFoldersToWatch); + } + + protected doWatch(watcher: WatcherService, requests: IWatchRequest[]): Promise { + return watcher.watch(requests); + } + + protected abstract createRecursiveWatcher( + folders: number, + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ): WatcherService; + + private watchNonRecursive(resource: URI): IDisposable { + const watcherService = this.createNonRecursiveWatcher( + this.toFilePath(resource), + changes => this._onDidChangeFile.fire(toFileChanges(changes)), + msg => this.onWatcherLogMessage(msg), + this.logService.getLevel() === LogLevel.Trace + ); + + const logLevelListener = this.logService.onDidChangeLogLevel(() => { + watcherService.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); + }); + + return combinedDisposable(watcherService, logLevelListener); + } + + private onWatcherLogMessage(msg: ILogMessage): void { + if (msg.type === 'error') { + this._onDidErrorOccur.fire(msg.message); + } + + this.logService[msg.type](msg.message); + } + + protected abstract createNonRecursiveWatcher( + path: string, + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ): IDisposable & { setVerboseLogging: (verboseLogging: boolean) => void }; + + protected toFilePath(resource: URI): string { + return normalize(resource.fsPath); + } + + //#endregion +} diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 3cc2405f81..8c59a0e1e4 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -25,7 +25,10 @@ export class FileService extends Disposable implements IFileService { declare readonly _serviceBrand: undefined; - private readonly BUFFER_SIZE = 64 * 1024; + // Choose a buffer size that is a balance between memory needs and + // manageable IPC overhead. The larger the buffer size, the less + // roundtrips we have to do for reading/writing data. + private readonly BUFFER_SIZE = 256 * 1024; constructor(@ILogService private readonly logService: ILogService) { super(); @@ -96,7 +99,15 @@ export class FileService extends Disposable implements IFileService { await Promises.settled(joiners); } - canHandleResource(resource: URI): boolean { + async canHandleResource(resource: URI): Promise { + + // Await activation of potentially extension contributed providers + await this.activateProvider(resource.scheme); + + return this.hasProvider(resource); + } + + hasProvider(resource: URI): boolean { return this.provider.has(resource.scheme); } @@ -172,7 +183,7 @@ export class FileService extends Disposable implements IFileService { // Specially handle file not found case as file operation result if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) { - throw new FileOperationError(localize('fileNotFoundError', "Unable to resolve non-existing file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); + throw new FileOperationError(localize('fileNotFoundError', "Unable to resolve nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); } // Bubble up any other error as is @@ -201,9 +212,7 @@ export class FileService extends Disposable implements IFileService { trie = TernarySearchTree.forUris(() => !isPathCaseSensitive); trie.set(resource, true); if (resolveTo) { - for (const uri of resolveTo) { - trie.set(uri, true); - } + trie.fill(true, resolveTo); } } @@ -935,7 +944,7 @@ export class FileService extends Disposable implements IFileService { 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); + throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete nonexistent file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); } // Validate recursive @@ -1069,21 +1078,19 @@ export class FileService extends Disposable implements IFileService { // Forward watch request to provider and // wire in disposables. - { - let watchDisposed = false; - let disposeWatch = () => { watchDisposed = true; }; - disposables.add(toDisposable(() => disposeWatch())); + let watchDisposed = false; + let disposeWatch = () => { watchDisposed = true; }; + disposables.add(toDisposable(() => disposeWatch())); - // Watch and wire in disposable which is async but - // check if we got disposed meanwhile and forward - this.doWatch(resource, options).then(disposable => { - if (watchDisposed) { - dispose(disposable); - } else { - disposeWatch = () => dispose(disposable); - } - }, error => this.logService.error(error)); - } + // Watch and wire in disposable which is async but + // check if we got disposed meanwhile and forward + this.doWatch(resource, options).then(disposable => { + if (watchDisposed) { + dispose(disposable); + } else { + disposeWatch = () => dispose(disposable); + } + }, error => this.logService.error(error)); // Remember as watched resource and unregister // properly on disposal. @@ -1119,8 +1126,10 @@ export class FileService extends Disposable implements IFileService { const key = this.toWatchKey(provider, resource, options); // Only start watching if we are the first for the given key - const watcher = this.activeWatchers.get(key) || { count: 0, disposable: provider.watch(resource, options) }; - if (!this.activeWatchers.has(key)) { + let watcher = this.activeWatchers.get(key); + if (!watcher) { + watcher = { count: 0, disposable: provider.watch(resource, options) }; + this.activeWatchers.set(key, watcher); } @@ -1128,14 +1137,16 @@ export class FileService extends Disposable implements IFileService { watcher.count += 1; return toDisposable(() => { + if (watcher) { - // Unref - watcher.count--; + // Unref + watcher.count--; - // Dispose only when last user is reached - if (watcher.count === 0) { - dispose(watcher.disposable); - this.activeWatchers.delete(key); + // Dispose only when last user is reached + if (watcher.count === 0) { + dispose(watcher.disposable); + this.activeWatchers.delete(key); + } } }); } @@ -1216,8 +1227,7 @@ export class FileService extends Disposable implements IFileService { stream = streamOrBufferedStream; } - return new Promise(async (resolve, reject) => { - + return new Promise((resolve, reject) => { listenStream(stream, { onData: async chunk => { diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 045d802c08..53ce8de43d 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -57,9 +57,22 @@ export interface IFileService { activateProvider(scheme: string): Promise; /** - * Checks if this file service can handle the given resource. + * Checks if this file service can handle the given resource by + * first activating any extension that wants to be activated + * on the provided resource scheme to include extensions that + * contribute file system providers for the given resource. */ - canHandleResource(resource: URI): boolean; + canHandleResource(resource: URI): Promise; + + /** + * Checks if the file service has a registered provider for the + * provided resource. + * + * Note: this does NOT account for contributed providers from + * extensions that have not been activated yet. To include those, + * consider to call `await fileService.canHandleResource(resource)`. + */ + hasProvider(resource: URI): boolean; /** * Checks if the provider for the provided resource has the provided file system capability. @@ -663,25 +676,31 @@ export class FileChangesEvent { private readonly deleted: TernarySearchTree | undefined = undefined; constructor(changes: readonly IFileChange[], ignorePathCasing: boolean) { + + const entriesByType = new Map(); + for (const change of changes) { - switch (change.type) { + const array = entriesByType.get(change.type); + if (array) { + array.push([change.resource, change]); + } else { + entriesByType.set(change.type, [[change.resource, change]]); + } + } + + for (const [key, value] of entriesByType) { + switch (key) { case FileChangeType.ADDED: - if (!this.added) { - this.added = TernarySearchTree.forUris(() => ignorePathCasing); - } - this.added.set(change.resource, change); + this.added = TernarySearchTree.forUris(() => ignorePathCasing); + this.added.fill(value); break; case FileChangeType.UPDATED: - if (!this.updated) { - this.updated = TernarySearchTree.forUris(() => ignorePathCasing); - } - this.updated.set(change.resource, change); + this.updated = TernarySearchTree.forUris(() => ignorePathCasing); + this.updated.fill(value); break; case FileChangeType.DELETED: - if (!this.deleted) { - this.deleted = TernarySearchTree.forUris(() => ignorePathCasing); - } - this.deleted.set(change.resource, change); + this.deleted = TernarySearchTree.forUris(() => ignorePathCasing); + this.deleted.fill(value); break; } } @@ -1069,6 +1088,7 @@ export interface IFilesConfiguration { associations: { [filepattern: string]: string }; exclude: IExpression; watcherExclude: { [filepattern: string]: boolean }; + watcherInclude: string[]; encoding: string; autoGuessEncoding: boolean; defaultLanguage: string; @@ -1108,7 +1128,7 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined } export async function whenProviderRegistered(file: URI, fileService: IFileService): Promise { - if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) { + if (fileService.hasProvider(URI.from({ scheme: file.scheme }))) { return; } diff --git a/src/vs/platform/files/common/io.ts b/src/vs/platform/files/common/io.ts index 36a339d3b0..6805de9462 100644 --- a/src/vs/platform/files/common/io.ts +++ b/src/vs/platform/files/common/io.ts @@ -10,7 +10,6 @@ import { IDataTransformer, IErrorTransformer, WriteableStream } from 'vs/base/co import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { createFileSystemProviderError, ensureFileSystemProviderError, FileReadStreamOptions, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability } from 'vs/platform/files/common/files'; -import product from 'vs/platform/product/common/product'; export interface ICreateReadStreamOptions extends FileReadStreamOptions { @@ -128,7 +127,7 @@ function throwIfTooLarge(totalBytesRead: number, options: ICreateReadStreamOptio // Return early if file is too large to load and we have configured limits if (options?.limits) { if (typeof options.limits.memory === 'number' && totalBytesRead > options.limits.memory) { - throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", product.nameShort), FileSystemProviderErrorCode.FileExceedsMemoryLimit); + throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow to use more memory"), FileSystemProviderErrorCode.FileExceedsMemoryLimit); } if (typeof options.limits.size === 'number' && totalBytesRead > options.limits.size) { diff --git a/src/vs/platform/files/common/ipcFileSystemProvider.ts b/src/vs/platform/files/common/ipcFileSystemProvider.ts index 80d3c0c6ac..a898f4259f 100644 --- a/src/vs/platform/files/common/ipcFileSystemProvider.ts +++ b/src/vs/platform/files/common/ipcFileSystemProvider.ts @@ -7,105 +7,54 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { canceled } from 'vs/base/common/errors'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } from 'vs/base/common/stream'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { FileChangeType, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, IFileChange, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; - -interface IFileChangeDto { - resource: UriComponents; - type: FileChangeType; -} +import { createFileSystemProviderError, FileChangeType, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileChange, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; /** - * An abstract file system provider that delegates all calls to a provided - * `IChannel` via IPC communication. + * An implementation of a file system provider that is backed by a `IChannel` + * and thus implemented via IPC on a different process. */ -export abstract class IPCFileSystemProvider extends Disposable implements +export class IPCFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability { - private readonly session: string = generateUuid(); - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChange.event; - - private _onDidWatchErrorOccur = this._register(new Emitter()); - readonly onDidErrorOccur = this._onDidWatchErrorOccur.event; - - private readonly _onDidChangeCapabilities = this._register(new Emitter()); - readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; - - private _capabilities = FileSystemProviderCapabilities.FileReadWrite - | FileSystemProviderCapabilities.FileOpenReadWriteClose - | FileSystemProviderCapabilities.FileReadStream - | FileSystemProviderCapabilities.FileFolderCopy - | FileSystemProviderCapabilities.FileWriteUnlock; - get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } - - constructor(private readonly channel: IChannel) { + constructor(readonly capabilities: FileSystemProviderCapabilities, private readonly channel: IChannel) { super(); - this.registerListeners(); + this.registerFileChangeListeners(); } - private registerListeners(): void { - this._register(this.channel.listen('filechange', [this.session])(eventsOrError => { - if (Array.isArray(eventsOrError)) { - const events = eventsOrError; - this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type }))); - } else { - const error = eventsOrError; - this._onDidWatchErrorOccur.fire(error); - } - })); - } + //#region File Capabilities - protected setCaseSensitive(isCaseSensitive: boolean) { - if (isCaseSensitive) { - this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; - } else { - this._capabilities &= ~FileSystemProviderCapabilities.PathCaseSensitive; - } + readonly onDidChangeCapabilities: Event = Event.None; - this._onDidChangeCapabilities.fire(undefined); - } + //#endregion - // --- forwarding calls + //#region File Metadata Resolving stat(resource: URI): Promise { return this.channel.call('stat', [resource]); } - open(resource: URI, opts: FileOpenOptions): Promise { - return this.channel.call('open', [resource, opts]); + readdir(resource: URI): Promise<[string, FileType][]> { + return this.channel.call('readdir', [resource]); } - close(fd: number): Promise { - return this.channel.call('close', [fd]); - } + //#endregion - async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]); - - // copy back the data that was written into the buffer on the remote - // side. we need to do this because buffers are not referenced by - // pointer, but only by value and as such cannot be directly written - // to from the other process. - data.set(bytes.buffer.slice(0, bytesRead), offset); - - return bytesRead; - } + //#region File Reading/Writing async readFile(resource: URI): Promise { - const buff = await this.channel.call('readFile', [resource]); + const { buffer } = await this.channel.call('readFile', [resource]) as VSBuffer; - return buff.buffer; + return buffer; } readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { @@ -131,7 +80,7 @@ export abstract class IPCFileSystemProvider extends Disposable implements // error here to forward it properly. let error = dataOrErrorOrEnd; if (!(error instanceof Error)) { - error = new Error(toErrorMessage(error)); + error = createFileSystemProviderError(toErrorMessage(error), FileSystemProviderErrorCode.Unknown); } stream.error(error); @@ -160,24 +109,44 @@ export abstract class IPCFileSystemProvider extends Disposable implements return stream; } - write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]); - } - writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]); } - delete(resource: URI, opts: FileDeleteOptions): Promise { - return this.channel.call('delete', [resource, opts]); + open(resource: URI, opts: FileOpenOptions): Promise { + return this.channel.call('open', [resource, opts]); } + close(fd: number): Promise { + return this.channel.call('close', [fd]); + } + + async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]); + + // copy back the data that was written into the buffer on the remote + // side. we need to do this because buffers are not referenced by + // pointer, but only by value and as such cannot be directly written + // to from the other process. + data.set(bytes.buffer.slice(0, bytesRead), offset); + + return bytesRead; + } + + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]); + } + + //#endregion + + //#region Move/Copy/Delete/Create Folder + mkdir(resource: URI): Promise { return this.channel.call('mkdir', [resource]); } - readdir(resource: URI): Promise<[string, FileType][]> { - return this.channel.call('readdir', [resource]); + delete(resource: URI, opts: FileDeleteOptions): Promise { + return this.channel.call('delete', [resource, opts]); } rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { @@ -188,10 +157,50 @@ export abstract class IPCFileSystemProvider extends Disposable implements return this.channel.call('copy', [resource, target, opts]); } - watch(resource: URI, opts: IWatchOptions): IDisposable { - const req = Math.random(); - this.channel.call('watch', [this.session, req, resource, opts]); + //#endregion - return toDisposable(() => this.channel.call('unwatch', [this.session, req])); + //#region File Watching + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChange.event; + + private readonly _onDidErrorOccur = this._register(new Emitter()); + readonly onDidErrorOccur = this._onDidErrorOccur.event; + + // The contract for file watching via remote is to identify us + // via a unique but readonly session ID. Since the remote is + // managing potentially many watchers from different clients, + // this helps the server to properly partition events to the right + // clients. + private readonly sessionId = generateUuid(); + + private registerFileChangeListeners(): void { + + // The contract for file changes is that there is one listener + // for both events and errors from the watcher. So we need to + // unwrap the event from the remote and emit through the proper + // emitter. + this._register(this.channel.listen<{ resource: UriComponents; type: FileChangeType; }[] | string>('fileChange', [this.sessionId])(eventsOrError => { + if (Array.isArray(eventsOrError)) { + const events = eventsOrError; + this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type }))); + } else { + const error = eventsOrError; + this._onDidErrorOccur.fire(error); + } + })); } + + watch(resource: URI, opts: IWatchOptions): IDisposable { + + // Generate a request UUID to correlate the watcher + // back to us when we ask to dispose the watcher later. + const req = generateUuid(); + + this.channel.call('watch', [this.sessionId, req, resource, opts]); + + return toDisposable(() => this.channel.call('unwatch', [this.sessionId, req])); + } + + //#endregion } diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts new file mode 100644 index 0000000000..b33fa5e21b --- /dev/null +++ b/src/vs/platform/files/common/watcher.ts @@ -0,0 +1,295 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { isLinux } from 'vs/base/common/platform'; +import { URI as uri } from 'vs/base/common/uri'; +import { FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files'; + +export interface IWatcherService { + + /** + * A normalized file change event from the raw events + * the watcher emits. + */ + readonly onDidChangeFile: Event; + + /** + * An event to indicate a message that should get logged. + */ + readonly onDidLogMessage: Event; + + /** + * An event to indicate an error occured from the watcher + * that is unrecoverable. Listeners should restart the + * service if possible. + */ + readonly onDidError: Event; + + /** + * Configures the watcher service to watch according + * to the requests. Any existing watched path that + * is not in the array, will be removed from watching + * and any new path will be added to watching. + */ + watch(requests: IWatchRequest[]): Promise; + + /** + * Enable verbose logging in the watcher. + */ + setVerboseLogging(enabled: boolean): Promise; + + /** + * Stop all watchers. + */ + stop(): Promise; +} + +export abstract class AbstractWatcherService extends Disposable { + + private static readonly MAX_RESTARTS = 5; + + private service: IWatcherService | undefined; + private readonly serviceDisposables = this._register(new MutableDisposable()); + + private requests: IWatchRequest[] | undefined = undefined; + + private restartCounter = 0; + + constructor( + private readonly onFileChanges: (changes: IDiskFileChange[]) => void, + private readonly onLogMessage: (msg: ILogMessage) => void, + private verboseLogging: boolean + ) { + super(); + } + + protected abstract createService(disposables: DisposableStore): IWatcherService; + + protected init(): void { + + // Associate disposables to the service + const disposables = new DisposableStore(); + this.serviceDisposables.value = disposables; + + // Ask implementors to create the service + this.service = this.createService(disposables); + this.service.setVerboseLogging(this.verboseLogging); + + // Wire in event handlers + disposables.add(this.service.onDidChangeFile(e => this.onFileChanges(e))); + disposables.add(this.service.onDidLogMessage(e => this.onLogMessage(e))); + disposables.add(this.service.onDidError(e => this.onError(e))); + } + + protected onError(error: string): void { + + // Restart up to N times + if (this.restartCounter < AbstractWatcherService.MAX_RESTARTS && this.requests) { + this.error(`restarting watcher after error: ${error}`); + this.restart(this.requests); + } + + // Otherwise log that we have given up to restart + else { + this.error(`gave up attempting to restart watcher after error: ${error}`); + } + } + + private restart(requests: IWatchRequest[]): void { + this.restartCounter++; + + this.init(); + this.watch(requests); + } + + async watch(requests: IWatchRequest[]): Promise { + this.requests = requests; + + await this.service?.watch(requests); + } + + async setVerboseLogging(verboseLogging: boolean): Promise { + this.verboseLogging = verboseLogging; + + await this.service?.setVerboseLogging(verboseLogging); + } + + private error(message: string) { + this.onLogMessage({ type: 'error', message: `[File Watcher (parcel)] ${message}` }); + } + + override dispose(): void { + + // Render the serve invalid from here + this.service = undefined; + + return super.dispose(); + } +} + +/** + * Base class of any watcher service we support. + * + * TODO@bpasero delete and replace with `AbstractWatcherService` + */ +export abstract class WatcherService extends Disposable { + + /** + * Asks to watch the provided folders. + */ + abstract watch(requests: IWatchRequest[]): Promise; + + /** + * Enable verbose logging from the watcher. + */ + abstract setVerboseLogging(verboseLogging: boolean): Promise; +} + +export interface IWatchRequest { + + /** + * The path to watch. + */ + path: string; + + /** + * A set of glob patterns or paths to exclude from watching. + */ + excludes: string[]; + + /** + * @deprecated this only exists for WSL1 support and should never + * be used in any other case. + */ + pollingInterval?: number; +} + +export interface IDiskFileChange { + type: FileChangeType; + path: string; +} + +export interface ILogMessage { + type: 'trace' | 'warn' | 'error' | 'info' | 'debug'; + message: string; +} + +export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] { + return changes.map(change => ({ + type: change.type, + resource: uri.file(change.path) + })); +} + +export function normalizeFileChanges(changes: IDiskFileChange[]): IDiskFileChange[] { + + // Build deltas + const normalizer = new EventNormalizer(); + for (const event of changes) { + normalizer.processEvent(event); + } + + return normalizer.normalize(); +} + +class EventNormalizer { + + private readonly normalized = new Set(); + private readonly mapPathToChange = new Map(); + + private toKey(event: IDiskFileChange): string { + if (isLinux) { + return event.path; + } + + return event.path.toLowerCase(); // normalise to file system case sensitivity + } + + processEvent(event: IDiskFileChange): void { + const existingEvent = this.mapPathToChange.get(this.toKey(event)); + + let keepEvent = false; + + // Event path already exists + if (existingEvent) { + const currentChangeType = existingEvent.type; + const newChangeType = event.type; + + // macOS/Windows: track renames to different case but + // same name by changing current event to DELETED + // this encodes some underlying knowledge about the + // file watcher being used by assuming we first get + // an event for the CREATE and then an event that we + // consider as DELETE if same name / different case. + if (existingEvent.path !== event.path && event.type === FileChangeType.DELETED) { + keepEvent = true; + } + + // Ignore CREATE followed by DELETE in one go + else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.DELETED) { + this.mapPathToChange.delete(this.toKey(event)); + this.normalized.delete(existingEvent); + } + + // Flatten DELETE followed by CREATE into CHANGE + else if (currentChangeType === FileChangeType.DELETED && newChangeType === FileChangeType.ADDED) { + existingEvent.type = FileChangeType.UPDATED; + } + + // Do nothing. Keep the created event + else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) { } + + // Otherwise apply change type + else { + existingEvent.type = newChangeType; + } + } + + // Otherwise keep + else { + keepEvent = true; + } + + if (keepEvent) { + this.normalized.add(event); + this.mapPathToChange.set(this.toKey(event), event); + } + } + + normalize(): IDiskFileChange[] { + const addOrChangeEvents: IDiskFileChange[] = []; + const deletedPaths: string[] = []; + + // This algorithm will remove all DELETE events up to the root folder + // that got deleted if any. This ensures that we are not producing + // DELETE events for each file inside a folder that gets deleted. + // + // 1.) split ADD/CHANGE and DELETED events + // 2.) sort short deleted paths to the top + // 3.) for each DELETE, check if there is a deleted parent and ignore the event in that case + return Array.from(this.normalized).filter(e => { + if (e.type !== FileChangeType.DELETED) { + addOrChangeEvents.push(e); + + return false; // remove ADD / CHANGE + } + + return true; // keep DELETE + }).sort((e1, e2) => { + return e1.path.length - e2.path.length; // shortest path first + }).filter(e => { + if (deletedPaths.some(deletedPath => isParent(e.path, deletedPath, !isLinux /* ignorecase */))) { + return false; // DELETE is ignored if parent is deleted already + } + + // otherwise mark as deleted + deletedPaths.push(e.path); + + return true; + }).concat(addOrChangeEvents); + } +} diff --git a/src/vs/platform/files/electron-main/diskFileSystemProviderIpc.ts b/src/vs/platform/files/electron-main/diskFileSystemProviderIpc.ts new file mode 100644 index 0000000000..f343712da6 --- /dev/null +++ b/src/vs/platform/files/electron-main/diskFileSystemProviderIpc.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { shell } from 'electron'; +import { localize } from 'vs/nls'; +import { isWindows } from 'vs/base/common/platform'; +import { Emitter } from 'vs/base/common/event'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { FileDeleteOptions, IFileChange, IWatchOptions, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; +import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { basename, normalize } from 'vs/base/common/path'; +import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ILogMessage, toFileChanges } from 'vs/platform/files/common/watcher'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { AbstractDiskFileSystemProviderChannel, ISessionFileWatcher } from 'vs/platform/files/node/diskFileSystemProviderIpc'; +import { DefaultURITransformer, IURITransformer } from 'vs/base/common/uriIpc'; + +/** + * A server implementation for a IPC based file system provider (see `IPCFileSystemProvider`) + * client. + */ +export class DiskFileSystemProviderChannel extends AbstractDiskFileSystemProviderChannel { + + constructor( + provider: DiskFileSystemProvider, + logService: ILogService + ) { + super(provider, logService); + } + + protected override getUriTransformer(ctx: unknown): IURITransformer { + return DefaultURITransformer; + } + + protected override transformIncoming(uriTransformer: IURITransformer, _resource: UriComponents): URI { + return URI.revive(_resource); + } + + //#region Delete: override to support Electron's trash support + + protected override async delete(uriTransformer: IURITransformer, _resource: UriComponents, opts: FileDeleteOptions): Promise { + if (!opts.useTrash) { + return super.delete(uriTransformer, _resource, opts); + } + + const resource = this.transformIncoming(uriTransformer, _resource); + const filePath = normalize(resource.fsPath); + try { + await shell.trashItem(filePath); + } catch (error) { + throw createFileSystemProviderError(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath)), FileSystemProviderErrorCode.Unknown); + } + } + + //#endregion + + //#region File Watching + + protected createSessionFileWatcher(uriTransformer: IURITransformer, emitter: Emitter): ISessionFileWatcher { + return new SessionFileWatcher(emitter, this.logService); + } + + //#endregion + +} + +class SessionFileWatcher extends Disposable implements ISessionFileWatcher { + + private readonly watcherRequests = new Map(); + + constructor( + private readonly sessionEmitter: Emitter, + private readonly logService: ILogService + ) { + super(); + } + + watch(req: number, resource: URI, opts: IWatchOptions): IDisposable { + if (opts.recursive) { + throw createFileSystemProviderError('Recursive watcher is not supported from main process', FileSystemProviderErrorCode.Unavailable); + } + + const disposable = new DisposableStore(); + + this.watcherRequests.set(req, disposable); + disposable.add(toDisposable(() => this.watcherRequests.delete(req))); + + const watcher = disposable.add(new NodeJSWatcherService( + normalize(resource.fsPath), + changes => this.sessionEmitter.fire(toFileChanges(changes)), + msg => this.onWatcherLogMessage(msg), + this.logService.getLevel() === LogLevel.Trace + )); + + disposable.add(this.logService.onDidChangeLogLevel(() => { + watcher.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); + })); + + return disposable; + } + + private onWatcherLogMessage(msg: ILogMessage): void { + if (msg.type === 'error') { + this.sessionEmitter.fire(msg.message); + } + + this.logService[msg.type](msg.message); + } + + override dispose(): void { + super.dispose(); + + this.watcherRequests.forEach(disposable => dispose(disposable)); + this.watcherRequests.clear(); + } +} diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 672faa06ad..78d45d0ee7 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,58 +3,85 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Stats } from 'fs'; -import { insert } from 'vs/base/common/arrays'; -import { retry, ThrottledDelayer } from 'vs/base/common/async'; +import * as fs from 'fs'; +import { gracefulify } from 'graceful-fs'; +import { retry } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { isEqual } from 'vs/base/common/extpath'; -import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { basename, dirname, normalize } from 'vs/base/common/path'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { basename, dirname } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; import { IDirent, Promises, RimRafMode, SymlinkSupport } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; -import { createFileSystemProviderError, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileChange, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { createFileSystemProviderError, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat } from 'vs/platform/files/common/files'; import { readFileIntoStream } from 'vs/platform/files/common/io'; import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService'; import { FileWatcher as NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcherService'; -import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService'; -import { IDiskFileChange, ILogMessage, toFileChanges } from 'vs/platform/files/node/watcher/watcher'; -import { FileWatcher as WindowsWatcherService } from 'vs/platform/files/node/watcher/win32/watcherService'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { FileWatcher as ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/watcherService'; +import { IDiskFileChange, ILogMessage, IWatchRequest, WatcherService } from 'vs/platform/files/common/watcher'; +import { ILogService } from 'vs/platform/log/common/log'; +import { AbstractDiskFileSystemProvider } from 'vs/platform/files/common/diskFileSystemProvider'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; + +/** + * Enable graceful-fs very early from here to have it enabled + * in all contexts that leverage the disk file system provider. + */ +(() => { + try { + gracefulify(fs); + } catch (error) { + console.error(`Error enabling graceful-fs: ${toErrorMessage(error)}`); + } +})(); export interface IWatcherOptions { - pollingInterval?: number; + + /** + * If `true`, will enable polling for all watchers, otherwise + * will enable it for paths included in the string array. + * + * @deprecated this only exists for WSL1 support and should never + * be used in any other case. + */ usePolling: boolean | string[]; + + /** + * If polling is enabled (via `usePolling`), defines the duration + * in which the watcher will poll for changes. + * + * @deprecated this only exists for WSL1 support and should never + * be used in any other case. + */ + pollingInterval?: number; } export interface IDiskFileSystemProviderOptions { - bufferSize?: number; watcher?: IWatcherOptions; + legacyWatcher?: string; } -export class DiskFileSystemProvider extends Disposable implements +export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability { - private readonly BUFFER_SIZE = this.options?.bufferSize || 64 * 1024; - constructor( - protected readonly logService: ILogService, + logService: ILogService, private readonly options?: IDiskFileSystemProviderOptions ) { - super(); + super(logService); } //#region File Capabilities - onDidChangeCapabilities: Event = Event.None; + readonly onDidChangeCapabilities: Event = Event.None; protected _capabilities: FileSystemProviderCapabilities | undefined; get capabilities(): FileSystemProviderCapabilities { @@ -119,10 +146,10 @@ export class DiskFileSystemProvider extends Disposable implements } } - private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { + private toType(entry: fs.Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { // Signal file type by checking for file / directory, except: - // - symbolic links pointing to non-existing files are FileType.Unknown + // - symbolic links pointing to nonexistent files are FileType.Unknown // - files that are neither file nor directory are FileType.Unknown let type: FileType; if (symbolicLink?.dangling) { @@ -162,7 +189,7 @@ export class DiskFileSystemProvider extends Disposable implements readFileIntoStream(this, resource, stream, data => data.buffer, { ...opts, - bufferSize: this.BUFFER_SIZE + bufferSize: 256 * 1024 // read into chunks of 256kb each to reduce IPC overhead }, token); return stream; @@ -201,7 +228,7 @@ export class DiskFileSystemProvider extends Disposable implements } } - private readonly mapHandleToPos: Map = new Map(); + private readonly mapHandleToPos = new Map(); private readonly writeHandles = new Map(); private canFlush: boolean = true; @@ -289,7 +316,7 @@ export class DiskFileSystemProvider extends Disposable implements // to flush the contents to disk if possible. if (this.writeHandles.delete(fd) && this.canFlush) { try { - await Promises.fdatasync(fd); + await Promises.fdatasync(fd); // https://github.com/microsoft/vscode/issues/9589 } 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 @@ -524,159 +551,77 @@ export class DiskFileSystemProvider extends Disposable implements //#region File Watching - private readonly _onDidWatchErrorOccur = this._register(new Emitter()); - readonly onDidErrorOccur = this._onDidWatchErrorOccur.event; + protected createRecursiveWatcher( + folders: number, + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ): WatcherService { + let watcherImpl: { + new( + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean, + watcherOptions?: IWatcherOptions + ): WatcherService + }; - private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChangeFile.event; - - private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined; - private readonly recursiveFoldersToWatch: { path: string, excludes: string[] }[] = []; - private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer(0)); - - private recursiveWatcherLogLevelListener: IDisposable | undefined; - - watch(resource: URI, opts: IWatchOptions): IDisposable { - if (opts.recursive) { - return this.watchRecursive(resource, opts.excludes); + let enableLegacyWatcher = false; + if (this.options?.watcher?.usePolling) { + enableLegacyWatcher = false; // must use Parcel watcher for when polling is required + } else { + enableLegacyWatcher = this.options?.legacyWatcher === 'on'; // setting always wins } - return this.watchNonRecursive(resource); - } - - private watchRecursive(resource: URI, excludes: string[]): IDisposable { - - // Add to list of folders to watch recursively - const folderToWatch = { path: this.toFilePath(resource), excludes }; - const remove = insert(this.recursiveFoldersToWatch, folderToWatch); - - // Trigger update - this.refreshRecursiveWatchers(); - - return toDisposable(() => { - - // Remove from list of folders to watch recursively - remove(); - - // Trigger update - this.refreshRecursiveWatchers(); - }); - } - - private refreshRecursiveWatchers(): void { - - // Buffer requests for recursive watching to decide on right watcher - // that supports potentially watching more than one folder at once - this.recursiveWatchRequestDelayer.trigger(async () => { - this.doRefreshRecursiveWatchers(); - }); - } - - private doRefreshRecursiveWatchers(): void { - - // Reuse existing - if (this.recursiveWatcher instanceof NsfwWatcherService) { - this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch); + if (enableLegacyWatcher) { + watcherImpl = NsfwWatcherService; + } else { + watcherImpl = ParcelWatcherService; } - // Create new - else { + return new watcherImpl( + changes => onChange(changes), + msg => onLogMessage(msg), + verboseLogging, + this.options?.watcher + ); + } - // Dispose old - dispose(this.recursiveWatcher); - this.recursiveWatcher = undefined; - - // Create new if we actually have folders to watch - if (this.recursiveFoldersToWatch.length > 0) { - let watcherImpl: { - new( - folders: { path: string, excludes: string[] }[], - onChange: (changes: IDiskFileChange[]) => void, - onLogMessage: (msg: ILogMessage) => void, - verboseLogging: boolean, - watcherOptions?: IWatcherOptions - ): WindowsWatcherService | UnixWatcherService | NsfwWatcherService - }; - - let watcherOptions: IWatcherOptions | undefined = undefined; - - // requires a polling watcher - if (this.options?.watcher?.usePolling) { - watcherImpl = UnixWatcherService; - watcherOptions = this.options?.watcher; - } - - // Single Folder Watcher - else { - if (this.recursiveFoldersToWatch.length === 1) { - if (isWindows) { - watcherImpl = WindowsWatcherService; - } else { - watcherImpl = UnixWatcherService; - } - } - - // Multi Folder Watcher - else { - watcherImpl = NsfwWatcherService; - } - } - - // Create and start watching - this.recursiveWatcher = new watcherImpl( - this.recursiveFoldersToWatch, - event => this._onDidChangeFile.fire(toFileChanges(event)), - msg => { - if (msg.type === 'error') { - this._onDidWatchErrorOccur.fire(msg.message); - } - - this.logService[msg.type](msg.message); - }, - this.logService.getLevel() === LogLevel.Trace, - watcherOptions - ); - - if (!this.recursiveWatcherLogLevelListener) { - this.recursiveWatcherLogLevelListener = this.logService.onDidChangeLogLevel(() => { - if (this.recursiveWatcher) { - this.recursiveWatcher.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); - } - }); + protected override doWatch(watcher: WatcherService, requests: IWatchRequest[]): Promise { + const usePolling = this.options?.watcher?.usePolling; + if (usePolling === true) { + for (const request of requests) { + request.pollingInterval = this.options?.watcher?.pollingInterval ?? 5000; + } + } else if (Array.isArray(usePolling)) { + for (const request of requests) { + if (usePolling.includes(request.path)) { + request.pollingInterval = this.options?.watcher?.pollingInterval ?? 5000; } } } + + return super.doWatch(watcher, requests); } - private watchNonRecursive(resource: URI): IDisposable { - const watcherService = new NodeJSWatcherService( - this.toFilePath(resource), - changes => this._onDidChangeFile.fire(toFileChanges(changes)), - msg => { - if (msg.type === 'error') { - this._onDidWatchErrorOccur.fire(msg.message); - } - - this.logService[msg.type](msg.message); - }, - this.logService.getLevel() === LogLevel.Trace + protected createNonRecursiveWatcher( + path: string, + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ): IDisposable & { setVerboseLogging: (verboseLogging: boolean) => void } { + return new NodeJSWatcherService( + path, + changes => onChange(changes), + msg => onLogMessage(msg), + verboseLogging ); - - const logLevelListener = this.logService.onDidChangeLogLevel(() => { - watcherService.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); - }); - - return combinedDisposable(watcherService, logLevelListener); } //#endregion //#region Helpers - protected toFilePath(resource: URI): string { - return normalize(resource.fsPath); - } - private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError { if (error instanceof FileSystemProviderError) { return error; // avoid double conversion @@ -728,14 +673,4 @@ export class DiskFileSystemProvider extends Disposable implements } //#endregion - - override dispose(): void { - super.dispose(); - - dispose(this.recursiveWatcher); - this.recursiveWatcher = undefined; - - dispose(this.recursiveWatcherLogLevelListener); - this.recursiveWatcherLogLevelListener = undefined; - } } diff --git a/src/vs/platform/files/node/diskFileSystemProviderIpc.ts b/src/vs/platform/files/node/diskFileSystemProviderIpc.ts new file mode 100644 index 0000000000..44593d594e --- /dev/null +++ b/src/vs/platform/files/node/diskFileSystemProviderIpc.ts @@ -0,0 +1,244 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ReadableStreamEventPayload, listenStream } from 'vs/base/common/stream'; +import { IStat, FileReadStreamOptions, FileWriteOptions, FileOpenOptions, FileDeleteOptions, FileOverwriteOptions, IFileChange, IWatchOptions, FileType } from 'vs/platform/files/common/files'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; + +export interface ISessionFileWatcher extends IDisposable { + watch(req: number, resource: URI, opts: IWatchOptions): IDisposable; +} + +/** + * A server implementation for a IPC based file system provider + * (see `IPCFileSystemProvider`) client. + */ +export abstract class AbstractDiskFileSystemProviderChannel extends Disposable implements IServerChannel { + + constructor( + protected readonly provider: DiskFileSystemProvider, + protected readonly logService: ILogService + ) { + super(); + } + + call(ctx: T, command: string, arg?: any): Promise { + const uriTransformer = this.getUriTransformer(ctx); + + switch (command) { + case 'stat': return this.stat(uriTransformer, arg[0]); + case 'readdir': return this.readdir(uriTransformer, arg[0]); + case 'open': return this.open(uriTransformer, arg[0], arg[1]); + case 'close': return this.close(arg[0]); + case 'read': return this.read(arg[0], arg[1], arg[2]); + case 'readFile': return this.readFile(uriTransformer, arg[0]); + case 'write': return this.write(arg[0], arg[1], arg[2], arg[3], arg[4]); + case 'writeFile': return this.writeFile(uriTransformer, arg[0], arg[1], arg[2]); + case 'rename': return this.rename(uriTransformer, arg[0], arg[1], arg[2]); + case 'copy': return this.copy(uriTransformer, arg[0], arg[1], arg[2]); + case 'mkdir': return this.mkdir(uriTransformer, arg[0]); + case 'delete': return this.delete(uriTransformer, arg[0], arg[1]); + case 'watch': return this.watch(uriTransformer, arg[0], arg[1], arg[2], arg[3]); + case 'unwatch': return this.unwatch(arg[0], arg[1]); + } + + throw new Error(`IPC Command ${command} not found`); + } + + listen(ctx: T, event: string, arg: any): Event { + const uriTransformer = this.getUriTransformer(ctx); + + switch (event) { + case 'fileChange': return this.onFileChange(uriTransformer, arg[0]); + case 'readFileStream': return this.onReadFileStream(uriTransformer, arg[0], arg[1]); + } + + throw new Error(`Unknown event ${event}`); + } + + protected abstract getUriTransformer(ctx: T): IURITransformer; + + protected abstract transformIncoming(uriTransformer: IURITransformer, _resource: UriComponents, supportVSCodeResource?: boolean): URI; + + //#region File Metadata Resolving + + private stat(uriTransformer: IURITransformer, _resource: UriComponents): Promise { + const resource = this.transformIncoming(uriTransformer, _resource, true); + + return this.provider.stat(resource); + } + + private readdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<[string, FileType][]> { + const resource = this.transformIncoming(uriTransformer, _resource); + + return this.provider.readdir(resource); + } + + //#endregion + + //#region File Reading/Writing + + private async readFile(uriTransformer: IURITransformer, _resource: UriComponents): Promise { + const resource = this.transformIncoming(uriTransformer, _resource, true); + const buffer = await this.provider.readFile(resource); + + return VSBuffer.wrap(buffer); + } + + private onReadFileStream(uriTransformer: IURITransformer, _resource: URI, opts: FileReadStreamOptions): Event> { + const resource = this.transformIncoming(uriTransformer, _resource, true); + const cts = new CancellationTokenSource(); + + const emitter = new Emitter>({ + onLastListenerRemove: () => { + + // Ensure to cancel the read operation when there is no more + // listener on the other side to prevent unneeded work. + cts.cancel(); + } + }); + + const fileStream = this.provider.readFileStream(resource, opts, cts.token); + listenStream(fileStream, { + onData: chunk => emitter.fire(VSBuffer.wrap(chunk)), + onError: error => emitter.fire(error), + onEnd: () => { + + // Forward event + emitter.fire('end'); + + // Cleanup + emitter.dispose(); + cts.dispose(); + } + }); + + return emitter.event; + } + + private writeFile(uriTransformer: IURITransformer, _resource: UriComponents, content: VSBuffer, opts: FileWriteOptions): Promise { + const resource = this.transformIncoming(uriTransformer, _resource); + + return this.provider.writeFile(resource, content.buffer, opts); + } + + private open(uriTransformer: IURITransformer, _resource: UriComponents, opts: FileOpenOptions): Promise { + const resource = this.transformIncoming(uriTransformer, _resource, true); + + return this.provider.open(resource, opts); + } + + private close(fd: number): Promise { + return this.provider.close(fd); + } + + private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> { + const buffer = VSBuffer.alloc(length); + const bufferOffset = 0; // offset is 0 because we create a buffer to read into for each call + const bytesRead = await this.provider.read(fd, pos, buffer.buffer, bufferOffset, length); + + return [buffer, bytesRead]; + } + + private write(fd: number, pos: number, data: VSBuffer, offset: number, length: number): Promise { + return this.provider.write(fd, pos, data.buffer, offset, length); + } + + //#endregion + + //#region Move/Copy/Delete/Create Folder + + private mkdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise { + const resource = this.transformIncoming(uriTransformer, _resource); + + return this.provider.mkdir(resource); + } + + protected delete(uriTransformer: IURITransformer, _resource: UriComponents, opts: FileDeleteOptions): Promise { + const resource = this.transformIncoming(uriTransformer, _resource); + + return this.provider.delete(resource, opts); + } + + private rename(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: FileOverwriteOptions): Promise { + const source = this.transformIncoming(uriTransformer, _source); + const target = this.transformIncoming(uriTransformer, _target); + + return this.provider.rename(source, target, opts); + } + + private copy(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: FileOverwriteOptions): Promise { + const source = this.transformIncoming(uriTransformer, _source); + const target = this.transformIncoming(uriTransformer, _target); + + return this.provider.copy(source, target, opts); + } + + //#endregion + + //#region File Watching + + private readonly sessionToWatcher = new Map(); + private readonly watchRequests = new Map(); + + private onFileChange(uriTransformer: IURITransformer, sessionId: string): Event { + + // We want a specific emitter for the given session so that events + // from the one session do not end up on the other session. As such + // we create a `SessionFileWatcher` and a `Emitter` for that session. + + const emitter = new Emitter({ + onFirstListenerAdd: () => { + this.sessionToWatcher.set(sessionId, this.createSessionFileWatcher(uriTransformer, emitter)); + }, + onLastListenerRemove: () => { + dispose(this.sessionToWatcher.get(sessionId)); + this.sessionToWatcher.delete(sessionId); + } + }); + + return emitter.event; + } + + private async watch(uriTransformer: IURITransformer, sessionId: string, req: number, _resource: UriComponents, opts: IWatchOptions): Promise { + const watcher = this.sessionToWatcher.get(sessionId); + if (watcher) { + const resource = this.transformIncoming(uriTransformer, _resource); + const disposable = watcher.watch(req, resource, opts); + this.watchRequests.set(sessionId + req, disposable); + } + } + + private async unwatch(sessionId: string, req: number): Promise { + const id = sessionId + req; + const disposable = this.watchRequests.get(id); + if (disposable) { + dispose(disposable); + this.watchRequests.delete(id); + } + } + + protected abstract createSessionFileWatcher(uriTransformer: IURITransformer, emitter: Emitter): ISessionFileWatcher; + + //#endregion + + override dispose(): void { + super.dispose(); + + this.watchRequests.forEach(disposable => dispose(disposable)); + this.watchRequests.clear(); + + this.sessionToWatcher.forEach(disposable => dispose(disposable)); + this.sessionToWatcher.clear(); + } +} diff --git a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts index e49e663bcd..4b97061dd2 100644 --- a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts @@ -10,12 +10,12 @@ import { realpath } from 'vs/base/node/extpath'; import { SymlinkSupport } from 'vs/base/node/pfs'; import { CHANGE_BUFFER_DELAY, watchFile, watchFolder } from 'vs/base/node/watcher'; import { FileChangeType } from 'vs/platform/files/common/files'; -import { IDiskFileChange, ILogMessage, normalizeFileChanges } from 'vs/platform/files/node/watcher/watcher'; +import { IDiskFileChange, ILogMessage, normalizeFileChanges } from 'vs/platform/files/common/watcher'; export class FileWatcher extends Disposable { private isDisposed: boolean | undefined; - private fileChangesDelayer: ThrottledDelayer = this._register(new ThrottledDelayer(CHANGE_BUFFER_DELAY * 2 /* sync on delay from underlying library */)); + private readonly fileChangesDelayer: ThrottledDelayer = this._register(new ThrottledDelayer(CHANGE_BUFFER_DELAY * 2 /* sync on delay from underlying library */)); private fileChangesBuffer: IDiskFileChange[] = []; constructor( @@ -100,8 +100,8 @@ export class FileWatcher extends Disposable { // Logging if (this.verboseLogging) { - for (const e of normalizedFileChanges) { - this.onVerbose(`>> normalized ${e.type === FileChangeType.ADDED ? '[ADDED]' : e.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${e.path}`); + for (const event of normalizedFileChanges) { + this.onVerbose(`>> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); } } diff --git a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts index d34e560f85..952982ecff 100644 --- a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -3,39 +3,64 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nsfw from 'nsfw'; -import { ThrottledDelayer } from 'vs/base/common/async'; +import * as nsfw from 'vscode-nsfw'; +import { existsSync } from 'fs'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; -import { isEqualOrParent } from 'vs/base/common/extpath'; import { parse, ParsedPattern } from 'vs/base/common/glob'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { TernarySearchTree } from 'vs/base/common/map'; import { normalizeNFC } from 'vs/base/common/normalization'; -import { join } from 'vs/base/common/path'; +import { dirname, join } from 'vs/base/common/path'; import { isMacintosh } from 'vs/base/common/platform'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { FileChangeType } from 'vs/platform/files/common/files'; -import { IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher'; -import { IDiskFileChange, ILogMessage, normalizeFileChanges } from 'vs/platform/files/node/watcher/watcher'; +import { IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher'; +import { IDiskFileChange, ILogMessage, normalizeFileChanges, IWatchRequest } from 'vs/platform/files/common/watcher'; +import { watchFolder } from 'vs/base/node/watcher'; -const nsfwActionToRawChangeType: { [key: number]: number } = []; -nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED; -nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED; -nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED; +interface IWatcher extends IDisposable { -interface IWatcher { - start(): void; - stop(): void; -} + /** + * The NSFW instance is resolved when the watching has started. + */ + readonly instance: Promise; -interface IPathWatcher { - readonly ready: Promise; - watcher?: IWatcher; + /** + * The watch request associated to the watcher. + */ + request: IWatchRequest; + + /** + * Associated ignored patterns for the watcher that can be updated. + */ ignored: ParsedPattern[]; + + /** + * How often this watcher has been restarted in case of an unexpected + * shutdown. + */ + restarts: number; + + /** + * The cancellation token associated with the lifecycle of the watcher. + */ + token: CancellationToken; } export class NsfwWatcherService extends Disposable implements IWatcherService { - private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) + private static readonly MAX_RESTARTS = 5; // number of restarts we allow before giving up in case of unexpected shutdown + + private static readonly MAP_NSFW_ACTION_TO_FILE_CHANGE = new Map( + [ + [nsfw.actions.CREATED, FileChangeType.ADDED], + [nsfw.actions.MODIFIED, FileChangeType.UPDATED], + [nsfw.actions.DELETED, FileChangeType.DELETED], + ] + ); private readonly _onDidChangeFile = this._register(new Emitter()); readonly onDidChangeFile = this._onDidChangeFile.event; @@ -43,75 +68,185 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { private readonly _onDidLogMessage = this._register(new Emitter()); readonly onDidLogMessage = this._onDidLogMessage.event; - private pathWatchers: { [watchPath: string]: IPathWatcher } = {}; - private verboseLogging: boolean | undefined; - private enospcErrorLogged: boolean | undefined; + protected readonly watchers = new Map(); + + private verboseLogging = false; + private enospcErrorLogged = false; constructor() { super(); - process.on('uncaughtException', (e: Error | string) => { - // Specially handle ENOSPC errors that can happen when - // the watcher consumes so many file descriptors that - // we are running into a limit. We only want to warn - // once in this case to avoid log spam. - // See https://github.com/microsoft/vscode/issues/7950 - if (e === 'Inotify limit reached' && !this.enospcErrorLogged) { - this.enospcErrorLogged = true; - this.error('Inotify limit reached (ENOSPC)'); - } - }); + this.registerListeners(); } - async setRoots(roots: IWatcherRequest[]): Promise { - const normalizedRoots = this.normalizeRoots(roots); + private registerListeners(): void { - // Gather roots that are not currently being watched - const rootsToStartWatching = normalizedRoots.filter(root => { - return !(root.path in this.pathWatchers); + // Error handling on process + process.on('uncaughtException', error => this.onError(error)); + process.on('unhandledRejection', error => this.onError(error)); + } + + async watch(requests: IWatchRequest[]): Promise { + + // Figure out duplicates to remove from the requests + const normalizedRequests = this.normalizeRequests(requests); + + // Gather paths that we should start watching + const requestsToStartWatching = normalizedRequests.filter(request => { + return !this.watchers.has(request.path); }); - // Gather current roots that don't exist in the new roots array - const rootsToStopWatching = Object.keys(this.pathWatchers).filter(root => { - return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== root); + // Gather paths that we should stop watching + const pathsToStopWatching = Array.from(this.watchers.keys()).filter(watchedPath => { + return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === watchedPath); }); // Logging - this.debug(`Start watching: ${rootsToStartWatching.map(root => `${root.path} (excludes: ${root.excludes})`).join(',')}`); - this.debug(`Stop watching: ${rootsToStopWatching.join(',')}`); + this.debug(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes})`).join(',')}`); + this.debug(`Request to stop watching: ${pathsToStopWatching.join(',')}`); - // Stop watching some roots - for (const root of rootsToStopWatching) { - this.pathWatchers[root].ready.then(watcher => watcher.stop()); - delete this.pathWatchers[root]; + // Stop watching as instructed + for (const pathToStopWatching of pathsToStopWatching) { + this.stopWatching(pathToStopWatching); } - // Start watching some roots - for (const root of rootsToStartWatching) { - this.doWatch(root); + // Start watching as instructed + for (const request of requestsToStartWatching) { + this.startWatching(request); } - // Refresh ignored arrays in case they changed - for (const root of roots) { - if (root.path in this.pathWatchers) { - this.pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => parse(ignored)) : []; + // Update ignore rules for all watchers + for (const request of normalizedRequests) { + const watcher = this.watchers.get(request.path); + if (watcher) { + watcher.request = request; + watcher.ignored = this.toExcludePatterns(request.excludes); } } } - private doWatch(request: IWatcherRequest): void { - let readyPromiseResolve: (watcher: IWatcher) => void; - this.pathWatchers[request.path] = { - ready: new Promise(resolve => readyPromiseResolve = resolve), - ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => parse(ignored)) : [] + private toExcludePatterns(excludes: string[] | undefined): ParsedPattern[] { + return Array.isArray(excludes) ? excludes.map(exclude => parse(exclude)) : []; + } + + private startWatching(request: IWatchRequest, restarts = 0): void { + const cts = new CancellationTokenSource(); + + let nsfwPromiseResolve: (watcher: nsfw.NSFW) => void; + const instance = new Promise(resolve => nsfwPromiseResolve = resolve); + + // Remember as watcher instance + const watcher: IWatcher = { + request, + instance, + ignored: this.toExcludePatterns(request.excludes), + restarts, + token: cts.token, + dispose: () => { + cts.dispose(true); + instance.then(instance => instance.stop()); + } + }; + this.watchers.set(request.path, watcher); + + // Path checks for symbolic links / wrong casing + const { realBasePathDiffers, realBasePathLength } = this.checkRequest(request); + + let undeliveredFileEvents: IDiskFileChange[] = []; + + const onRawFileEvent = (path: string, type: FileChangeType) => { + if (!this.isPathIgnored(path, watcher.ignored)) { + undeliveredFileEvents.push({ type, path }); + } else if (this.verboseLogging) { + this.log(` >> ignored ${path}`); + } }; - // NSFW does not report file changes in the path provided on macOS if - // - the path uses wrong casing - // - the path is a symbolic link - // We have to detect this case and massage the events to correct this. + nsfw(request.path, events => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed + } + + for (const event of events) { + + // Log the raw event before normalization or checking for ignore patterns + if (this.verboseLogging) { + const logPath = event.action === nsfw.actions.RENAMED ? `${join(event.directory, event.oldFile || '')} -> ${event.newFile}` : join(event.directory, event.file || ''); + this.log(`${event.action === nsfw.actions.CREATED ? '[ADDED]' : event.action === nsfw.actions.DELETED ? '[DELETED]' : event.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`); + } + + // Rename: convert into DELETE & ADD + if (event.action === nsfw.actions.RENAMED) { + onRawFileEvent(join(event.directory, event.oldFile || ''), FileChangeType.DELETED); // Rename fires when a file's name changes within a single directory + onRawFileEvent(join(event.newDirectory || event.directory, event.newFile || ''), FileChangeType.ADDED); + } + + // Created, modified, deleted: take as is + else { + onRawFileEvent(join(event.directory, event.file || ''), NsfwWatcherService.MAP_NSFW_ACTION_TO_FILE_CHANGE.get(event.action)!); + } + } + + // Reset undelivered events array + const undeliveredFileEventsToEmit = undeliveredFileEvents; + undeliveredFileEvents = []; + + // Broadcast to clients normalized + const normalizedEvents = normalizeFileChanges(this.normalizeEvents(undeliveredFileEventsToEmit, request, realBasePathDiffers, realBasePathLength)); + this.emitEvents(normalizedEvents); + }, this.getOptions(watcher)).then(async nsfwWatcher => { + + // Begin watching unless disposed already + if (!watcher.token.isCancellationRequested) { + await nsfwWatcher.start(); + } + + return nsfwWatcher; + }).then(nsfwWatcher => { + this.debug(`Started watching: ${request.path}`); + + nsfwPromiseResolve(nsfwWatcher); + }); + } + + protected getOptions(watcher: IWatcher): nsfw.Options { + return { + + // We must install an error callback, otherwise any error + // that is thrown from the watcher will result in process exit + errorCallback: error => { + if (!watcher.token.isCancellationRequested) { + this.onError(error, watcher); // error handling only if we are not disposed yet + } + }, + + // The default delay of NSFW is 500 but we want to + // react a bit faster than that. + debounceMS: 250 + }; + } + + private emitEvents(events: IDiskFileChange[]): void { + + // Send outside + this._onDidChangeFile.fire(events); + + // Logging + if (this.verboseLogging) { + for (const event of events) { + this.log(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); + } + } + } + + private checkRequest(request: IWatchRequest): { realBasePathDiffers: boolean, realBasePathLength: number } { let realBasePathDiffers = false; let realBasePathLength = request.path.length; + + // macOS: nsfw will report paths in their dereferenced and real casing + // form, so we need to detect this early on to be able to rewrite the + // file events to the original requested form. + // Note: Other platforms do not seem to have these path issues. if (isMacintosh) { try { @@ -127,137 +262,217 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { realBasePathLength = realBasePath.length; realBasePathDiffers = true; - this.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`); + this.warn(`correcting a path to watch that seems to be a symbolic link (original: ${request.path}, real: ${realBasePath})`); } } catch (error) { // ignore } } - this.debug(`Start watching with nsfw: ${request.path}`); + return { realBasePathDiffers, realBasePathLength }; + } - let undeliveredFileEvents: IDiskFileChange[] = []; - const fileEventDelayer = new ThrottledDelayer(NsfwWatcherService.FS_EVENT_DELAY); + private normalizeEvents(events: IDiskFileChange[], request: IWatchRequest, realBasePathDiffers: boolean, realBasePathLength: number): IDiskFileChange[] { + if (isMacintosh) { + for (const event of events) { - nsfw(request.path, events => { - for (const e of events) { + // Mac uses NFD unicode form on disk, but we want NFC + event.path = normalizeNFC(event.path); - // Logging - if (this.verboseLogging) { - const logPath = e.action === nsfw.actions.RENAMED ? join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : join(e.directory, e.file || ''); - this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`); + // Convert paths back to original form in case it differs + if (realBasePathDiffers) { + event.path = request.path + event.path.substr(realBasePathLength); + } + } + } + + return events; + } + + private onError(error: unknown, watcher?: IWatcher): void { + const msg = toErrorMessage(error); + + // Specially handle ENOSPC errors that can happen when + // the watcher consumes so many file descriptors that + // we are running into a limit. We only want to warn + // once in this case to avoid log spam. + // See https://github.com/microsoft/vscode/issues/7950 + if (msg.indexOf('Inotify limit reached') !== -1) { + if (!this.enospcErrorLogged) { + this.error('Inotify limit reached (ENOSPC)', watcher); + + this.enospcErrorLogged = true; + } + } + + // Any other error is unexpected and we should try to + // restart the watcher as a result to get into healthy + // state again. + else { + const handled = this.onUnexpectedError(msg, watcher); + if (!handled) { + this.error(`Unexpected error: ${msg} (EUNKNOWN)`, watcher); + } + } + } + + private onUnexpectedError(error: string, watcher?: IWatcher): boolean { + if (!watcher || watcher.restarts >= NsfwWatcherService.MAX_RESTARTS) { + return false; // we need a watcher that has not been restarted MAX_RESTARTS times already + } + + let handled = false; + + // Just try to restart watcher now if the path still exists + if (existsSync(watcher.request.path)) { + this.warn(`Watcher will be restarted due to unexpected error: ${error}`, watcher); + this.restartWatching(watcher); + + handled = true; + } + + // Otherwise try to monitor the path coming back before + // restarting the watcher + else { + handled = this.onWatchedPathDeleted(watcher); + } + + return handled; + } + + private onWatchedPathDeleted(watcher: IWatcher): boolean { + this.warn('Watcher shutdown because watched path got deleted', watcher); + + // Send a manual event given we know the root got deleted + this.emitEvents([{ path: watcher.request.path, type: FileChangeType.DELETED }]); + + const parentPath = dirname(watcher.request.path); + if (existsSync(parentPath)) { + const disposable = watchFolder(parentPath, (type, path) => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed } - // Convert nsfw event to `IRawFileChange` and add to queue - let absolutePath: string; - if (e.action === nsfw.actions.RENAMED) { - absolutePath = join(e.directory, e.oldFile || ''); // Rename fires when a file's name changes within a single directory + // Watcher path came back! Restart watching... + if (path === watcher.request.path && (type === 'added' || type === 'changed')) { + this.warn('Watcher restarts because watched path got created again', watcher); - if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { - undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); - } else if (this.verboseLogging) { - this.log(` >> ignored ${absolutePath}`); - } - - absolutePath = join(e.newDirectory || e.directory, e.newFile || ''); - - if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { - undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); - } else if (this.verboseLogging) { - this.log(` >> ignored ${absolutePath}`); - } - } else { - absolutePath = join(e.directory, e.file || ''); - - if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { - undeliveredFileEvents.push({ - type: nsfwActionToRawChangeType[e.action], - path: absolutePath - }); - } else if (this.verboseLogging) { - this.log(` >> ignored ${absolutePath}`); + // Stop watching that parent folder + disposable.dispose(); + + // Send a manual event given we know the root got added again + this.emitEvents([{ path: watcher.request.path, type: FileChangeType.ADDED }]); + + // Restart the file watching delayed + this.restartWatching(watcher); + } + }, error => { + // Ignore + }); + + // Make sure to stop watching when the watcher is disposed + watcher.token.onCancellationRequested(() => disposable.dispose()); + + return true; // handled + } + + return false; // not handled + } + + async stop(): Promise { + for (const [path] of this.watchers) { + this.stopWatching(path); + } + + this.watchers.clear(); + } + + private restartWatching(watcher: IWatcher, delay = 800): void { + + // Restart watcher delayed to accomodate for + // changes on disk that have triggered the + // need for a restart in the first place. + const scheduler = new RunOnceScheduler(() => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed + } + + // Stop/start watcher counting the restarts + this.stopWatching(watcher.request.path); + this.startWatching(watcher.request, watcher.restarts + 1); + }, delay); + scheduler.schedule(); + watcher.token.onCancellationRequested(() => scheduler.dispose()); + } + + private stopWatching(path: string): void { + const watcher = this.watchers.get(path); + if (watcher) { + watcher.dispose(); + this.watchers.delete(path); + } + } + + protected normalizeRequests(requests: IWatchRequest[]): IWatchRequest[] { + const requestTrie = TernarySearchTree.forPaths(); + + // Sort requests by path length to have shortest first + // to have a way to prevent children to be watched if + // parents exist. + requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length); + + // Only consider requests for watching that are not + // a child of an existing request path to prevent + // duplication. + // + // However, allow explicit requests to watch folders + // that are symbolic links because the NSFW watcher + // does not allow to recursively watch symbolic links. + for (const request of requests) { + if (requestTrie.findSubstr(request.path)) { + try { + const realpath = realpathSync(request.path); + if (realpath === request.path) { + this.warn(`ignoring a path for watching who's parent is already watched: ${request.path}`); + + continue; // path is not a symbolic link or similar } + } catch (error) { + continue; // invalid path - ignore from watching } } - // Delay and send buffer - fileEventDelayer.trigger(async () => { - const events = undeliveredFileEvents; - undeliveredFileEvents = []; + requestTrie.set(request.path, request); + } - if (isMacintosh) { - for (const e of events) { + return Array.from(requestTrie).map(([, request]) => request); + } - // Mac uses NFD unicode form on disk, but we want NFC - e.path = normalizeNFC(e.path); - - // Convert paths back to original form in case it differs - if (realBasePathDiffers) { - e.path = request.path + e.path.substr(realBasePathLength); - } - } - } - - // Broadcast to clients normalized - const normalizedEvents = normalizeFileChanges(events); - this._onDidChangeFile.fire(normalizedEvents); - - // Logging - if (this.verboseLogging) { - for (const e of normalizedEvents) { - this.log(` >> normalized ${e.type === FileChangeType.ADDED ? '[ADDED]' : e.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${e.path}`); - } - } - }); - }).then(watcher => { - this.pathWatchers[request.path].watcher = watcher; - const startPromise = watcher.start(); - startPromise.then(() => readyPromiseResolve(watcher)); - - return startPromise; - }); + private isPathIgnored(absolutePath: string, ignored: ParsedPattern[]): boolean { + return ignored.some(ignore => ignore(absolutePath)); } async setVerboseLogging(enabled: boolean): Promise { this.verboseLogging = enabled; } - async stop(): Promise { - for (let path in this.pathWatchers) { - let watcher = this.pathWatchers[path]; - watcher.ready.then(watcher => watcher.stop()); - - delete this.pathWatchers[path]; - } - - this.pathWatchers = Object.create(null); - } - - protected normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] { - // Normalizes a set of root paths by removing any root paths that are - // sub-paths of other roots. - return roots.filter(root => roots.every(otherRoot => { - return !(root.path.length > otherRoot.path.length && isEqualOrParent(root.path, otherRoot.path)); - })); - } - - private isPathIgnored(absolutePath: string, ignored: ParsedPattern[]): boolean { - return ignored && ignored.some(ignore => ignore(absolutePath)); - } - private log(message: string) { - this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (nsfw)] ` + message }); + this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message) }); } - private warn(message: string) { - this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (nsfw)] ` + message }); + private warn(message: string, watcher?: IWatcher) { + this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) }); } - private error(message: string) { - this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message }); + private error(message: string, watcher: IWatcher | undefined) { + this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, watcher) }); } - private debug(message: string) { - this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (nsfw)] ` + message }); + private debug(message: string): void { + this._onDidLogMessage.fire({ type: 'debug', message: this.toMessage(message) }); + } + + private toMessage(message: string, watcher?: IWatcher): string { + return watcher ? `[File Watcher (nsfw)] ${message} (path: ${watcher.request.path})` : `[File Watcher (nsfw)] ${message}`; } } diff --git a/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts b/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts deleted file mode 100644 index 87b03afda5..0000000000 --- a/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; -import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher'; - -suite('NSFW Watcher Service', async () => { - - // Load `nsfwWatcherService` within the suite to prevent all tests - // from failing to start if `nsfw` was not properly installed - const { NsfwWatcherService } = await import('vs/platform/files/node/watcher/nsfw/nsfwWatcherService'); - - class TestNsfwWatcherService extends NsfwWatcherService { - - testNormalizeRoots(roots: string[]): string[] { - - // Work with strings as paths to simplify testing - const requests: IWatcherRequest[] = roots.map(r => { - return { path: r, excludes: [] }; - }); - - return this.normalizeRoots(requests).map(r => r.path); - } - } - - suite('_normalizeRoots', () => { - test('should not impacts roots that don\'t overlap', () => { - const service = new TestNsfwWatcherService(); - if (platform.isWindows) { - assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a']), ['C:\\a']); - assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); - } else { - assert.deepStrictEqual(service.testNormalizeRoots(['/a']), ['/a']); - assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/b']), ['/a', '/b']); - assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); - } - }); - - test('should remove sub-folders of other roots', () => { - const service = new TestNsfwWatcherService(); - if (platform.isWindows) { - assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']); - assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(service.testNormalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); - } else { - assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/a/b']), ['/a']); - assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepStrictEqual(service.testNormalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']); - } - }); - }); -}); diff --git a/src/vs/platform/files/node/watcher/nsfw/watcher.ts b/src/vs/platform/files/node/watcher/nsfw/watcher.ts index 1ae34fac5d..df7c2ad430 100644 --- a/src/vs/platform/files/node/watcher/nsfw/watcher.ts +++ b/src/vs/platform/files/node/watcher/nsfw/watcher.ts @@ -4,20 +4,36 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; - -export interface IWatcherRequest { - path: string; - excludes: string[]; -} +import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/common/watcher'; export interface IWatcherService { + /** + * A normalized file change event from the raw events + * the watcher emits. + */ readonly onDidChangeFile: Event; + + /** + * An event to indicate a message that should get logged. + */ readonly onDidLogMessage: Event; - setRoots(roots: IWatcherRequest[]): Promise; + /** + * Configures the watcher service to watch according + * to the requests. Any existing watched path that + * is not in the array, will be removed from watching + * and any new path will be added to watching. + */ + watch(requests: IWatchRequest[]): Promise; + + /** + * Enable verbose logging in the watcher. + */ setVerboseLogging(enabled: boolean): Promise; + /** + * Stop all watchers. + */ stop(): Promise; } diff --git a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts index 4686d8b878..abf2f6a0a8 100644 --- a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts @@ -3,32 +3,25 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; -import { IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher'; -import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; +import { IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher'; +import { IDiskFileChange, ILogMessage, IWatchRequest, WatcherService } from 'vs/platform/files/common/watcher'; -export class FileWatcher extends Disposable { - - private static readonly MAX_RESTARTS = 5; +export class FileWatcher extends WatcherService { private service: IWatcherService | undefined; - private isDisposed: boolean; - private restartCounter: number; + + private isDisposed = false; constructor( - private folders: IWatcherRequest[], private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void, private readonly onLogMessage: (msg: ILogMessage) => void, - private verboseLogging: boolean, + private verboseLogging: boolean ) { super(); - this.isDisposed = false; - this.restartCounter = 0; - this.startWatching(); } @@ -37,7 +30,7 @@ export class FileWatcher extends Disposable { FileAccess.asFileUri('bootstrap-fork', require).fsPath, { serverName: 'File Watcher (nsfw)', - args: ['--type=watcherService'], + args: ['--type=watcherServiceNSFW'], env: { VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp', VSCODE_PIPE_LOGGING: 'true', @@ -46,20 +39,6 @@ export class FileWatcher extends Disposable { } )); - this._register(client.onDidProcessExit(() => { - // our watcher app should never be completed because it keeps on watching. being in here indicates - // that the watcher process died and we want to restart it here. we only do it a max number of times - if (!this.isDisposed) { - if (this.restartCounter <= FileWatcher.MAX_RESTARTS) { - this.error('terminated unexpectedly and is restarted again...'); - this.restartCounter++; - this.startWatching(); - } else { - this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!'); - } - } - })); - // Initialize watcher this.service = ProxyChannel.toService(getNextTickChannel(client.getChannel('watcher'))); this.service.setVerboseLogging(this.verboseLogging); @@ -67,15 +46,13 @@ export class FileWatcher extends Disposable { // Wire in event handlers this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e))); this._register(this.service.onDidLogMessage(e => this.onLogMessage(e))); - - // Start watching - this.setFolders(this.folders); } - setVerboseLogging(verboseLogging: boolean): void { + async setVerboseLogging(verboseLogging: boolean): Promise { this.verboseLogging = verboseLogging; - if (!this.isDisposed && this.service) { - this.service.setVerboseLogging(verboseLogging); + + if (!this.isDisposed) { + await this.service?.setVerboseLogging(verboseLogging); } } @@ -83,11 +60,9 @@ export class FileWatcher extends Disposable { this.onLogMessage({ type: 'error', message: `[File Watcher (nsfw)] ${message}` }); } - setFolders(folders: IWatcherRequest[]): void { - this.folders = folders; - - if (this.service) { - this.service.setRoots(folders); + async watch(requests: IWatchRequest[]): Promise { + if (!this.isDisposed) { + await this.service?.watch(requests); } } diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcherService.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcherService.ts new file mode 100644 index 0000000000..951327d474 --- /dev/null +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcherService.ts @@ -0,0 +1,661 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as parcelWatcher from '@parcel/watcher'; +import { existsSync, unlinkSync } from 'fs'; +import { tmpdir } from 'os'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { Emitter } from 'vs/base/common/event'; +import { isEqualOrParent } from 'vs/base/common/extpath'; +import { parse, ParsedPattern } from 'vs/base/common/glob'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { TernarySearchTree } from 'vs/base/common/map'; +import { normalizeNFC } from 'vs/base/common/normalization'; +import { dirname, isAbsolute, join, normalize, sep } from 'vs/base/common/path'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { rtrim } from 'vs/base/common/strings'; +import { generateUuid } from 'vs/base/common/uuid'; +import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; +import { watchFolder } from 'vs/base/node/watcher'; +import { FileChangeType } from 'vs/platform/files/common/files'; +import { IDiskFileChange, ILogMessage, normalizeFileChanges, IWatchRequest, IWatcherService } from 'vs/platform/files/common/watcher'; + +export interface IWatcher extends IDisposable { + + /** + * Signals when the watcher is ready to watch. + */ + readonly ready: Promise; + + /** + * The watch request associated to the watcher. + */ + readonly request: IWatchRequest; + + /** + * How often this watcher has been restarted in case of an unexpected + * shutdown. + */ + readonly restarts: number; + + /** + * The cancellation token associated with the lifecycle of the watcher. + */ + readonly token: CancellationToken; + + /** + * Stops and disposes the watcher. Same as `dispose` but allows to await + * the watcher getting unsubscribed. + */ + stop(): Promise; +} + +export class ParcelWatcherService extends Disposable implements IWatcherService { + + private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map( + [ + ['create', FileChangeType.ADDED], + ['update', FileChangeType.UPDATED], + ['delete', FileChangeType.DELETED] + ] + ); + + private static readonly GLOB_MARKERS = { + Star: '*', + GlobStar: '**', + GlobStarPosix: '**/**', + GlobStarWindows: '**\\**', + GlobStarPathStartPosix: '**/', + GlobStarPathEndPosix: '/**', + StarPathEndPosix: '/*', + GlobStarPathStartWindows: '**\\', + GlobStarPathEndWindows: '\\**' + }; + + private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events'; + + private readonly _onDidChangeFile = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChangeFile.event; + + private readonly _onDidLogMessage = this._register(new Emitter()); + readonly onDidLogMessage = this._onDidLogMessage.event; + + private readonly _onDidError = this._register(new Emitter()); + readonly onDidError = this._onDidError.event; + + protected readonly watchers = new Map(); + + private verboseLogging = false; + private enospcErrorLogged = false; + + constructor() { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Error handling on process + process.on('uncaughtException', error => this.onUnexpectedError(error)); + process.on('unhandledRejection', error => this.onUnexpectedError(error)); + } + + async watch(requests: IWatchRequest[]): Promise { + + // Figure out duplicates to remove from the requests + const normalizedRequests = this.normalizeRequests(requests); + + // Gather paths that we should start watching + const requestsToStartWatching = normalizedRequests.filter(request => { + const watcher = this.watchers.get(request.path); + if (!watcher) { + return true; // not yet watching that path + } + + // Re-watch path if excludes have changed or polling interval + return watcher.request.excludes !== request.excludes || watcher.request.pollingInterval !== request.pollingInterval; + }); + + // Gather paths that we should stop watching + const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { + return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && normalizedRequest.excludes === request.excludes && normalizedRequest.pollingInterval === request.pollingInterval); + }).map(({ request }) => request.path); + + // Logging + this.debug(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes})`).join(',')}`); + this.debug(`Request to stop watching: ${pathsToStopWatching.join(',')}`); + + // Stop watching as instructed + for (const pathToStopWatching of pathsToStopWatching) { + await this.stopWatching(pathToStopWatching); + } + + // Start watching as instructed + for (const request of requestsToStartWatching) { + if (request.pollingInterval) { + this.startPolling(request, request.pollingInterval); + } else { + this.startWatching(request); + } + } + } + + private toExcludePatterns(excludes: string[] | undefined): ParsedPattern[] { + return Array.isArray(excludes) ? excludes.map(exclude => parse(exclude)) : []; + } + + protected toExcludePaths(path: string, excludes: string[] | undefined): string[] | undefined { + if (!Array.isArray(excludes)) { + return undefined; + } + + const excludePaths = new Set(); + + // Parcel watcher currently does not support glob patterns + // for native exclusions. As long as that is the case, try + // to convert exclude patterns into absolute paths that the + // watcher supports natively to reduce the overhead at the + // level of the file watcher as much as possible. + // Refs: https://github.com/parcel-bundler/watcher/issues/64 + for (const exclude of excludes) { + const isGlob = exclude.includes(ParcelWatcherService.GLOB_MARKERS.Star); + + // Glob pattern: check for typical patterns and convert + let normalizedExclude: string | undefined = undefined; + if (isGlob) { + + // Examples: **, **/**, **\** + if ( + exclude === ParcelWatcherService.GLOB_MARKERS.GlobStar || + exclude === ParcelWatcherService.GLOB_MARKERS.GlobStarPosix || + exclude === ParcelWatcherService.GLOB_MARKERS.GlobStarWindows + ) { + normalizedExclude = path; + } + + // Examples: + // - **/node_modules/** + // - **/.git/objects/** + // - **/build-folder + // - output/** + else { + const startsWithGlobStar = exclude.startsWith(ParcelWatcherService.GLOB_MARKERS.GlobStarPathStartPosix) || exclude.startsWith(ParcelWatcherService.GLOB_MARKERS.GlobStarPathStartWindows); + const endsWithGlobStar = exclude.endsWith(ParcelWatcherService.GLOB_MARKERS.GlobStarPathEndPosix) || exclude.endsWith(ParcelWatcherService.GLOB_MARKERS.GlobStarPathEndWindows); + if (startsWithGlobStar || endsWithGlobStar) { + if (startsWithGlobStar && endsWithGlobStar) { + normalizedExclude = exclude.substring(ParcelWatcherService.GLOB_MARKERS.GlobStarPathStartPosix.length, exclude.length - ParcelWatcherService.GLOB_MARKERS.GlobStarPathEndPosix.length); + } else if (startsWithGlobStar) { + normalizedExclude = exclude.substring(ParcelWatcherService.GLOB_MARKERS.GlobStarPathStartPosix.length); + } else { + normalizedExclude = exclude.substring(0, exclude.length - ParcelWatcherService.GLOB_MARKERS.GlobStarPathEndPosix.length); + } + } + + // Support even more glob patterns on Linux where we know + // that each folder requires a file handle to watch. + // Examples: + // - node_modules/* (full form: **/node_modules/*/**) + if (isLinux && normalizedExclude) { + const endsWithStar = normalizedExclude?.endsWith(ParcelWatcherService.GLOB_MARKERS.StarPathEndPosix); + if (endsWithStar) { + normalizedExclude = normalizedExclude.substring(0, normalizedExclude.length - ParcelWatcherService.GLOB_MARKERS.StarPathEndPosix.length); + } + } + } + } + + // Not a glob pattern, take as is + else { + normalizedExclude = exclude; + } + + if (!normalizedExclude || normalizedExclude.includes(ParcelWatcherService.GLOB_MARKERS.Star)) { + continue; // skip for parcel (will be applied later by our glob matching) + } + + // Absolute path: normalize to watched path and + // exclude if not a parent of it otherwise. + if (isAbsolute(normalizedExclude)) { + if (!isEqualOrParent(normalizedExclude, path, !isLinux)) { + continue; // exclude points to path outside of watched folder, ignore + } + + // convert to relative path to ensure we + // get the correct path casing going forward + normalizedExclude = normalizedExclude.substr(path.length); + } + + // Finally take as relative path joined to watched path + excludePaths.add(rtrim(join(path, normalizedExclude), sep)); + } + + if (excludePaths.size > 0) { + return Array.from(excludePaths); + } + + return undefined; + } + + private startPolling(request: IWatchRequest, pollingInterval: number, restarts = 0): void { + const cts = new CancellationTokenSource(); + + let parcelWatcherPromiseResolve: () => void; + const instance = new Promise(resolve => parcelWatcherPromiseResolve = resolve); + + const snapshotFile = join(tmpdir(), `vscode-watcher-snapshot-${generateUuid()}`); + + // Remember as watcher instance + const watcher: IWatcher = { + request, + ready: instance, + restarts, + token: cts.token, + stop: async () => { + cts.dispose(true); + pollingWatcher.dispose(); + unlinkSync(snapshotFile); + }, + dispose: () => { + watcher.stop(); + } + }; + this.watchers.set(request.path, watcher); + + // Path checks for symbolic links / wrong casing + const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); + + // Warm up exclude patterns for usage + const excludePatterns = this.toExcludePatterns(request.excludes); + + const ignore = this.toExcludePaths(realPath, watcher.request.excludes); + + this.debug(`Started watching: '${realPath}' with polling interval '${pollingInterval}' and native excludes '${ignore?.join(', ')}'`); + + let counter = 0; + + const pollingWatcher = new RunOnceScheduler(async () => { + counter++; + + if (cts.token.isCancellationRequested) { + return; + } + + // We already ran before, check for events since + if (counter > 1) { + const parcelEvents = await parcelWatcher.getEventsSince(realPath, snapshotFile, { ignore, backend: ParcelWatcherService.PARCEL_WATCHER_BACKEND }); + + if (cts.token.isCancellationRequested) { + return; + } + + // Handle & emit events + this.onParcelEvents(parcelEvents, watcher, excludePatterns, realPathDiffers, realPathLength); + } + + // Store a snapshot of files to the snapshot file + await parcelWatcher.writeSnapshot(realPath, snapshotFile, { ignore, backend: ParcelWatcherService.PARCEL_WATCHER_BACKEND }); + + // Signal we are ready now when the first snapshot was written + if (counter === 1) { + parcelWatcherPromiseResolve(); + } + + if (cts.token.isCancellationRequested) { + return; + } + + // Schedule again at the next interval + pollingWatcher.schedule(); + }, pollingInterval); + pollingWatcher.schedule(0); + } + + private startWatching(request: IWatchRequest, restarts = 0): void { + const cts = new CancellationTokenSource(); + + let parcelWatcherPromiseResolve: (watcher: parcelWatcher.AsyncSubscription | undefined) => void; + const instance = new Promise(resolve => parcelWatcherPromiseResolve = resolve); + + // Remember as watcher instance + const watcher: IWatcher = { + request, + ready: instance, + restarts, + token: cts.token, + stop: async () => { + cts.dispose(true); + + const watcherInstance = await instance; + await watcherInstance?.unsubscribe(); + }, + dispose: () => { + watcher.stop(); + } + }; + this.watchers.set(request.path, watcher); + + // Path checks for symbolic links / wrong casing + const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); + + // Warm up exclude patterns for usage + const excludePatterns = this.toExcludePatterns(request.excludes); + + const ignore = this.toExcludePaths(realPath, watcher.request.excludes); + parcelWatcher.subscribe(realPath, (error, parcelEvents) => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed + } + + // In any case of an error, treat this like a unhandled exception + // that might require the watcher to restart. We do not really know + // the state of parcel at this point and as such will try to restart + // up to our maximum of restarts. + if (error) { + this.onUnexpectedError(error, watcher); + } + + // Handle & emit events + this.onParcelEvents(parcelEvents, watcher, excludePatterns, realPathDiffers, realPathLength); + }, { + backend: ParcelWatcherService.PARCEL_WATCHER_BACKEND, + ignore + }).then(parcelWatcher => { + this.debug(`Started watching: '${realPath}' with backend '${ParcelWatcherService.PARCEL_WATCHER_BACKEND}' and native excludes '${ignore?.join(', ')}'`); + + parcelWatcherPromiseResolve(parcelWatcher); + }).catch(error => { + this.onUnexpectedError(error, watcher); + + parcelWatcherPromiseResolve(undefined); + }); + } + + private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: IWatcher, excludes: ParsedPattern[], realPathDiffers: boolean, realPathLength: number): void { + if (parcelEvents.length === 0) { + return; + } + + // Check for excludes + const rawEvents = this.handleExcludes(parcelEvents, excludes); + + // Normalize and detect root path deletes + const { events: normalizedEvents, rootDeleted } = this.normalizeEvents(rawEvents, watcher.request, realPathDiffers, realPathLength); + + // Broadcast to clients coalesced + const coalescedEvents = normalizeFileChanges(normalizedEvents); + this.emitEvents(coalescedEvents); + + // Handle root path delete if confirmed from coalseced events + if (rootDeleted && coalescedEvents.some(event => event.path === watcher.request.path && event.type === FileChangeType.DELETED)) { + this.onWatchedPathDeleted(watcher); + } + } + + private handleExcludes(parcelEvents: parcelWatcher.Event[], excludes: ParsedPattern[]): IDiskFileChange[] { + const events: IDiskFileChange[] = []; + + for (const { path, type: parcelEventType } of parcelEvents) { + const type = ParcelWatcherService.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(parcelEventType)!; + if (this.verboseLogging) { + this.log(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`); + } + + if (!this.isPathIgnored(path, excludes)) { + events.push({ type, path }); + } else { + if (this.verboseLogging) { + this.log(` >> ignored ${path}`); + } + } + } + + return events; + } + + private emitEvents(events: IDiskFileChange[]): void { + + // Send outside + this._onDidChangeFile.fire(events); + + // Logging + if (this.verboseLogging) { + for (const event of events) { + this.log(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); + } + } + } + + private normalizePath(request: IWatchRequest): { realPath: string, realPathDiffers: boolean, realPathLength: number } { + let realPath = request.path; + let realPathDiffers = false; + let realPathLength = request.path.length; + + try { + + // First check for symbolic link + realPath = realpathSync(request.path); + + // Second check for casing difference + if (request.path === realPath) { + realPath = realcaseSync(request.path) ?? request.path; + } + + // Correct watch path as needed + if (request.path !== realPath) { + realPathLength = realPath.length; + realPathDiffers = true; + + this.warn(`correcting a path to watch that seems to be a symbolic link (original: ${request.path}, real: ${realPath})`); + } + } catch (error) { + // ignore + } + + return { realPath, realPathDiffers, realPathLength }; + } + + private normalizeEvents(events: IDiskFileChange[], request: IWatchRequest, realPathDiffers: boolean, realPathLength: number): { events: IDiskFileChange[], rootDeleted: boolean } { + let rootDeleted = false; + + for (const event of events) { + + // Mac uses NFD unicode form on disk, but we want NFC + if (isMacintosh) { + event.path = normalizeNFC(event.path); + } + + // TODO@bpasero workaround for https://github.com/parcel-bundler/watcher/issues/68 + // where watching root drive letter adds extra backslashes. + if (isWindows) { + if (request.path.length <= 3) { // for ex. c:, C:\ + event.path = normalize(event.path); + } + } + + // Convert paths back to original form in case it differs + if (realPathDiffers) { + event.path = request.path + event.path.substr(realPathLength); + } + + // Check for root deleted + if (event.path === request.path && event.type === FileChangeType.DELETED) { + rootDeleted = true; + } + } + + return { events, rootDeleted }; + } + + private onWatchedPathDeleted(watcher: IWatcher): void { + this.warn('Watcher shutdown because watched path got deleted', watcher); + + const parentPath = dirname(watcher.request.path); + if (existsSync(parentPath)) { + const disposable = watchFolder(parentPath, (type, path) => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed + } + + // Watcher path came back! Restart watching... + if (path === watcher.request.path && (type === 'added' || type === 'changed')) { + this.warn('Watcher restarts because watched path got created again', watcher); + + // Stop watching that parent folder + disposable.dispose(); + + // Send a manual event given we know the root got added again + this.emitEvents([{ path: watcher.request.path, type: FileChangeType.ADDED }]); + + // Restart the file watching + this.restartWatching(watcher); + } + }, error => { + // Ignore + }); + + // Make sure to stop watching when the watcher is disposed + watcher.token.onCancellationRequested(() => disposable.dispose()); + } + } + + private onUnexpectedError(error: unknown, watcher?: IWatcher): void { + const msg = toErrorMessage(error); + + // Specially handle ENOSPC errors that can happen when + // the watcher consumes so many file descriptors that + // we are running into a limit. We only want to warn + // once in this case to avoid log spam. + // See https://github.com/microsoft/vscode/issues/7950 + if (msg.indexOf('No space left on device') !== -1) { + if (!this.enospcErrorLogged) { + this.error('Inotify limit reached (ENOSPC)', watcher); + + this.enospcErrorLogged = true; + } + } + + // Any other error is unexpected and we should try to + // restart the watcher as a result to get into healthy + // state again if possible and if not attempted too much + else { + this.error(`Unexpected error: ${msg} (EUNKNOWN)`, watcher); + + this._onDidError.fire(msg); + } + } + + async stop(): Promise { + for (const [path] of this.watchers) { + await this.stopWatching(path); + } + + this.watchers.clear(); + } + + protected restartWatching(watcher: IWatcher, delay = 800): void { + + // Restart watcher delayed to accomodate for + // changes on disk that have triggered the + // need for a restart in the first place. + const scheduler = new RunOnceScheduler(async () => { + if (watcher.token.isCancellationRequested) { + return; // return early when disposed + } + + // Await the watcher having stopped, as this is + // needed to properly re-watch the same path + await this.stopWatching(watcher.request.path); + + // Start watcher again counting the restarts + if (watcher.request.pollingInterval) { + this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1); + } else { + this.startWatching(watcher.request, watcher.restarts + 1); + } + }, delay); + + scheduler.schedule(); + watcher.token.onCancellationRequested(() => scheduler.dispose()); + } + + private async stopWatching(path: string): Promise { + const watcher = this.watchers.get(path); + if (watcher) { + this.watchers.delete(path); + + try { + await watcher.stop(); + } catch (error) { + this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher); + } + } + } + + protected normalizeRequests(requests: IWatchRequest[]): IWatchRequest[] { + const requestTrie = TernarySearchTree.forPaths(); + + // Sort requests by path length to have shortest first + // to have a way to prevent children to be watched if + // parents exist. + requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length); + + // Only consider requests for watching that are not + // a child of an existing request path to prevent + // duplication. + // + // However, allow explicit requests to watch folders + // that are symbolic links because the Parcel watcher + // does not allow to recursively watch symbolic links. + for (const request of requests) { + if (requestTrie.findSubstr(request.path)) { + try { + const realpath = realpathSync(request.path); + if (realpath === request.path) { + this.warn(`ignoring a path for watching who's parent is already watched: ${request.path}`); + + continue; // path is not a symbolic link or similar + } + } catch (error) { + continue; // invalid path - ignore from watching + } + } + + requestTrie.set(request.path, request); + } + + return Array.from(requestTrie).map(([, request]) => request); + } + + private isPathIgnored(absolutePath: string, ignored: ParsedPattern[]): boolean { + return ignored.some(ignore => ignore(absolutePath)); + } + + async setVerboseLogging(enabled: boolean): Promise { + this.verboseLogging = enabled; + } + + private log(message: string) { + this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message) }); + } + + private warn(message: string, watcher?: IWatcher) { + this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) }); + } + + private error(message: string, watcher: IWatcher | undefined) { + this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, watcher) }); + } + + private debug(message: string): void { + this._onDidLogMessage.fire({ type: 'debug', message: this.toMessage(message) }); + } + + private toMessage(message: string, watcher?: IWatcher): string { + return watcher ? `[File Watcher (parcel)] ${message} (path: ${watcher.request.path})` : `[File Watcher (parcel)] ${message}`; + } +} diff --git a/src/vs/platform/files/node/watcher/unix/watcherApp.ts b/src/vs/platform/files/node/watcher/parcel/watcherApp.ts similarity index 79% rename from src/vs/platform/files/node/watcher/unix/watcherApp.ts rename to src/vs/platform/files/node/watcher/parcel/watcherApp.ts index 95c985cc36..ea30439814 100644 --- a/src/vs/platform/files/node/watcher/unix/watcherApp.ts +++ b/src/vs/platform/files/node/watcher/parcel/watcherApp.ts @@ -5,8 +5,8 @@ import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; -import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService'; +import { ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/parcelWatcherService'; const server = new Server('watcher'); -const service = new ChokidarWatcherService(); +const service = new ParcelWatcherService(); server.registerChannel('watcher', ProxyChannel.fromService(service)); diff --git a/src/vs/platform/files/node/watcher/parcel/watcherService.ts b/src/vs/platform/files/node/watcher/parcel/watcherService.ts new file mode 100644 index 0000000000..539a0c36ff --- /dev/null +++ b/src/vs/platform/files/node/watcher/parcel/watcherService.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; +import { AbstractWatcherService, IDiskFileChange, ILogMessage, IWatcherService } from 'vs/platform/files/common/watcher'; + +export class FileWatcher extends AbstractWatcherService { + + constructor( + onFileChanges: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ) { + super(onFileChanges, onLogMessage, verboseLogging); + + this.init(); + } + + protected override createService(disposables: DisposableStore): IWatcherService { + + // Fork the parcel file watcher and build a client around + // its server for passing over requests and receiving events. + const client = disposables.add(new Client( + FileAccess.asFileUri('bootstrap-fork', require).fsPath, + { + serverName: 'File Watcher (parcel, node.js)', + args: ['--type=watcherServiceParcel'], + env: { + VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/parcel/watcherApp', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client + } + } + )); + + // React on unexpected termination of the watcher process + disposables.add(client.onDidProcessExit(({ code, signal }) => this.onError(`terminated by itself with code ${code}, signal: ${signal}`))); + + return ProxyChannel.toService(getNextTickChannel(client.getChannel('watcher'))); + } +} diff --git a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts deleted file mode 100644 index 35192394cc..0000000000 --- a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts +++ /dev/null @@ -1,374 +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 chokidar from 'chokidar'; -import * as fs from 'fs'; -import * as gracefulFs from 'graceful-fs'; -import { equals } from 'vs/base/common/arrays'; -import { ThrottledDelayer } from 'vs/base/common/async'; -import { Emitter } from 'vs/base/common/event'; -import { isEqualOrParent } from 'vs/base/common/extpath'; -import { match, parse, ParsedPattern } from 'vs/base/common/glob'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { normalizeNFC } from 'vs/base/common/normalization'; -import { isLinux, isMacintosh } from 'vs/base/common/platform'; -import { realcaseSync } from 'vs/base/node/extpath'; -import { FileChangeType } from 'vs/platform/files/common/files'; -import { IWatcherOptions, IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher'; -import { IDiskFileChange, ILogMessage, normalizeFileChanges } from 'vs/platform/files/node/watcher/watcher'; - -gracefulFs.gracefulify(fs); // enable gracefulFs - -process.noAsar = true; // disable ASAR support in watcher process - -interface IWatcher { - requests: ExtendedWatcherRequest[]; - stop(): Promise; -} - -interface ExtendedWatcherRequest extends IWatcherRequest { - parsedPattern?: ParsedPattern; -} - -export class ChokidarWatcherService extends Disposable implements IWatcherService { - - private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) - private static readonly EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000; // warn after certain time span of event spam - - private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChangeFile.event; - - private readonly _onDidLogMessage = this._register(new Emitter()); - readonly onDidLogMessage = this._onDidLogMessage.event; - - private watchers = new Map(); - - private _watcherCount = 0; - get wacherCount() { return this._watcherCount; } - - private pollingInterval?: number; - private usePolling?: boolean | string[]; - private verboseLogging: boolean | undefined; - - private spamCheckStartTime: number | undefined; - private spamWarningLogged: boolean | undefined; - private enospcErrorLogged: boolean | undefined; - - async init(options: IWatcherOptions): Promise { - this.pollingInterval = options.pollingInterval; - this.usePolling = options.usePolling; - this.watchers.clear(); - this._watcherCount = 0; - this.verboseLogging = options.verboseLogging; - } - - async setVerboseLogging(enabled: boolean): Promise { - this.verboseLogging = enabled; - } - - async setRoots(requests: IWatcherRequest[]): Promise { - const watchers = new Map(); - const newRequests: string[] = []; - - const requestsByBasePath = normalizeRoots(requests); - - // evaluate new & remaining watchers - for (const basePath in requestsByBasePath) { - const watcher = this.watchers.get(basePath); - if (watcher && isEqualRequests(watcher.requests, requestsByBasePath[basePath])) { - watchers.set(basePath, watcher); - this.watchers.delete(basePath); - } else { - newRequests.push(basePath); - } - } - - // stop all old watchers - for (const [, watcher] of this.watchers) { - await watcher.stop(); - } - - // start all new watchers - for (const basePath of newRequests) { - const requests = requestsByBasePath[basePath]; - watchers.set(basePath, this.watch(basePath, requests)); - } - - this.watchers = watchers; - } - - private watch(basePath: string, requests: IWatcherRequest[]): IWatcher { - const pollingInterval = this.pollingInterval || 5000; - let usePolling = this.usePolling; // boolean or a list of path patterns - if (Array.isArray(usePolling)) { - // switch to polling if one of the paths matches with a watched path - usePolling = usePolling.some(pattern => requests.some(request => match(pattern, request.path))); - } - - const watcherOpts: chokidar.WatchOptions = { - ignoreInitial: true, - ignorePermissionErrors: true, - followSymlinks: true, // this is the default of chokidar and supports file events through symlinks - interval: pollingInterval, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals - binaryInterval: pollingInterval, - usePolling, - disableGlobbing: true // fix https://github.com/microsoft/vscode/issues/4586 - }; - - const excludes: string[] = []; - - const isSingleFolder = requests.length === 1; - if (isSingleFolder) { - excludes.push(...requests[0].excludes); // if there's only one request, use the built-in ignore-filterering - } - - if ((isMacintosh || isLinux) && (basePath.length === 0 || basePath === '/')) { - excludes.push('/dev/**'); - if (isLinux) { - excludes.push('/proc/**', '/sys/**'); - } - } - - excludes.push('**/*.asar'); // Ensure we never recurse into ASAR archives - - watcherOpts.ignored = excludes; - - // Chokidar fails when the basePath does not match case-identical to the path on disk - // so we have to find the real casing of the path and do some path massaging to fix this - // see https://github.com/paulmillr/chokidar/issues/418 - const realBasePath = isMacintosh ? (realcaseSync(basePath) || basePath) : basePath; - const realBasePathLength = realBasePath.length; - const realBasePathDiffers = (basePath !== realBasePath); - - if (realBasePathDiffers) { - this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`); - } - - this.debug(`Start watching: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`); - - let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts); - this._watcherCount++; - - // Detect if for some reason the native watcher library fails to load - if (isMacintosh && chokidarWatcher.options && !chokidarWatcher.options.useFsEvents) { - this.warn('Watcher is not using native fsevents library and is falling back to unefficient polling.'); - } - - let undeliveredFileEvents: IDiskFileChange[] = []; - let fileEventDelayer: ThrottledDelayer | null = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY); - - const watcher: IWatcher = { - requests, - stop: async () => { - try { - if (this.verboseLogging) { - this.log(`Stop watching: ${basePath}]`); - } - - if (chokidarWatcher) { - await chokidarWatcher.close(); - this._watcherCount--; - chokidarWatcher = null; - } - - if (fileEventDelayer) { - fileEventDelayer.cancel(); - fileEventDelayer = null; - } - } catch (error) { - this.warn('Error while stopping watcher: ' + error.toString()); - } - } - }; - - chokidarWatcher.on('all', (type: string, path: string) => { - if (isMacintosh) { - // Mac: uses NFD unicode form on disk, but we want NFC - // See also https://github.com/nodejs/node/issues/2165 - path = normalizeNFC(path); - } - - if (path.indexOf(realBasePath) < 0) { - return; // we really only care about absolute paths here in our basepath context here - } - - // Make sure to convert the path back to its original basePath form if the realpath is different - if (realBasePathDiffers) { - path = basePath + path.substr(realBasePathLength); - } - - let eventType: FileChangeType; - switch (type) { - case 'change': - eventType = FileChangeType.UPDATED; - break; - case 'add': - case 'addDir': - eventType = FileChangeType.ADDED; - break; - case 'unlink': - case 'unlinkDir': - eventType = FileChangeType.DELETED; - break; - default: - return; - } - - // if there's more than one request we need to do - // extra filtering due to potentially overlapping roots - if (!isSingleFolder) { - if (isIgnored(path, watcher.requests)) { - return; - } - } - - const event = { type: eventType, path }; - - // Logging - if (this.verboseLogging) { - this.log(`${eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`); - } - - // Check for spam - const now = Date.now(); - if (undeliveredFileEvents.length === 0) { - this.spamWarningLogged = false; - this.spamCheckStartTime = now; - } else if (!this.spamWarningLogged && typeof this.spamCheckStartTime === 'number' && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) { - this.spamWarningLogged = true; - this.warn(`Watcher is busy catching up with ${undeliveredFileEvents.length} file changes in 60 seconds. Latest changed path is "${event.path}"`); - } - - // Add to buffer - undeliveredFileEvents.push(event); - - if (fileEventDelayer) { - - // Delay and send buffer - fileEventDelayer.trigger(async () => { - const events = undeliveredFileEvents; - undeliveredFileEvents = []; - - // Broadcast to clients normalized - const normalizedEvents = normalizeFileChanges(events); - this._onDidChangeFile.fire(normalizedEvents); - - // Logging - if (this.verboseLogging) { - for (const e of normalizedEvents) { - this.log(` >> normalized ${e.type === FileChangeType.ADDED ? '[ADDED]' : e.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${e.path}`); - } - } - - return undefined; - }); - } - }); - - chokidarWatcher.on('error', (error: NodeJS.ErrnoException) => { - if (error) { - - // Specially handle ENOSPC errors that can happen when - // the watcher consumes so many file descriptors that - // we are running into a limit. We only want to warn - // once in this case to avoid log spam. - // See https://github.com/microsoft/vscode/issues/7950 - if (error.code === 'ENOSPC') { - if (!this.enospcErrorLogged) { - this.enospcErrorLogged = true; - this.stop(); - this.error('Inotify limit reached (ENOSPC)'); - } - } else { - this.warn(error.toString()); - } - } - }); - return watcher; - } - - async stop(): Promise { - for (const [, watcher] of this.watchers) { - await watcher.stop(); - } - - this.watchers.clear(); - } - - private log(message: string) { - this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message }); - } - - private debug(message: string) { - this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (chokidar)] ` + message }); - } - - private warn(message: string) { - this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message }); - } - - private error(message: string) { - this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (chokidar)] ` + message }); - } -} - -function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { - for (const request of requests) { - if (request.path === path) { - return false; - } - - if (isEqualOrParent(path, request.path)) { - if (!request.parsedPattern) { - if (request.excludes && request.excludes.length > 0) { - const pattern = `{${request.excludes.join(',')}}`; - request.parsedPattern = parse(pattern); - } else { - request.parsedPattern = () => false; - } - } - - const relPath = path.substr(request.path.length + 1); - if (!request.parsedPattern(relPath)) { - return false; - } - } - } - - return true; -} - -/** - * Normalizes a set of root paths by grouping by the most parent root path. - * equests with Sub paths are skipped if they have the same ignored set as the parent. - */ -export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string]: IWatcherRequest[] } { - requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path)); - - let prevRequest: IWatcherRequest | null = null; - const result: { [basePath: string]: IWatcherRequest[] } = Object.create(null); - for (const request of requests) { - const basePath = request.path; - const ignored = (request.excludes || []).sort(); - if (prevRequest && (isEqualOrParent(basePath, prevRequest.path))) { - if (!isEqualIgnore(ignored, prevRequest.excludes)) { - result[prevRequest.path].push({ path: basePath, excludes: ignored }); - } - } else { - prevRequest = { path: basePath, excludes: ignored }; - result[basePath] = [prevRequest]; - } - } - - return result; -} - -function isEqualRequests(r1: readonly IWatcherRequest[], r2: readonly IWatcherRequest[]) { - return equals(r1, r2, (a, b) => a.path === b.path && isEqualIgnore(a.excludes, b.excludes)); -} - -function isEqualIgnore(i1: readonly string[], i2: readonly string[]) { - return equals(i1, i2); -} diff --git a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts b/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts deleted file mode 100644 index 5e80847515..0000000000 --- a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; -import { IWatcherRequest } from 'vs/platform/files/node/watcher/unix/watcher'; - -suite('Chokidar normalizeRoots', async () => { - - // Load `chokidarWatcherService` within the suite to prevent all tests - // from failing to start if `chokidar` was not properly installed - const { normalizeRoots } = await import('vs/platform/files/node/watcher/unix/chokidarWatcherService'); - - function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest { - return { path: basePath, excludes: ignored }; - } - - function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) { - const requests = inputPaths.map(path => newRequest(path)); - const actual = normalizeRoots(requests); - assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths); - } - - function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) { - const actual = normalizeRoots(inputRequests); - const actualPath = Object.keys(actual).sort(); - const expectedPaths = Object.keys(expectedRequests).sort(); - assert.deepStrictEqual(actualPath, expectedPaths); - for (let path of actualPath) { - let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); - let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); - assert.deepStrictEqual(a, e); - } - } - - test('should not impacts roots that don\'t overlap', () => { - if (platform.isWindows) { - assertNormalizedRootPath(['C:\\a'], ['C:\\a']); - assertNormalizedRootPath(['C:\\a', 'C:\\b'], ['C:\\a', 'C:\\b']); - assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\c\\d\\e'], ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); - } else { - assertNormalizedRootPath(['/a'], ['/a']); - assertNormalizedRootPath(['/a', '/b'], ['/a', '/b']); - assertNormalizedRootPath(['/a', '/b', '/c/d/e'], ['/a', '/b', '/c/d/e']); - } - }); - - test('should remove sub-folders of other roots', () => { - if (platform.isWindows) { - assertNormalizedRootPath(['C:\\a', 'C:\\a\\b'], ['C:\\a']); - assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']); - assertNormalizedRootPath(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']); - assertNormalizedRootPath(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d'], ['C:\\a']); - } else { - assertNormalizedRootPath(['/a', '/a/b'], ['/a']); - assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']); - assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']); - assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']); - assertNormalizedRootPath(['/a/c/d/e', '/a/b/d', '/a/c/d', '/a/c/e/f', '/a/b'], ['/a/b', '/a/c/d', '/a/c/e/f']); - } - }); - - test('should remove duplicates', () => { - if (platform.isWindows) { - assertNormalizedRootPath(['C:\\a', 'C:\\a\\', 'C:\\a'], ['C:\\a']); - } else { - assertNormalizedRootPath(['/a', '/a/', '/a'], ['/a']); - assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']); - assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']); - assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']); - } - }); - - test('nested requests', () => { - let p1, p2, p3; - if (platform.isWindows) { - p1 = 'C:\\a'; - p2 = 'C:\\a\\b'; - p3 = 'C:\\a\\b\\c'; - } else { - p1 = '/a'; - p2 = '/a/b'; - p3 = '/a/b/c'; - } - const r1 = newRequest(p1, ['**/*.ts']); - const r2 = newRequest(p2, ['**/*.js']); - const r3 = newRequest(p3, ['**/*.ts']); - assertNormalizedRequests([r1, r2], { [p1]: [r1, r2] }); - assertNormalizedRequests([r2, r1], { [p1]: [r1, r2] }); - assertNormalizedRequests([r1, r2, r3], { [p1]: [r1, r2, r3] }); - assertNormalizedRequests([r1, r3], { [p1]: [r1] }); - assertNormalizedRequests([r2, r3], { [p2]: [r2, r3] }); - }); -}); diff --git a/src/vs/platform/files/node/watcher/unix/watcher.ts b/src/vs/platform/files/node/watcher/unix/watcher.ts deleted file mode 100644 index eac100cf1f..0000000000 --- a/src/vs/platform/files/node/watcher/unix/watcher.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from 'vs/base/common/event'; -import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; - -export interface IWatcherRequest { - path: string; - excludes: string[]; -} - -export interface IWatcherOptions { - pollingInterval?: number; - usePolling?: boolean | string[]; // boolean or a set of glob patterns matching folders that need polling - verboseLogging?: boolean; -} - -export interface IWatcherService { - - readonly onDidChangeFile: Event; - readonly onDidLogMessage: Event; - - init(options: IWatcherOptions): Promise; - - setRoots(roots: IWatcherRequest[]): Promise; - setVerboseLogging(enabled: boolean): Promise; - - stop(): Promise; -} diff --git a/src/vs/platform/files/node/watcher/unix/watcherService.ts b/src/vs/platform/files/node/watcher/unix/watcherService.ts deleted file mode 100644 index bb92dd77d4..0000000000 --- a/src/vs/platform/files/node/watcher/unix/watcherService.ts +++ /dev/null @@ -1,100 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { FileAccess } from 'vs/base/common/network'; -import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; -import { IWatcherOptions, IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher'; -import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; - -export class FileWatcher extends Disposable { - - private static readonly MAX_RESTARTS = 5; - - private isDisposed: boolean; - private restartCounter: number; - private service: IWatcherService | undefined; - - constructor( - private folders: IWatcherRequest[], - private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void, - private readonly onLogMessage: (msg: ILogMessage) => void, - private verboseLogging: boolean, - private readonly watcherOptions: IWatcherOptions = {} - ) { - super(); - - this.isDisposed = false; - this.restartCounter = 0; - - this.startWatching(); - } - - private startWatching(): void { - const client = this._register(new Client( - FileAccess.asFileUri('bootstrap-fork', require).fsPath, - { - serverName: 'File Watcher (chokidar)', - args: ['--type=watcherService'], - env: { - VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp', - VSCODE_PIPE_LOGGING: 'true', - VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client - } - } - )); - - this._register(client.onDidProcessExit(() => { - // our watcher app should never be completed because it keeps on watching. being in here indicates - // that the watcher process died and we want to restart it here. we only do it a max number of times - if (!this.isDisposed) { - if (this.restartCounter <= FileWatcher.MAX_RESTARTS) { - this.error('terminated unexpectedly and is restarted again...'); - this.restartCounter++; - this.startWatching(); - } else { - this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!'); - } - } - })); - - // Initialize watcher - this.service = ProxyChannel.toService(getNextTickChannel(client.getChannel('watcher'))); - this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging }); - - this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e))); - this._register(this.service.onDidLogMessage(e => this.onLogMessage(e))); - - // Start watching - this.service.setRoots(this.folders); - } - - error(message: string) { - this.onLogMessage({ type: 'error', message: `[File Watcher (chokidar)] ${message}` }); - } - - setVerboseLogging(verboseLogging: boolean): void { - this.verboseLogging = verboseLogging; - - if (this.service) { - this.service.setVerboseLogging(verboseLogging); - } - } - - setFolders(folders: IWatcherRequest[]): void { - this.folders = folders; - - if (this.service) { - this.service.setRoots(folders); - } - } - - override dispose(): void { - this.isDisposed = true; - - super.dispose(); - } -} diff --git a/src/vs/platform/files/node/watcher/watcher.ts b/src/vs/platform/files/node/watcher/watcher.ts deleted file mode 100644 index 1efd60a6e8..0000000000 --- a/src/vs/platform/files/node/watcher/watcher.ts +++ /dev/null @@ -1,109 +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 { isLinux } from 'vs/base/common/platform'; -import { URI as uri } from 'vs/base/common/uri'; -import { FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files'; - -export interface IDiskFileChange { - type: FileChangeType; - path: string; -} - -export interface ILogMessage { - type: 'trace' | 'warn' | 'error' | 'info' | 'debug'; - message: string; -} - -export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] { - return changes.map(change => ({ - type: change.type, - resource: uri.file(change.path) - })); -} - -export function normalizeFileChanges(changes: IDiskFileChange[]): IDiskFileChange[] { - - // Build deltas - const normalizer = new EventNormalizer(); - for (const event of changes) { - normalizer.processEvent(event); - } - - return normalizer.normalize(); -} - -class EventNormalizer { - private normalized: IDiskFileChange[] = []; - private mapPathToChange: Map = new Map(); - - processEvent(event: IDiskFileChange): void { - const existingEvent = this.mapPathToChange.get(event.path); - - // Event path already exists - if (existingEvent) { - const currentChangeType = existingEvent.type; - const newChangeType = event.type; - - // ignore CREATE followed by DELETE in one go - if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.DELETED) { - this.mapPathToChange.delete(event.path); - this.normalized.splice(this.normalized.indexOf(existingEvent), 1); - } - - // flatten DELETE followed by CREATE into CHANGE - else if (currentChangeType === FileChangeType.DELETED && newChangeType === FileChangeType.ADDED) { - existingEvent.type = FileChangeType.UPDATED; - } - - // Do nothing. Keep the created event - else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) { } - - // Otherwise apply change type - else { - existingEvent.type = newChangeType; - } - } - - // Otherwise store new - else { - this.normalized.push(event); - this.mapPathToChange.set(event.path, event); - } - } - - normalize(): IDiskFileChange[] { - const addedChangeEvents: IDiskFileChange[] = []; - const deletedPaths: string[] = []; - - // This algorithm will remove all DELETE events up to the root folder - // that got deleted if any. This ensures that we are not producing - // DELETE events for each file inside a folder that gets deleted. - // - // 1.) split ADD/CHANGE and DELETED events - // 2.) sort short deleted paths to the top - // 3.) for each DELETE, check if there is a deleted parent and ignore the event in that case - return this.normalized.filter(e => { - if (e.type !== FileChangeType.DELETED) { - addedChangeEvents.push(e); - - return false; // remove ADD / CHANGE - } - - return true; // keep DELETE - }).sort((e1, e2) => { - return e1.path.length - e2.path.length; // shortest path first - }).filter(e => { - if (deletedPaths.some(deletedPath => isParent(e.path, deletedPath, !isLinux /* ignorecase */))) { - return false; // DELETE is ignored if parent is deleted already - } - - // otherwise mark as deleted - deletedPaths.push(e.path); - - return true; - }).concat(addedChangeEvents); - } -} diff --git a/src/vs/platform/files/node/watcher/win32/CodeHelper.exe b/src/vs/platform/files/node/watcher/win32/CodeHelper.exe deleted file mode 100644 index 3574c753fb0b294a42ef76fe36ac3bc939daafa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57856 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 Yp-ImJ6Nw38lVKSyd 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 diff --git a/src/vs/platform/files/node/watcher/win32/CodeHelper.md b/src/vs/platform/files/node/watcher/win32/CodeHelper.md deleted file mode 100644 index e63983bbc4..0000000000 --- a/src/vs/platform/files/node/watcher/win32/CodeHelper.md +++ /dev/null @@ -1,8 +0,0 @@ -# Native File Watching for Windows using C# FileSystemWatcher - -- Repository: https://github.com/microsoft/vscode-filewatcher-windows - -# Build - -- Build in "Release" config -- Copy CodeHelper.exe over into this folder diff --git a/src/vs/platform/files/node/watcher/win32/csharpWatcherService.ts b/src/vs/platform/files/node/watcher/win32/csharpWatcherService.ts deleted file mode 100644 index b915de2903..0000000000 --- a/src/vs/platform/files/node/watcher/win32/csharpWatcherService.ts +++ /dev/null @@ -1,142 +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 { ChildProcess, spawn } from 'child_process'; -import { parse, ParsedPattern } from 'vs/base/common/glob'; -import { FileAccess } from 'vs/base/common/network'; -import { LineDecoder } from 'vs/base/node/decoder'; -import { FileChangeType } from 'vs/platform/files/common/files'; -import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; - -export class OutOfProcessWin32FolderWatcher { - - private static readonly MAX_RESTARTS = 5; - - private static readonly changeTypeMap = [FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED]; - - private readonly ignored: ParsedPattern[]; - - private handle: ChildProcess | undefined; - private restartCounter: number; - - constructor( - private watchedFolder: string, - ignored: string[], - private eventCallback: (events: IDiskFileChange[]) => void, - private logCallback: (message: ILogMessage) => void, - private verboseLogging: boolean - ) { - this.restartCounter = 0; - - if (Array.isArray(ignored)) { - this.ignored = ignored.map(ignore => parse(ignore)); - } else { - this.ignored = []; - } - - // Logging - if (this.verboseLogging) { - this.log(`Start watching: ${watchedFolder}, excludes: ${ignored.join(',')}`); - } - - this.startWatcher(); - } - - private startWatcher(): void { - const args = [this.watchedFolder]; - if (this.verboseLogging) { - args.push('-verbose'); - } - - this.handle = spawn(FileAccess.asFileUri('vs/platform/files/node/watcher/win32/CodeHelper.exe', require).fsPath, args); - - const stdoutLineDecoder = new LineDecoder(); - - // Events over stdout - this.handle.stdout!.on('data', (data: Buffer) => { - - // Collect raw events from output - const rawEvents: IDiskFileChange[] = []; - for (const line of stdoutLineDecoder.write(data)) { - const eventParts = line.split('|'); - if (eventParts.length === 2) { - const changeType = Number(eventParts[0]); - const absolutePath = eventParts[1]; - - // File Change Event (0 Changed, 1 Created, 2 Deleted) - if (changeType >= 0 && changeType < 3) { - - // Support ignores - if (this.ignored && this.ignored.some(ignore => ignore(absolutePath))) { - if (this.verboseLogging) { - this.log(absolutePath); - } - - continue; - } - - // Otherwise record as event - rawEvents.push({ - type: OutOfProcessWin32FolderWatcher.changeTypeMap[changeType], - path: absolutePath - }); - } - - // 3 Logging - else { - this.log(eventParts[1]); - } - } - } - - // Trigger processing of events through the delayer to batch them up properly - if (rawEvents.length > 0) { - this.eventCallback(rawEvents); - } - }); - - // Errors - this.handle.on('error', (error: Error) => this.onError(error)); - this.handle.stderr!.on('data', (data: Buffer) => this.onError(data)); - - // Exit - this.handle.on('exit', (code: number, signal: string) => this.onExit(code, signal)); - } - - private onError(error: Error | Buffer): void { - this.error('process error: ' + error.toString()); - } - - private onExit(code: number, signal: string): void { - if (this.handle) { - - // exit while not yet being disposed is unexpected! - this.error(`terminated unexpectedly (code: ${code}, signal: ${signal})`); - - if (this.restartCounter <= OutOfProcessWin32FolderWatcher.MAX_RESTARTS) { - this.error('is restarted again...'); - this.restartCounter++; - this.startWatcher(); // restart - } else { - this.error('Watcher failed to start after retrying for some time, giving up. Please report this as a bug report!'); - } - } - } - - private error(message: string) { - this.logCallback({ type: 'error', message: `[File Watcher (C#)] ${message}` }); - } - - private log(message: string) { - this.logCallback({ type: 'trace', message: `[File Watcher (C#)] ${message}` }); - } - - dispose(): void { - if (this.handle) { - this.handle.kill(); - this.handle = undefined; - } - } -} diff --git a/src/vs/platform/files/node/watcher/win32/watcherService.ts b/src/vs/platform/files/node/watcher/win32/watcherService.ts deleted file mode 100644 index f13b53ed40..0000000000 --- a/src/vs/platform/files/node/watcher/win32/watcherService.ts +++ /dev/null @@ -1,75 +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 { IDisposable } from 'vs/base/common/lifecycle'; -import { posix } from 'vs/base/common/path'; -import { rtrim } from 'vs/base/common/strings'; -import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; -import { OutOfProcessWin32FolderWatcher } from 'vs/platform/files/node/watcher/win32/csharpWatcherService'; - -export class FileWatcher implements IDisposable { - - private folder: { path: string, excludes: string[] }; - private service: OutOfProcessWin32FolderWatcher | undefined = undefined; - - constructor( - folders: { path: string, excludes: string[] }[], - private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void, - private readonly onLogMessage: (msg: ILogMessage) => void, - private verboseLogging: boolean - ) { - this.folder = folders[0]; - - if (this.folder.path.indexOf('\\\\') === 0 && this.folder.path.endsWith(posix.sep)) { - // for some weird reason, node adds a trailing slash to UNC paths - // we never ever want trailing slashes as our base path unless - // someone opens root ("/"). - // See also https://github.com/nodejs/io.js/issues/1765 - this.folder.path = rtrim(this.folder.path, posix.sep); - } - - this.service = this.startWatching(); - } - - private get isDisposed(): boolean { - return !this.service; - } - - private startWatching(): OutOfProcessWin32FolderWatcher { - return new OutOfProcessWin32FolderWatcher( - this.folder.path, - this.folder.excludes, - events => this.onFileEvents(events), - message => this.onLogMessage(message), - this.verboseLogging - ); - } - - setVerboseLogging(verboseLogging: boolean): void { - this.verboseLogging = verboseLogging; - if (this.service) { - this.service.dispose(); - this.service = this.startWatching(); - } - } - - private onFileEvents(events: IDiskFileChange[]): void { - if (this.isDisposed) { - return; - } - - // Emit through event emitter - if (events.length > 0) { - this.onDidFilesChange(events); - } - } - - dispose(): void { - if (this.service) { - this.service.dispose(); - this.service = undefined; - } - } -} diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 3df15e8278..50b0c33f5f 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -21,7 +21,8 @@ suite('File Service', () => { const resource = URI.parse('test://foo/bar'); const provider = new NullFileSystemProvider(); - assert.strictEqual(service.canHandleResource(resource), false); + assert.strictEqual(await service.canHandleResource(resource), false); + assert.strictEqual(service.hasProvider(resource), false); assert.strictEqual(service.getProvider(resource.scheme), undefined); const registrations: IFileSystemProviderRegistrationEvent[] = []; @@ -48,9 +49,8 @@ suite('File Service', () => { } }); - await service.activateProvider('test'); - - assert.strictEqual(service.canHandleResource(resource), true); + assert.strictEqual(await service.canHandleResource(resource), true); + assert.strictEqual(service.hasProvider(resource), true); assert.strictEqual(service.getProvider(resource.scheme), provider); assert.strictEqual(registrations.length, 1); @@ -73,7 +73,8 @@ suite('File Service', () => { registrationDisposable!.dispose(); - assert.strictEqual(service.canHandleResource(resource), false); + assert.strictEqual(await service.canHandleResource(resource), false); + assert.strictEqual(service.hasProvider(resource), false); assert.strictEqual(registrations.length, 2); assert.strictEqual(registrations[1].scheme, 'test'); diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts index c5e777dc8e..c3829049c0 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts @@ -82,9 +82,9 @@ flakySuite('IndexedDB File Service', function () { }); teardown(async () => { - disposables.clear(); await logFileProvider.delete(logfileURIFromPaths([]), { recursive: true, useTrash: false }); await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false }); + disposables.clear(); }); test('root is always present', async () => { @@ -233,7 +233,7 @@ flakySuite('IndexedDB File Service', function () { } const makeBatchTester = (size: number, name: string) => { - const batch = Array.from({ length: 50 }).map((_, i) => ({ contents: `Hello${i}`, resource: userdataURIFromPaths(['batched', name, `Hello${i}.txt`]) })); + const batch = Array.from({ length: size }).map((_, i) => ({ contents: `Hello${i}`, resource: userdataURIFromPaths(['batched', name, `Hello${i}.txt`]) })); let stats: Promise | undefined = undefined; return { async create() { diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/node/diskFileService.test.ts similarity index 99% rename from src/vs/platform/files/test/electron-browser/diskFileService.test.ts rename to src/vs/platform/files/test/node/diskFileService.test.ts index 0fb41b83a3..fb10f926d2 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/node/diskFileService.test.ts @@ -442,7 +442,7 @@ flakySuite('Disk File Service', function () { assert.strictEqual(resolved.isSymbolicLink, true); }); - test('resolve - symbolic link pointing to non-existing file does not break', async () => { + test('resolve - symbolic link pointing to nonexistent file does not break', async () => { await Promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); @@ -513,7 +513,7 @@ flakySuite('Disk File Service', function () { assert.strictEqual(existsSync(target.fsPath), true); // target the link pointed to is never deleted }); - (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to nonexistent file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); await Promises.symlink(target.fsPath, link.fsPath); @@ -2055,7 +2055,7 @@ flakySuite('Disk File Service', function () { assert.ok(!error); }); - test('writeFile - no error when writing to same non-existing folder multiple times different new files', async () => { + test('writeFile - no error when writing to same nonexistent folder multiple times different new files', async () => { const newFolder = URI.file(join(testDir, 'some', 'new', 'folder')); const file1 = joinPath(newFolder, 'file-1'); diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/company.js b/src/vs/platform/files/test/node/fixtures/resolver/examples/company.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/company.js rename to src/vs/platform/files/test/node/fixtures/resolver/examples/company.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/conway.js b/src/vs/platform/files/test/node/fixtures/resolver/examples/conway.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/conway.js rename to src/vs/platform/files/test/node/fixtures/resolver/examples/conway.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/employee.js b/src/vs/platform/files/test/node/fixtures/resolver/examples/employee.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/employee.js rename to src/vs/platform/files/test/node/fixtures/resolver/examples/employee.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/small.js b/src/vs/platform/files/test/node/fixtures/resolver/examples/small.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/small.js rename to src/vs/platform/files/test/node/fixtures/resolver/examples/small.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/index.html b/src/vs/platform/files/test/node/fixtures/resolver/index.html similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/index.html rename to src/vs/platform/files/test/node/fixtures/resolver/index.html diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/company.js b/src/vs/platform/files/test/node/fixtures/resolver/other/deep/company.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/company.js rename to src/vs/platform/files/test/node/fixtures/resolver/other/deep/company.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/conway.js b/src/vs/platform/files/test/node/fixtures/resolver/other/deep/conway.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/conway.js rename to src/vs/platform/files/test/node/fixtures/resolver/other/deep/conway.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/employee.js b/src/vs/platform/files/test/node/fixtures/resolver/other/deep/employee.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/employee.js rename to src/vs/platform/files/test/node/fixtures/resolver/other/deep/employee.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/small.js b/src/vs/platform/files/test/node/fixtures/resolver/other/deep/small.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/small.js rename to src/vs/platform/files/test/node/fixtures/resolver/other/deep/small.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/resolver/site.css b/src/vs/platform/files/test/node/fixtures/resolver/site.css similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/resolver/site.css rename to src/vs/platform/files/test/node/fixtures/resolver/site.css diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/binary.txt b/src/vs/platform/files/test/node/fixtures/service/binary.txt similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/binary.txt rename to src/vs/platform/files/test/node/fixtures/service/binary.txt diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/deep/company.js b/src/vs/platform/files/test/node/fixtures/service/deep/company.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/deep/company.js rename to src/vs/platform/files/test/node/fixtures/service/deep/company.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/deep/conway.js b/src/vs/platform/files/test/node/fixtures/service/deep/conway.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/deep/conway.js rename to src/vs/platform/files/test/node/fixtures/service/deep/conway.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/deep/employee.js b/src/vs/platform/files/test/node/fixtures/service/deep/employee.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/deep/employee.js rename to src/vs/platform/files/test/node/fixtures/service/deep/employee.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/deep/small.js b/src/vs/platform/files/test/node/fixtures/service/deep/small.js similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/deep/small.js rename to src/vs/platform/files/test/node/fixtures/service/deep/small.js diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/index.html b/src/vs/platform/files/test/node/fixtures/service/index.html similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/index.html rename to src/vs/platform/files/test/node/fixtures/service/index.html diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/lorem.txt b/src/vs/platform/files/test/node/fixtures/service/lorem.txt similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/lorem.txt rename to src/vs/platform/files/test/node/fixtures/service/lorem.txt diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/small.txt b/src/vs/platform/files/test/node/fixtures/service/small.txt similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/small.txt rename to src/vs/platform/files/test/node/fixtures/service/small.txt diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/small_umlaut.txt b/src/vs/platform/files/test/node/fixtures/service/small_umlaut.txt similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/small_umlaut.txt rename to src/vs/platform/files/test/node/fixtures/service/small_umlaut.txt diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/some_utf16le.css b/src/vs/platform/files/test/node/fixtures/service/some_utf16le.css similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/some_utf16le.css rename to src/vs/platform/files/test/node/fixtures/service/some_utf16le.css diff --git a/src/vs/platform/files/test/electron-browser/fixtures/service/some_utf8_bom.txt b/src/vs/platform/files/test/node/fixtures/service/some_utf8_bom.txt similarity index 100% rename from src/vs/platform/files/test/electron-browser/fixtures/service/some_utf8_bom.txt rename to src/vs/platform/files/test/node/fixtures/service/some_utf8_bom.txt diff --git a/src/vs/platform/files/test/node/recursiveWatcher.integrationTest.ts b/src/vs/platform/files/test/node/recursiveWatcher.integrationTest.ts new file mode 100644 index 0000000000..965b8a954c --- /dev/null +++ b/src/vs/platform/files/test/node/recursiveWatcher.integrationTest.ts @@ -0,0 +1,593 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { realpathSync } from 'fs'; +import { tmpdir } from 'os'; +import { timeout } from 'vs/base/common/async'; +import { dirname, join, sep } from 'vs/base/common/path'; +import { isLinux, isWindows } from 'vs/base/common/platform'; +import { Promises, RimRafMode } from 'vs/base/node/pfs'; +import { flakySuite, getPathFromAmdModule, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { FileChangeType } from 'vs/platform/files/common/files'; +import { IWatcher, ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/parcelWatcherService'; +import { IWatchRequest } from 'vs/platform/files/common/watcher'; + +flakySuite('Recursive Watcher (parcel)', () => { + + class TestParcelWatcherService extends ParcelWatcherService { + + testNormalizePaths(paths: string[]): string[] { + + // Work with strings as paths to simplify testing + const requests: IWatchRequest[] = paths.map(path => { + return { path, excludes: [] }; + }); + + return this.normalizeRequests(requests).map(request => request.path); + } + + override async watch(requests: IWatchRequest[]): Promise { + await super.watch(requests); + await this.whenReady(); + } + + async whenReady(): Promise { + for (const [, watcher] of this.watchers) { + await watcher.ready; + } + } + + override toExcludePaths(path: string, excludes: string[] | undefined): string[] | undefined { + return super.toExcludePaths(path, excludes); + } + + override restartWatching(watcher: IWatcher, delay = 10): void { + return super.restartWatching(watcher, delay); + } + } + + let testDir: string; + let service: TestParcelWatcherService; + + let loggingEnabled = false; + + function enableLogging(enable: boolean) { + loggingEnabled = enable; + service?.setVerboseLogging(enable); + } + + enableLogging(false); + + setup(async () => { + service = new TestParcelWatcherService(); + + service.onDidLogMessage(e => { + if (loggingEnabled) { + console.log(`[recursive watcher test message] ${e.message}`); + } + }); + + service.onDidError(e => { + if (loggingEnabled) { + console.log(`[recursive watcher test error] ${e}`); + } + }); + + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher'); + + const sourceDir = getPathFromAmdModule(require, './fixtures/service'); + + await Promises.copy(sourceDir, testDir, { preserveSymlinks: false }); + }); + + teardown(async () => { + await service.stop(); + + // Possible that the file watcher is still holding + // onto the folders on Windows specifically and the + // unlink would fail. In that case, do not fail the + // test suite. + return Promises.rm(testDir).catch(error => console.error(error)); + }); + + function toMsg(type: FileChangeType): string { + switch (type) { + case FileChangeType.ADDED: return 'added'; + case FileChangeType.DELETED: return 'deleted'; + default: return 'changed'; + } + } + + async function awaitEvent(service: TestParcelWatcherService, path: string, type: FileChangeType, failOnEventReason?: string): Promise { + if (loggingEnabled) { + console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`); + } + + // Await the event + await new Promise((resolve, reject) => { + const disposable = service.onDidChangeFile(events => { + for (const event of events) { + if (event.path === path && event.type === type) { + disposable.dispose(); + if (failOnEventReason) { + reject(new Error(`Unexpected file event: ${failOnEventReason}`)); + } else { + resolve(); + } + break; + } + } + }); + }); + } + + test('basics', async function () { + await service.watch([{ path: testDir, excludes: [] }]); + + // New file + const newFilePath = join(testDir, 'deep', 'newFile.txt'); + let changeFuture: Promise = awaitEvent(service, newFilePath, FileChangeType.ADDED); + await Promises.writeFile(newFilePath, 'Hello World'); + await changeFuture; + + // New folder + const newFolderPath = join(testDir, 'deep', 'New Folder'); + changeFuture = awaitEvent(service, newFolderPath, FileChangeType.ADDED); + await Promises.mkdir(newFolderPath); + await changeFuture; + + // Rename file + let renamedFilePath = join(testDir, 'deep', 'renamedFile.txt'); + changeFuture = Promise.all([ + awaitEvent(service, newFilePath, FileChangeType.DELETED), + awaitEvent(service, renamedFilePath, FileChangeType.ADDED) + ]); + await Promises.rename(newFilePath, renamedFilePath); + await changeFuture; + + // Rename folder + let renamedFolderPath = join(testDir, 'deep', 'Renamed Folder'); + changeFuture = Promise.all([ + awaitEvent(service, newFolderPath, FileChangeType.DELETED), + awaitEvent(service, renamedFolderPath, FileChangeType.ADDED) + ]); + await Promises.rename(newFolderPath, renamedFolderPath); + await changeFuture; + + // Rename file (same name, different case) + const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt'); + changeFuture = Promise.all([ + awaitEvent(service, renamedFilePath, FileChangeType.DELETED), + awaitEvent(service, caseRenamedFilePath, FileChangeType.ADDED) + ]); + await Promises.rename(renamedFilePath, caseRenamedFilePath); + await changeFuture; + renamedFilePath = caseRenamedFilePath; + + // Rename folder (same name, different case) + const caseRenamedFolderPath = join(testDir, 'deep', 'REnamed Folder'); + changeFuture = Promise.all([ + awaitEvent(service, renamedFolderPath, FileChangeType.DELETED), + awaitEvent(service, caseRenamedFolderPath, FileChangeType.ADDED) + ]); + await Promises.rename(renamedFolderPath, caseRenamedFolderPath); + await changeFuture; + renamedFolderPath = caseRenamedFolderPath; + + // Move file + const movedFilepath = join(testDir, 'movedFile.txt'); + changeFuture = Promise.all([ + awaitEvent(service, renamedFilePath, FileChangeType.DELETED), + awaitEvent(service, movedFilepath, FileChangeType.ADDED) + ]); + await Promises.rename(renamedFilePath, movedFilepath); + await changeFuture; + + // Move folder + const movedFolderpath = join(testDir, 'Moved Folder'); + changeFuture = Promise.all([ + awaitEvent(service, renamedFolderPath, FileChangeType.DELETED), + awaitEvent(service, movedFolderpath, FileChangeType.ADDED) + ]); + await Promises.rename(renamedFolderPath, movedFolderpath); + await changeFuture; + + // Copy file + const copiedFilepath = join(testDir, 'deep', 'copiedFile.txt'); + changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.ADDED); + await Promises.copyFile(movedFilepath, copiedFilepath); + await changeFuture; + + // Copy folder + const copiedFolderpath = join(testDir, 'deep', 'Copied Folder'); + changeFuture = awaitEvent(service, copiedFolderpath, FileChangeType.ADDED); + await Promises.copy(movedFolderpath, copiedFolderpath, { preserveSymlinks: false }); + await changeFuture; + + // Change file + changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.UPDATED); + await Promises.writeFile(copiedFilepath, 'Hello Change'); + await changeFuture; + + // Read file does not emit event + changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.UPDATED, 'unexpected-event-from-read-file'); + await Promises.readFile(copiedFilepath); + await Promise.race([timeout(100), changeFuture]); + + // Stat file does not emit event + changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.UPDATED, 'unexpected-event-from-stat'); + await Promises.stat(copiedFilepath); + await Promise.race([timeout(100), changeFuture]); + + // Delete file + changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.DELETED); + await Promises.unlink(copiedFilepath); + await changeFuture; + + // Delete folder + changeFuture = awaitEvent(service, copiedFolderpath, FileChangeType.DELETED); + await Promises.rmdir(copiedFolderpath); + await changeFuture; + }); + + (!isLinux /* polling is only used in linux environments (WSL) */ ? test.skip : test)('basics (polling)', async function () { + await service.watch([{ path: testDir, excludes: [], pollingInterval: 100 }]); + + // New file + const newFilePath = join(testDir, 'deep', 'newFile.txt'); + let changeFuture: Promise = awaitEvent(service, newFilePath, FileChangeType.ADDED); + await Promises.writeFile(newFilePath, 'Hello World'); + await changeFuture; + + // Change file + changeFuture = awaitEvent(service, newFilePath, FileChangeType.UPDATED); + await Promises.writeFile(newFilePath, 'Hello Change'); + await changeFuture; + + // Delete file + changeFuture = awaitEvent(service, newFilePath, FileChangeType.DELETED); + await Promises.unlink(newFilePath); + await changeFuture; + }); + + test('multiple events', async function () { + await service.watch([{ path: testDir, excludes: [] }]); + await Promises.mkdir(join(testDir, 'deep-multiple')); + + // multiple add + + const newFilePath1 = join(testDir, 'newFile-1.txt'); + const newFilePath2 = join(testDir, 'newFile-2.txt'); + const newFilePath3 = join(testDir, 'newFile-3.txt'); + const newFilePath4 = join(testDir, 'deep-multiple', 'newFile-1.txt'); + const newFilePath5 = join(testDir, 'deep-multiple', 'newFile-2.txt'); + const newFilePath6 = join(testDir, 'deep-multiple', 'newFile-3.txt'); + + const addedFuture1: Promise = awaitEvent(service, newFilePath1, FileChangeType.ADDED); + const addedFuture2: Promise = awaitEvent(service, newFilePath2, FileChangeType.ADDED); + const addedFuture3: Promise = awaitEvent(service, newFilePath3, FileChangeType.ADDED); + const addedFuture4: Promise = awaitEvent(service, newFilePath4, FileChangeType.ADDED); + const addedFuture5: Promise = awaitEvent(service, newFilePath5, FileChangeType.ADDED); + const addedFuture6: Promise = awaitEvent(service, newFilePath6, FileChangeType.ADDED); + + await Promise.all([ + await Promises.writeFile(newFilePath1, 'Hello World 1'), + await Promises.writeFile(newFilePath2, 'Hello World 2'), + await Promises.writeFile(newFilePath3, 'Hello World 3'), + await Promises.writeFile(newFilePath4, 'Hello World 4'), + await Promises.writeFile(newFilePath5, 'Hello World 5'), + await Promises.writeFile(newFilePath6, 'Hello World 6') + ]); + + await Promise.all([addedFuture1, addedFuture2, addedFuture3, addedFuture4, addedFuture5, addedFuture6]); + + // multiple change + + const changeFuture1: Promise = awaitEvent(service, newFilePath1, FileChangeType.UPDATED); + const changeFuture2: Promise = awaitEvent(service, newFilePath2, FileChangeType.UPDATED); + const changeFuture3: Promise = awaitEvent(service, newFilePath3, FileChangeType.UPDATED); + const changeFuture4: Promise = awaitEvent(service, newFilePath4, FileChangeType.UPDATED); + const changeFuture5: Promise = awaitEvent(service, newFilePath5, FileChangeType.UPDATED); + const changeFuture6: Promise = awaitEvent(service, newFilePath6, FileChangeType.UPDATED); + + await Promise.all([ + await Promises.writeFile(newFilePath1, 'Hello Update 1'), + await Promises.writeFile(newFilePath2, 'Hello Update 2'), + await Promises.writeFile(newFilePath3, 'Hello Update 3'), + await Promises.writeFile(newFilePath4, 'Hello Update 4'), + await Promises.writeFile(newFilePath5, 'Hello Update 5'), + await Promises.writeFile(newFilePath6, 'Hello Update 6') + ]); + + await Promise.all([changeFuture1, changeFuture2, changeFuture3, changeFuture4, changeFuture5, changeFuture6]); + + // copy with multiple files + + const copyFuture1: Promise = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-1.txt'), FileChangeType.ADDED); + const copyFuture2: Promise = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-2.txt'), FileChangeType.ADDED); + const copyFuture3: Promise = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-3.txt'), FileChangeType.ADDED); + const copyFuture4: Promise = awaitEvent(service, join(testDir, 'deep-multiple-copy'), FileChangeType.ADDED); + + await Promises.copy(join(testDir, 'deep-multiple'), join(testDir, 'deep-multiple-copy'), { preserveSymlinks: false }); + + await Promise.all([copyFuture1, copyFuture2, copyFuture3, copyFuture4]); + + // multiple delete (single files) + + const deleteFuture1: Promise = awaitEvent(service, newFilePath1, FileChangeType.DELETED); + const deleteFuture2: Promise = awaitEvent(service, newFilePath2, FileChangeType.DELETED); + const deleteFuture3: Promise = awaitEvent(service, newFilePath3, FileChangeType.DELETED); + const deleteFuture4: Promise = awaitEvent(service, newFilePath4, FileChangeType.DELETED); + const deleteFuture5: Promise = awaitEvent(service, newFilePath5, FileChangeType.DELETED); + const deleteFuture6: Promise = awaitEvent(service, newFilePath6, FileChangeType.DELETED); + + await Promise.all([ + await Promises.unlink(newFilePath1), + await Promises.unlink(newFilePath2), + await Promises.unlink(newFilePath3), + await Promises.unlink(newFilePath4), + await Promises.unlink(newFilePath5), + await Promises.unlink(newFilePath6) + ]); + + await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3, deleteFuture4, deleteFuture5, deleteFuture6]); + + // multiple delete (folder) + + const deleteFolderFuture1: Promise = awaitEvent(service, join(testDir, 'deep-multiple'), FileChangeType.DELETED); + const deleteFolderFuture2: Promise = awaitEvent(service, join(testDir, 'deep-multiple-copy'), FileChangeType.DELETED); + + await Promise.all([Promises.rm(join(testDir, 'deep-multiple'), RimRafMode.UNLINK), Promises.rm(join(testDir, 'deep-multiple-copy'), RimRafMode.UNLINK)]); + + await Promise.all([deleteFolderFuture1, deleteFolderFuture2]); + }); + + test('subsequent watch updates watchers (path)', async function () { + await service.watch([{ path: testDir, excludes: [join(realpathSync(testDir), 'unrelated')] }]); + + // New file (*.txt) + let newTextFilePath = join(testDir, 'deep', 'newFile.txt'); + let changeFuture: Promise = awaitEvent(service, newTextFilePath, FileChangeType.ADDED); + await Promises.writeFile(newTextFilePath, 'Hello World'); + await changeFuture; + + await service.watch([{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'unrelated')] }]); + newTextFilePath = join(testDir, 'deep', 'newFile2.txt'); + changeFuture = awaitEvent(service, newTextFilePath, FileChangeType.ADDED); + await Promises.writeFile(newTextFilePath, 'Hello World'); + await changeFuture; + + await service.watch([{ path: join(testDir, 'deep'), excludes: [realpathSync(testDir)] }]); + await service.watch([{ path: join(testDir, 'deep'), excludes: [] }]); + newTextFilePath = join(testDir, 'deep', 'newFile3.txt'); + changeFuture = awaitEvent(service, newTextFilePath, FileChangeType.ADDED); + await Promises.writeFile(newTextFilePath, 'Hello World'); + await changeFuture; + + return service.stop(); + }); + + test('subsequent watch updates watchers (excludes)', async function () { + await service.watch([{ path: testDir, excludes: [realpathSync(testDir)] }]); + await service.watch([{ path: testDir, excludes: [] }]); + + // New file (*.txt) + let newTextFilePath = join(testDir, 'deep', 'newFile.txt'); + let changeFuture: Promise = awaitEvent(service, newTextFilePath, FileChangeType.ADDED); + await Promises.writeFile(newTextFilePath, 'Hello World'); + await changeFuture; + + return service.stop(); + }); + + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () { + const link = join(testDir, 'deep-linked'); + const linkTarget = join(testDir, 'deep'); + await Promises.symlink(linkTarget, link); + + await service.watch([{ path: link, excludes: [] }]); + + // New file + const newFilePath = join(link, 'newFile.txt'); + let changeFuture: Promise = awaitEvent(service, newFilePath, FileChangeType.ADDED); + await Promises.writeFile(newFilePath, 'Hello World'); + await changeFuture; + }); + + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (via extra watch)', async function () { + const link = join(testDir, 'deep-linked'); + const linkTarget = join(testDir, 'deep'); + await Promises.symlink(linkTarget, link); + + await service.watch([{ path: testDir, excludes: [] }, { path: link, excludes: [] }]); + + // New file + const newFilePath = join(link, 'newFile.txt'); + let changeFuture: Promise = awaitEvent(service, newFilePath, FileChangeType.ADDED); + await Promises.writeFile(newFilePath, 'Hello World'); + await changeFuture; + }); + + (isLinux /* linux: is case sensitive */ ? test.skip : test)('wrong casing', async function () { + const deepWrongCasedPath = join(testDir, 'DEEP'); + + await service.watch([{ path: deepWrongCasedPath, excludes: [] }]); + + // New file + const newFilePath = join(deepWrongCasedPath, 'newFile.txt'); + let changeFuture: Promise = awaitEvent(service, newFilePath, FileChangeType.ADDED); + await Promises.writeFile(newFilePath, 'Hello World'); + await changeFuture; + }); + + test('invalid folder does not explode', async function () { + const invalidPath = join(testDir, 'invalid'); + + await service.watch([{ path: invalidPath, excludes: [] }]); + }); + + test('deleting watched path is handled properly', async function () { + const watchedPath = join(testDir, 'deep'); + + await service.watch([{ path: watchedPath, excludes: [] }]); + + // Delete watched path + let changeFuture: Promise = awaitEvent(service, watchedPath, FileChangeType.DELETED); + await Promises.rm(watchedPath, RimRafMode.UNLINK); + await changeFuture; + + // Restore watched path + changeFuture = awaitEvent(service, watchedPath, FileChangeType.ADDED); + await Promises.mkdir(watchedPath); + await changeFuture; + + await timeout(20); // restart is delayed + await service.whenReady(); + + // Verify events come in again + const newFilePath = join(watchedPath, 'newFile.txt'); + changeFuture = awaitEvent(service, newFilePath, FileChangeType.ADDED); + await Promises.writeFile(newFilePath, 'Hello World'); + await changeFuture; + }); + + test('should not exclude roots that do not overlap', () => { + if (isWindows) { + assert.deepStrictEqual(service.testNormalizePaths(['C:\\a']), ['C:\\a']); + assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); + } else { + assert.deepStrictEqual(service.testNormalizePaths(['/a']), ['/a']); + assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b']), ['/a', '/b']); + assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); + } + }); + + test('should remove sub-folders of other paths', () => { + if (isWindows) { + assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\a\\b']), ['C:\\a']); + assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.testNormalizePaths(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); + } else { + assert.deepStrictEqual(service.testNormalizePaths(['/a', '/a/b']), ['/a']); + assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(service.testNormalizePaths(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(service.testNormalizePaths(['/a', '/a/b', '/a/c/d']), ['/a']); + } + }); + + test('excludes are converted to absolute paths', () => { + + // undefined / empty + + assert.strictEqual(service.toExcludePaths(testDir, undefined), undefined); + assert.strictEqual(service.toExcludePaths(testDir, []), undefined); + + // absolute paths + + let excludes = service.toExcludePaths(testDir, [testDir]); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], testDir); + + excludes = service.toExcludePaths(testDir, [`${testDir}${sep}`, join(testDir, 'foo', 'bar'), `${join(testDir, 'other', 'deep')}${sep}`]); + assert.strictEqual(excludes?.length, 3); + assert.strictEqual(excludes[0], testDir); + assert.strictEqual(excludes[1], join(testDir, 'foo', 'bar')); + assert.strictEqual(excludes[2], join(testDir, 'other', 'deep')); + + // wrong casing is normalized for root + if (!isLinux) { + excludes = service.toExcludePaths(testDir, [join(testDir.toUpperCase(), 'node_modules', '**')]); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, 'node_modules')); + } + + // exclude ignored if not parent of watched dir + excludes = service.toExcludePaths(testDir, [join(dirname(testDir), 'node_modules', '**')]); + assert.strictEqual(excludes, undefined); + + // relative paths + + excludes = service.toExcludePaths(testDir, ['.']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], testDir); + + excludes = service.toExcludePaths(testDir, ['foo', `bar${sep}`, join('foo', 'bar'), `${join('other', 'deep')}${sep}`]); + assert.strictEqual(excludes?.length, 4); + assert.strictEqual(excludes[0], join(testDir, 'foo')); + assert.strictEqual(excludes[1], join(testDir, 'bar')); + assert.strictEqual(excludes[2], join(testDir, 'foo', 'bar')); + assert.strictEqual(excludes[3], join(testDir, 'other', 'deep')); + + // simple globs (relative) + + excludes = service.toExcludePaths(testDir, ['**']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], testDir); + + excludes = service.toExcludePaths(testDir, ['**/**']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], testDir); + + excludes = service.toExcludePaths(testDir, ['**\\**']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], testDir); + + excludes = service.toExcludePaths(testDir, ['**/node_modules/**']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, 'node_modules')); + + excludes = service.toExcludePaths(testDir, ['**/.git/objects/**']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, '.git', 'objects')); + + excludes = service.toExcludePaths(testDir, ['**/node_modules']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, 'node_modules')); + + excludes = service.toExcludePaths(testDir, ['**/.git/objects']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, '.git', 'objects')); + + excludes = service.toExcludePaths(testDir, ['node_modules/**']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, 'node_modules')); + + excludes = service.toExcludePaths(testDir, ['.git/objects/**']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, '.git', 'objects')); + + // simple globs (absolute) + + excludes = service.toExcludePaths(testDir, [join(testDir, 'node_modules', '**')]); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, 'node_modules')); + + // Linux: more restrictive glob treatment + if (isLinux) { + excludes = service.toExcludePaths(testDir, ['**/node_modules/*/**']); + assert.strictEqual(excludes?.length, 1); + assert.strictEqual(excludes[0], join(testDir, 'node_modules')); + } + + // unsupported globs + + else { + excludes = service.toExcludePaths(testDir, ['**/node_modules/*/**']); + assert.strictEqual(excludes, undefined); + } + + excludes = service.toExcludePaths(testDir, ['**/*.js']); + assert.strictEqual(excludes, undefined); + + excludes = service.toExcludePaths(testDir, ['*.js']); + assert.strictEqual(excludes, undefined); + + excludes = service.toExcludePaths(testDir, ['*']); + assert.strictEqual(excludes, undefined); + }); +}); diff --git a/src/vs/platform/files/test/electron-browser/normalizer.test.ts b/src/vs/platform/files/test/node/watcherNormalizer.test.ts similarity index 51% rename from src/vs/platform/files/test/electron-browser/normalizer.test.ts rename to src/vs/platform/files/test/node/watcherNormalizer.test.ts index 9cb820a722..ad3075cd0b 100644 --- a/src/vs/platform/files/test/electron-browser/normalizer.test.ts +++ b/src/vs/platform/files/test/node/watcherNormalizer.test.ts @@ -6,13 +6,10 @@ import * as assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; import { isLinux, isWindows } from 'vs/base/common/platform'; +import { isEqual } from 'vs/base/common/resources'; import { URI as uri } from 'vs/base/common/uri'; import { FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/platform/files/node/watcher/watcher'; - -function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent { - return new FileChangesEvent(toFileChanges(changes), !isLinux); -} +import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/platform/files/common/watcher'; class TestFileWatcher { private readonly _onDidFilesChange: Emitter<{ raw: IFileChange[], event: FileChangesEvent }>; @@ -36,9 +33,13 @@ class TestFileWatcher { // Emit through event emitter if (normalizedEvents.length > 0) { - this._onDidFilesChange.fire({ raw: toFileChanges(normalizedEvents), event: toFileChangesEvent(normalizedEvents) }); + this._onDidFilesChange.fire({ raw: toFileChanges(normalizedEvents), event: this.toFileChangesEvent(normalizedEvents) }); } } + + private toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent { + return new FileChangesEvent(toFileChanges(changes), !isLinux); + } } enum Path { @@ -47,9 +48,9 @@ enum Path { UNC } -suite('Normalizer', () => { +suite('Watcher Events Normalizer', () => { - test('simple add/update/delete', function (done: () => void) { + test('simple add/update/delete', done => { const watch = new TestFileWatcher(); const added = uri.file('/users/data/src/added.txt'); @@ -62,12 +63,12 @@ suite('Normalizer', () => { { path: deleted.fsPath, type: FileChangeType.DELETED }, ]; - watch.onDidFilesChange(({ event: e, raw }) => { - assert.ok(e); + watch.onDidFilesChange(({ event, raw }) => { + assert.ok(event); assert.strictEqual(raw.length, 3); - assert.ok(e.contains(added, FileChangeType.ADDED)); - assert.ok(e.contains(updated, FileChangeType.UPDATED)); - assert.ok(e.contains(deleted, FileChangeType.DELETED)); + assert.ok(event.contains(added, FileChangeType.ADDED)); + assert.ok(event.contains(updated, FileChangeType.UPDATED)); + assert.ok(event.contains(deleted, FileChangeType.DELETED)); done(); }); @@ -75,20 +76,19 @@ suite('Normalizer', () => { watch.report(raw); }); - let pathSpecs = isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX]; - pathSpecs.forEach((p) => { - test('delete only reported for top level folder (' + p + ')', function (done: () => void) { + (isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX]).forEach(path => { + test(`delete only reported for top level folder (${path})`, done => { const watch = new TestFileWatcher(); - const deletedFolderA = uri.file(p === Path.UNIX ? '/users/data/src/todelete1' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1'); - const deletedFolderB = uri.file(p === Path.UNIX ? '/users/data/src/todelete2' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2' : '\\\\localhost\\users\\data\\src\\todelete2'); - const deletedFolderBF1 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/file.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\file.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\file.txt'); - const deletedFolderBF2 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/more/test.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\more\\test.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\more\\test.txt'); - const deletedFolderBF3 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/super/bar/foo.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\super\\bar\\foo.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\super\\bar\\foo.txt'); - const deletedFileA = uri.file(p === Path.UNIX ? '/users/data/src/deleteme.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\deleteme.txt' : '\\\\localhost\\users\\data\\src\\deleteme.txt'); + const deletedFolderA = uri.file(path === Path.UNIX ? '/users/data/src/todelete1' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1'); + const deletedFolderB = uri.file(path === Path.UNIX ? '/users/data/src/todelete2' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2' : '\\\\localhost\\users\\data\\src\\todelete2'); + const deletedFolderBF1 = uri.file(path === Path.UNIX ? '/users/data/src/todelete2/file.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\file.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\file.txt'); + const deletedFolderBF2 = uri.file(path === Path.UNIX ? '/users/data/src/todelete2/more/test.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\more\\test.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\more\\test.txt'); + const deletedFolderBF3 = uri.file(path === Path.UNIX ? '/users/data/src/todelete2/super/bar/foo.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\super\\bar\\foo.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\super\\bar\\foo.txt'); + const deletedFileA = uri.file(path === Path.UNIX ? '/users/data/src/deleteme.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\deleteme.txt' : '\\\\localhost\\users\\data\\src\\deleteme.txt'); - const addedFile = uri.file(p === Path.UNIX ? '/users/data/src/added.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt'); - const updatedFile = uri.file(p === Path.UNIX ? '/users/data/src/updated.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt'); + const addedFile = uri.file(path === Path.UNIX ? '/users/data/src/added.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt'); + const updatedFile = uri.file(path === Path.UNIX ? '/users/data/src/updated.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt'); const raw: IDiskFileChange[] = [ { path: deletedFolderA.fsPath, type: FileChangeType.DELETED }, @@ -101,15 +101,15 @@ suite('Normalizer', () => { { path: updatedFile.fsPath, type: FileChangeType.UPDATED } ]; - watch.onDidFilesChange(({ event: e, raw }) => { - assert.ok(e); + watch.onDidFilesChange(({ event, raw }) => { + assert.ok(event); assert.strictEqual(raw.length, 5); - assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED)); - assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED)); - assert.ok(e.contains(deletedFileA, FileChangeType.DELETED)); - assert.ok(e.contains(addedFile, FileChangeType.ADDED)); - assert.ok(e.contains(updatedFile, FileChangeType.UPDATED)); + assert.ok(event.contains(deletedFolderA, FileChangeType.DELETED)); + assert.ok(event.contains(deletedFolderB, FileChangeType.DELETED)); + assert.ok(event.contains(deletedFileA, FileChangeType.DELETED)); + assert.ok(event.contains(addedFile, FileChangeType.ADDED)); + assert.ok(event.contains(updatedFile, FileChangeType.UPDATED)); done(); }); @@ -118,7 +118,7 @@ suite('Normalizer', () => { }); }); - test('event normalization: ignore CREATE followed by DELETE', function (done: () => void) { + test('event normalization: ignore CREATE followed by DELETE', done => { const watch = new TestFileWatcher(); const created = uri.file('/users/data/src/related'); @@ -131,11 +131,11 @@ suite('Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onDidFilesChange(({ event: e, raw }) => { - assert.ok(e); + watch.onDidFilesChange(({ event, raw }) => { + assert.ok(event); assert.strictEqual(raw.length, 1); - assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); + assert.ok(event.contains(unrelated, FileChangeType.UPDATED)); done(); }); @@ -143,7 +143,7 @@ suite('Normalizer', () => { watch.report(raw); }); - test('event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) { + test('event normalization: flatten DELETE followed by CREATE into CHANGE', done => { const watch = new TestFileWatcher(); const deleted = uri.file('/users/data/src/related'); @@ -156,12 +156,12 @@ suite('Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onDidFilesChange(({ event: e, raw }) => { - assert.ok(e); + watch.onDidFilesChange(({ event, raw }) => { + assert.ok(event); assert.strictEqual(raw.length, 2); - assert.ok(e.contains(deleted, FileChangeType.UPDATED)); - assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); + assert.ok(event.contains(deleted, FileChangeType.UPDATED)); + assert.ok(event.contains(unrelated, FileChangeType.UPDATED)); done(); }); @@ -169,7 +169,7 @@ suite('Normalizer', () => { watch.report(raw); }); - test('event normalization: ignore UPDATE when CREATE received', function (done: () => void) { + test('event normalization: ignore UPDATE when CREATE received', done => { const watch = new TestFileWatcher(); const created = uri.file('/users/data/src/related'); @@ -182,13 +182,13 @@ suite('Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onDidFilesChange(({ event: e, raw }) => { - assert.ok(e); + watch.onDidFilesChange(({ event, raw }) => { + assert.ok(event); assert.strictEqual(raw.length, 2); - assert.ok(e.contains(created, FileChangeType.ADDED)); - assert.ok(!e.contains(created, FileChangeType.UPDATED)); - assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); + assert.ok(event.contains(created, FileChangeType.ADDED)); + assert.ok(!event.contains(created, FileChangeType.UPDATED)); + assert.ok(event.contains(unrelated, FileChangeType.UPDATED)); done(); }); @@ -196,7 +196,7 @@ suite('Normalizer', () => { watch.report(raw); }); - test('event normalization: apply DELETE', function (done: () => void) { + test('event normalization: apply DELETE', done => { const watch = new TestFileWatcher(); const updated = uri.file('/users/data/src/related'); @@ -211,13 +211,44 @@ suite('Normalizer', () => { { path: updated.fsPath, type: FileChangeType.DELETED } ]; - watch.onDidFilesChange(({ event: e, raw }) => { - assert.ok(e); + watch.onDidFilesChange(({ event, raw }) => { + assert.ok(event); assert.strictEqual(raw.length, 2); - assert.ok(e.contains(deleted, FileChangeType.DELETED)); - assert.ok(!e.contains(updated, FileChangeType.UPDATED)); - assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); + assert.ok(event.contains(deleted, FileChangeType.DELETED)); + assert.ok(!event.contains(updated, FileChangeType.UPDATED)); + assert.ok(event.contains(unrelated, FileChangeType.UPDATED)); + + done(); + }); + + watch.report(raw); + }); + + test('event normalization: track case renames', done => { + const watch = new TestFileWatcher(); + + const oldPath = uri.file('/users/data/src/added'); + const newPath = uri.file('/users/data/src/ADDED'); + + const raw: IDiskFileChange[] = [ + { path: newPath.fsPath, type: FileChangeType.ADDED }, + { path: oldPath.fsPath, type: FileChangeType.DELETED } + ]; + + watch.onDidFilesChange(({ event, raw }) => { + assert.ok(event); + assert.strictEqual(raw.length, 2); + + for (const r of raw) { + if (isEqual(r.resource, oldPath)) { + assert.strictEqual(r.type, FileChangeType.DELETED); + } else if (isEqual(r.resource, newPath)) { + assert.strictEqual(r.type, FileChangeType.ADDED); + } else { + assert.fail(); + } + } done(); }); diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts index 9e71ad5d52..b926ccbd9e 100644 --- a/src/vs/platform/instantiation/common/instantiation.ts +++ b/src/vs/platform/instantiation/common/instantiation.ts @@ -161,6 +161,7 @@ export function refineServiceDecorator(serviceIdentifier: Serv /** * Mark a service dependency as optional. + * @deprecated Avoid, see https://github.com/microsoft/vscode/issues/119440 */ export function optional(serviceIdentifier: ServiceIdentifier) { diff --git a/src/vs/platform/ipc/electron-sandbox/services.ts b/src/vs/platform/ipc/electron-sandbox/services.ts index 4be2b2c495..2cb640151a 100644 --- a/src/vs/platform/ipc/electron-sandbox/services.ts +++ b/src/vs/platform/ipc/electron-sandbox/services.ts @@ -72,9 +72,13 @@ export function registerMainProcessRemoteService(id: ServiceIdentifier, ch export const ISharedProcessService = createDecorator('sharedProcessService'); export interface ISharedProcessService { + readonly _serviceBrand: undefined; + getChannel(channelName: string): IChannel; registerChannel(channelName: string, channel: IServerChannel): void; + + notifyRestored(): void; } class SharedProcessRemoteServiceStub extends RemoteServiceStub { diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 507c641226..8274a000b4 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -7,7 +7,8 @@ import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } f import * as arrays from 'vs/base/common/arrays'; import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { Keybinding, KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Keybinding, KeybindingModifier, ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keybindings'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -24,6 +25,8 @@ interface CurrentChord { label: string | null; } +const HIGH_FREQ_COMMANDS = /^(cursor|delete)/; + export abstract class AbstractKeybindingService extends Disposable implements IKeybindingService { public _serviceBrand: undefined; @@ -35,7 +38,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _currentChord: CurrentChord | null; private _currentChordChecker: IntervalTimer; private _currentChordStatusMessage: IDisposable | null; - private _currentSingleModifier: null | string; + private _ignoreSingleModifiers: KeybindingModifierSet; + private _currentSingleModifier: KeybindingModifier | null; private _currentSingleModifierClearTimeout: TimeoutTimer; protected _logging: boolean; @@ -56,6 +60,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK this._currentChord = null; this._currentChordChecker = new IntervalTimer(); this._currentChordStatusMessage = null; + this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY; this._currentSingleModifier = null; this._currentSingleModifierClearTimeout = new TimeoutTimer(); this._logging = false; @@ -169,8 +174,11 @@ export abstract class AbstractKeybindingService extends Disposable implements IK } public dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void { + this._log(`/ Dispatching keybinding triggered via menu entry accelerator - ${userSettingsLabel}`); const keybindings = this.resolveUserBinding(userSettingsLabel); - if (keybindings.length >= 1) { + if (keybindings.length === 0) { + this._log(`\\ Could not resolve - ${userSettingsLabel}`); + } else { this._doDispatch(keybindings[0], target, /*isSingleModiferChord*/false); } } @@ -183,25 +191,51 @@ export abstract class AbstractKeybindingService extends Disposable implements IK const keybinding = this.resolveKeyboardEvent(e); const [singleModifier,] = keybinding.getSingleModifierDispatchParts(); - if (singleModifier !== null && this._currentSingleModifier === null) { - // we have a valid `singleModifier`, store it for the next keyup, but clear it in 300ms - this._log(`+ Storing single modifier for possible chord ${singleModifier}.`); - this._currentSingleModifier = singleModifier; - this._currentSingleModifierClearTimeout.cancelAndSet(() => { - this._log(`+ Clearing single modifier due to 300ms elapsed.`); + if (singleModifier) { + + if (this._ignoreSingleModifiers.has(singleModifier)) { + this._log(`+ Ignoring single modifier ${singleModifier} due to it being pressed together with other keys.`); + this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY; + this._currentSingleModifierClearTimeout.cancel(); this._currentSingleModifier = null; - }, 300); + return false; + } + + this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY; + + if (this._currentSingleModifier === null) { + // we have a valid `singleModifier`, store it for the next keyup, but clear it in 300ms + this._log(`+ Storing single modifier for possible chord ${singleModifier}.`); + this._currentSingleModifier = singleModifier; + this._currentSingleModifierClearTimeout.cancelAndSet(() => { + this._log(`+ Clearing single modifier due to 300ms elapsed.`); + this._currentSingleModifier = null; + }, 300); + return false; + } + + if (singleModifier === this._currentSingleModifier) { + // bingo! + this._log(`/ Dispatching single modifier chord ${singleModifier} ${singleModifier}`); + this._currentSingleModifierClearTimeout.cancel(); + this._currentSingleModifier = null; + return this._doDispatch(keybinding, target, /*isSingleModiferChord*/true); + } + + this._log(`+ Clearing single modifier due to modifier mismatch: ${this._currentSingleModifier} ${singleModifier}`); + this._currentSingleModifierClearTimeout.cancel(); + this._currentSingleModifier = null; return false; } - if (singleModifier !== null && singleModifier === this._currentSingleModifier) { - // bingo! - this._log(`/ Dispatching single modifier chord ${singleModifier} ${singleModifier}`); - this._currentSingleModifierClearTimeout.cancel(); - this._currentSingleModifier = null; - return this._doDispatch(keybinding, target, /*isSingleModiferChord*/true); - } + // When pressing a modifier and holding it pressed with any other modifier or key combination, + // the pressed modifiers should no longer be considered for single modifier dispatch. + const [firstPart,] = keybinding.getParts(); + this._ignoreSingleModifiers = new KeybindingModifierSet(firstPart); + if (this._currentSingleModifier !== null) { + this._log(`+ Clearing single modifier due to other key up.`); + } this._currentSingleModifierClearTimeout.cancel(); this._currentSingleModifier = null; return false; @@ -263,7 +297,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; @@ -276,10 +312,36 @@ export abstract class AbstractKeybindingService extends Disposable implements IK } // weak check for certain ranges. this is properly implemented in a subclass // with access to the KeyboardMapperFactory. - if ((event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z) - || (event.keyCode >= KeyCode.KEY_0 && event.keyCode <= KeyCode.KEY_9)) { + if ((event.keyCode >= KeyCode.KeyA && event.keyCode <= KeyCode.KeyZ) + || (event.keyCode >= KeyCode.Digit0 && event.keyCode <= KeyCode.Digit9)) { return true; } return false; } } + +class KeybindingModifierSet { + + public static EMPTY = new KeybindingModifierSet(null); + + private readonly _ctrlKey: boolean; + private readonly _shiftKey: boolean; + private readonly _altKey: boolean; + private readonly _metaKey: boolean; + + constructor(source: ResolvedKeybindingPart | null) { + this._ctrlKey = source ? source.ctrlKey : false; + this._shiftKey = source ? source.shiftKey : false; + this._altKey = source ? source.altKey : false; + this._metaKey = source ? source.metaKey : false; + } + + has(modifier: KeybindingModifier) { + switch (modifier) { + case 'ctrl': return this._ctrlKey; + case 'shift': return this._shiftKey; + case 'alt': return this._altKey; + case 'meta': return this._metaKey; + } + } +} diff --git a/src/vs/platform/keybinding/common/baseResolvedKeybinding.ts b/src/vs/platform/keybinding/common/baseResolvedKeybinding.ts index aa1befd235..080f06cc50 100644 --- a/src/vs/platform/keybinding/common/baseResolvedKeybinding.ts +++ b/src/vs/platform/keybinding/common/baseResolvedKeybinding.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { illegalArgument } from 'vs/base/common/errors'; -import { AriaLabelProvider, ElectronAcceleratorLabelProvider, Modifiers, UILabelProvider, UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels'; -import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keyCodes'; +import { AriaLabelProvider, ElectronAcceleratorLabelProvider, UILabelProvider, UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels'; +import { IBaseKeybinding, KeybindingModifier, ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keybindings'; import { OperatingSystem } from 'vs/base/common/platform'; -export abstract class BaseResolvedKeybinding extends ResolvedKeybinding { +export abstract class BaseResolvedKeybinding extends ResolvedKeybinding { protected readonly _os: OperatingSystem; protected readonly _parts: T[]; @@ -32,7 +32,12 @@ export abstract class BaseResolvedKeybinding extends Resolv public getElectronAccelerator(): string | null { if (this._parts.length > 1) { - // Electron cannot handle chords + // [Electron Accelerators] Electron cannot handle chords + return null; + } + if (this._parts[0].isDuplicateModifierCase()) { + // [Electron Accelerators] Electron cannot handle modifier only keybindings + // e.g. "shift shift" return null; } return ElectronAcceleratorLabelProvider.toLabel(this._os, this._parts, (keybinding) => this._getElectronAccelerator(keybinding)); @@ -69,7 +74,7 @@ export abstract class BaseResolvedKeybinding extends Resolv return this._parts.map((keybinding) => this._getDispatchPart(keybinding)); } - public getSingleModifierDispatchParts(): (string | null)[] { + public getSingleModifierDispatchParts(): (KeybindingModifier | null)[] { return this._parts.map((keybinding) => this._getSingleModifierDispatchPart(keybinding)); } @@ -79,5 +84,5 @@ export abstract class BaseResolvedKeybinding extends Resolv protected abstract _getUserSettingsLabel(keybinding: T): string | null; protected abstract _isWYSIWYG(keybinding: T): boolean; protected abstract _getDispatchPart(keybinding: T): string | null; - protected abstract _getSingleModifierDispatchPart(keybinding: T): string | null; + protected abstract _getSingleModifierDispatchPart(keybinding: T): KeybindingModifier | null; } diff --git a/src/vs/platform/keybinding/common/keybinding.ts b/src/vs/platform/keybinding/common/keybinding.ts index 88b1c79ad2..7b20c23170 100644 --- a/src/vs/platform/keybinding/common/keybinding.ts +++ b/src/vs/platform/keybinding/common/keybinding.ts @@ -5,7 +5,8 @@ import { Event } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { Keybinding, KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Keybinding, ResolvedKeybinding } from 'vs/base/common/keybindings'; 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'; diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index 4b9739808a..0eb2a3e6eb 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 { ContextKeyExpression, ContextKeyExprType, IContext, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { implies, ContextKeyExpression, ContextKeyExprType, IContext, IContextKeyService, expressionsAreEqualWithConstantSubstitution } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; export interface IResolveResult { @@ -77,7 +77,7 @@ export class KeybindingResolver { if (!defaultKb.when) { return false; } - if (!when.equals(defaultKb.when)) { + if (!expressionsAreEqualWithConstantSubstitution(when, defaultKb.when)) { return false; } } @@ -190,35 +190,7 @@ export class KeybindingResolver { return false; } - return this._implies(a, b); - } - - /** - * Returns true if it is provable `p` implies `q`. - */ - private static _implies(p: ContextKeyExpression, q: ContextKeyExpression): boolean { - const notP = p.negate(); - - const terminals = (node: ContextKeyExpression) => { - if (node.type === ContextKeyExprType.Or) { - return node.expr; - } - return [node]; - }; - - let expr = terminals(notP).concat(terminals(q)); - for (let i = 0; i < expr.length; i++) { - const a = expr[i]; - const notA = a.negate(); - for (let j = i + 1; j < expr.length; j++) { - const b = expr[j]; - if (notA.equals(b)) { - return true; - } - } - } - - return false; + return implies(a, b); } public getDefaultBoundCommands(): Map { diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index 8146fc1b55..84d7ccd65e 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -3,14 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createKeybinding, Keybinding, KeyCode, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { createKeybinding, Keybinding, SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings'; import { OperatingSystem, OS } from 'vs/base/common/platform'; import { CommandsRegistry, ICommandHandler, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; export interface IKeybindingItem { - keybinding: Keybinding; + keybinding: (SimpleKeybinding | ScanCodeBinding)[]; command: string; commandArgs?: any; when: ContextKeyExpression | null | undefined; @@ -44,11 +45,8 @@ export interface IKeybindingRule extends IKeybindings { when?: ContextKeyExpression | null | undefined; } -export interface IKeybindingRule2 { - primary: Keybinding | null; - win?: { primary: Keybinding | null; } | null; - linux?: { primary: Keybinding | null; } | null; - mac?: { primary: Keybinding | null; } | null; +export interface IExtensionKeybindingRule { + keybinding: (SimpleKeybinding | ScanCodeBinding)[]; id: string; args?: any; weight: number; @@ -72,7 +70,7 @@ export interface ICommandAndKeybindingRule extends IKeybindingRule { export interface IKeybindingsRegistry { registerKeybindingRule(rule: IKeybindingRule): void; - setExtensionKeybindings(rules: IKeybindingRule2[]): void; + setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void; registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void; getDefaultKeybindings(): IKeybindingItem[]; } @@ -110,27 +108,6 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry { return kb; } - /** - * Take current platform into account and reduce to primary & secondary. - */ - private static bindToCurrentPlatform2(kb: IKeybindingRule2): { primary?: Keybinding | null; } { - if (OS === OperatingSystem.Windows) { - if (kb && kb.win) { - return kb.win; - } - } else if (OS === OperatingSystem.Macintosh) { - if (kb && kb.mac) { - return kb.mac; - } - } else { - if (kb && kb.linux) { - return kb.linux; - } - } - - return kb; - } - public registerKeybindingRule(rule: IKeybindingRule): void { const actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform(rule); @@ -152,15 +129,12 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry { } } - public setExtensionKeybindings(rules: IKeybindingRule2[]): void { + public setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void { let result: IKeybindingItem[] = [], keybindingsLen = 0; - for (let i = 0, len = rules.length; i < len; i++) { - const rule = rules[i]; - let actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform2(rule); - - if (actualKb && actualKb.primary) { + for (const rule of rules) { + if (rule.keybinding.length > 0) { result[keybindingsLen++] = { - keybinding: actualKb.primary, + keybinding: rule.keybinding, command: rule.id, commandArgs: rule.args, when: rule.when, @@ -182,28 +156,28 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry { } private static _mightProduceChar(keyCode: KeyCode): boolean { - if (keyCode >= KeyCode.KEY_0 && keyCode <= KeyCode.KEY_9) { + if (keyCode >= KeyCode.Digit0 && keyCode <= KeyCode.Digit9) { return true; } - if (keyCode >= KeyCode.KEY_A && keyCode <= KeyCode.KEY_Z) { + if (keyCode >= KeyCode.KeyA && keyCode <= KeyCode.KeyZ) { return true; } return ( - keyCode === KeyCode.US_SEMICOLON - || keyCode === KeyCode.US_EQUAL - || keyCode === KeyCode.US_COMMA - || keyCode === KeyCode.US_MINUS - || keyCode === KeyCode.US_DOT - || keyCode === KeyCode.US_SLASH - || keyCode === KeyCode.US_BACKTICK + keyCode === KeyCode.Semicolon + || keyCode === KeyCode.Equal + || keyCode === KeyCode.Comma + || keyCode === KeyCode.Minus + || keyCode === KeyCode.Period + || keyCode === KeyCode.Slash + || keyCode === KeyCode.Backquote || keyCode === KeyCode.ABNT_C1 || keyCode === KeyCode.ABNT_C2 - || keyCode === KeyCode.US_OPEN_SQUARE_BRACKET - || keyCode === KeyCode.US_BACKSLASH - || keyCode === KeyCode.US_CLOSE_SQUARE_BRACKET - || keyCode === KeyCode.US_QUOTE + || keyCode === KeyCode.BracketLeft + || keyCode === KeyCode.Backslash + || keyCode === KeyCode.BracketRight + || keyCode === KeyCode.Quote || keyCode === KeyCode.OEM_8 - || keyCode === KeyCode.OEM_102 + || keyCode === KeyCode.IntlBackslash ); } @@ -220,7 +194,7 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry { this._assertNoCtrlAlt(keybinding.parts[0], commandId); } this._coreKeybindings.push({ - keybinding: keybinding, + keybinding: keybinding.parts, command: commandId, commandArgs: commandArgs, when: when, diff --git a/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts b/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts index f27a05b4db..b72331fc07 100644 --- a/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts +++ b/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CharCode } from 'vs/base/common/charCode'; -import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; export class ResolvedKeybindingItem { diff --git a/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts b/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts index 86d360d8bc..216120947f 100644 --- a/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts +++ b/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts @@ -3,9 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Keybinding, KeyCode, KeyCodeUtils, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { KeyCode, KeyCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE, ScanCode } from 'vs/base/common/keyCodes'; +import { ChordKeybinding, Keybinding, KeybindingModifier, SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings'; import { OperatingSystem } from 'vs/base/common/platform'; import { BaseResolvedKeybinding } from 'vs/platform/keybinding/common/baseResolvedKeybinding'; +import { removeElementsAfterNulls } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; /** * Do not instantiate. Use KeybindingService to get a ResolvedKeybinding seeded with information about the current kb layout. @@ -46,31 +48,8 @@ export class USLayoutResolvedKeybinding extends BaseResolvedKeybinding= KeyCode.NUMPAD_0 && keyCode <= KeyCode.NUMPAD_DIVIDE) { - // Electron cannot handle numpad keys - return null; - } - - switch (keyCode) { - case KeyCode.UpArrow: - return 'Up'; - case KeyCode.DownArrow: - return 'Down'; - case KeyCode.LeftArrow: - return 'Left'; - case KeyCode.RightArrow: - return 'Right'; - } - - return KeyCodeUtils.toString(keyCode); - } - protected _getElectronAccelerator(keybinding: SimpleKeybinding): string | null { - if (keybinding.isDuplicateModifierCase()) { - return null; - } - return this._keyCodeToElectronAccelerator(keybinding.keyCode); + return KeyCodeUtils.toElectronAccelerator(keybinding.keyCode); } protected _getUserSettingsLabel(keybinding: SimpleKeybinding): string | null { @@ -112,7 +91,7 @@ export class USLayoutResolvedKeybinding extends BaseResolvedKeybinding this._resolveSimpleUserBinding(keybinding))); + if (parts.length > 0) { + return [new USLayoutResolvedKeybinding(new ChordKeybinding(parts), os)]; + } + return []; + } } diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index ebf22e4f2c..a1c3394b4b 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { createKeybinding, createSimpleKeybinding, Keybinding, KeyChord, KeyCode, KeyMod, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { createKeybinding, createSimpleKeybinding, Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keybindings'; import { Disposable } from 'vs/base/common/lifecycle'; import { OS } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; @@ -208,17 +209,17 @@ suite('AbstractKeybindingService', () => { test('issue #16498: chord mode is quit for invalid chords', () => { let kbService = createTestKeybindingService([ - kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X), 'chordCommand'), + kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX), 'chordCommand'), kbItem(KeyCode.Backspace, 'simpleCommand'), ]); // send Ctrl/Cmd + K - let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); + let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK); assert.strictEqual(shouldPreventDefault, true); assert.deepStrictEqual(executeCommandCalls, []); assert.deepStrictEqual(showMessageCalls, []); assert.deepStrictEqual(statusMessageCalls, [ - `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` + `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}) was pressed. Waiting for second key of chord...` ]); assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; @@ -232,10 +233,10 @@ suite('AbstractKeybindingService', () => { assert.deepStrictEqual(executeCommandCalls, []); assert.deepStrictEqual(showMessageCalls, []); assert.deepStrictEqual(statusMessageCalls, [ - `The key combination (${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}, ${toUsLabel(KeyCode.Backspace)}) is not a command.` + `The key combination (${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}, ${toUsLabel(KeyCode.Backspace)}) is not a command.` ]); assert.deepStrictEqual(statusMessageCallsDisposed, [ - `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` + `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}) was pressed. Waiting for second key of chord...` ]); executeCommandCalls = []; showMessageCalls = []; @@ -303,8 +304,8 @@ suite('AbstractKeybindingService', () => { test('can trigger command that is sharing keybinding with chord', () => { let kbService = createTestKeybindingService([ - kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X), 'chordCommand'), - kbItem(KeyMod.CtrlCmd | KeyCode.KEY_K, 'simpleCommand', ContextKeyExpr.has('key1')), + kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX), 'chordCommand'), + kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, 'simpleCommand', ContextKeyExpr.has('key1')), ]); @@ -312,7 +313,7 @@ suite('AbstractKeybindingService', () => { currentContextValue = createContext({ key1: true }); - let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); + let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK); assert.strictEqual(shouldPreventDefault, true); assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', @@ -328,12 +329,12 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); - shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); + shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK); assert.strictEqual(shouldPreventDefault, true); assert.deepStrictEqual(executeCommandCalls, []); assert.deepStrictEqual(showMessageCalls, []); assert.deepStrictEqual(statusMessageCalls, [ - `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` + `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}) was pressed. Waiting for second key of chord...` ]); assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; @@ -343,7 +344,7 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + X currentContextValue = createContext({}); - shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X); + shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyX); assert.strictEqual(shouldPreventDefault, true); assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'chordCommand', @@ -352,7 +353,7 @@ suite('AbstractKeybindingService', () => { assert.deepStrictEqual(showMessageCalls, []); assert.deepStrictEqual(statusMessageCalls, []); assert.deepStrictEqual(statusMessageCallsDisposed, [ - `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` + `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KeyK)}) was pressed. Waiting for second key of chord...` ]); executeCommandCalls = []; showMessageCalls = []; @@ -365,14 +366,14 @@ suite('AbstractKeybindingService', () => { test('cannot trigger chord if command is overwriting', () => { let kbService = createTestKeybindingService([ - kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_X), 'chordCommand', ContextKeyExpr.has('key1')), - kbItem(KeyMod.CtrlCmd | KeyCode.KEY_K, 'simpleCommand'), + kbItem(KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX), 'chordCommand', ContextKeyExpr.has('key1')), + kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, 'simpleCommand'), ]); // send Ctrl/Cmd + K currentContextValue = createContext({}); - let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); + let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK); assert.strictEqual(shouldPreventDefault, true); assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', @@ -390,7 +391,7 @@ suite('AbstractKeybindingService', () => { currentContextValue = createContext({ key1: true }); - shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); + shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK); assert.strictEqual(shouldPreventDefault, true); assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', @@ -408,7 +409,7 @@ suite('AbstractKeybindingService', () => { currentContextValue = createContext({ key1: true }); - shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X); + shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyX); assert.strictEqual(shouldPreventDefault, false); assert.deepStrictEqual(executeCommandCalls, []); assert.deepStrictEqual(showMessageCalls, []); @@ -425,12 +426,12 @@ suite('AbstractKeybindingService', () => { test('can have spying command', () => { let kbService = createTestKeybindingService([ - kbItem(KeyMod.CtrlCmd | KeyCode.KEY_K, '^simpleCommand'), + kbItem(KeyMod.CtrlCmd | KeyCode.KeyK, '^simpleCommand'), ]); // send Ctrl/Cmd + K currentContextValue = createContext({}); - let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); + let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KeyK); assert.strictEqual(shouldPreventDefault, false); assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index d28da4108e..3d729cb3d6 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { createKeybinding, KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { createKeybinding } from 'vs/base/common/keybindings'; import { OperatingSystem } from 'vs/base/common/platform'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; @@ -16,95 +17,95 @@ suite('KeybindingLabels', () => { test('Windows US label', () => { // no modifier - assertUSLabel(OperatingSystem.Windows, KeyCode.KEY_A, 'A'); + assertUSLabel(OperatingSystem.Windows, KeyCode.KeyA, 'A'); // one modifier - assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyCode.KEY_A, 'Ctrl+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyCode.KEY_A, 'Shift+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.Alt | KeyCode.KEY_A, 'Alt+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.WinCtrl | KeyCode.KEY_A, 'Windows+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyCode.KeyA, 'Ctrl+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyCode.KeyA, 'Shift+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.Alt | KeyCode.KeyA, 'Alt+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.WinCtrl | KeyCode.KeyA, 'Windows+A'); // two modifiers - assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A, 'Ctrl+Shift+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Alt+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Windows+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Shift+Alt+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Windows+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Alt+Windows+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, 'Ctrl+Shift+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyA, 'Ctrl+Alt+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Windows+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.Alt | KeyCode.KeyA, 'Shift+Alt+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KeyA, 'Shift+Windows+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Alt+Windows+A'); // three modifiers - assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Shift+Alt+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Windows+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Alt+Windows+A'); - assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Alt+Windows+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyA, 'Ctrl+Shift+Alt+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Shift+Windows+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Alt+Windows+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Shift+Alt+Windows+A'); // four modifiers - assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Windows+A'); + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Shift+Alt+Windows+A'); // chord - assertUSLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'Ctrl+A Ctrl+B'); + assertUSLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), 'Ctrl+A Ctrl+B'); }); test('Linux US label', () => { // no modifier - assertUSLabel(OperatingSystem.Linux, KeyCode.KEY_A, 'A'); + assertUSLabel(OperatingSystem.Linux, KeyCode.KeyA, 'A'); // one modifier - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyCode.KEY_A, 'Ctrl+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyCode.KEY_A, 'Shift+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyCode.KEY_A, 'Alt+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.WinCtrl | KeyCode.KEY_A, 'Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyCode.KeyA, 'Ctrl+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyCode.KeyA, 'Shift+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyCode.KeyA, 'Alt+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.WinCtrl | KeyCode.KeyA, 'Super+A'); // two modifiers - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A, 'Ctrl+Shift+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Alt+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Super+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Shift+Alt+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Super+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Alt+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, 'Ctrl+Shift+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyA, 'Ctrl+Alt+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyCode.KeyA, 'Shift+Alt+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KeyA, 'Shift+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Alt+Super+A'); // three modifiers - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Shift+Alt+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Super+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Alt+Super+A'); - assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Alt+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyA, 'Ctrl+Shift+Alt+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Shift+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Alt+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Shift+Alt+Super+A'); // four modifiers - assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A'); + assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Shift+Alt+Super+A'); // chord - assertUSLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'Ctrl+A Ctrl+B'); + assertUSLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), 'Ctrl+A Ctrl+B'); }); test('Mac US label', () => { // no modifier - assertUSLabel(OperatingSystem.Macintosh, KeyCode.KEY_A, 'A'); + assertUSLabel(OperatingSystem.Macintosh, KeyCode.KeyA, 'A'); // one modifier - assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyCode.KEY_A, '⌘A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyCode.KEY_A, '⇧A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.Alt | KeyCode.KEY_A, '⌥A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.WinCtrl | KeyCode.KEY_A, '⌃A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyCode.KeyA, '⌘A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyCode.KeyA, '⇧A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.Alt | KeyCode.KeyA, '⌥A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.WinCtrl | KeyCode.KeyA, '⌃A'); // two modifiers - assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A, '⇧⌘A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_A, '⌥⌘A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⌘A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, '⇧⌥A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⇧A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⌥A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, '⇧⌘A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyA, '⌥⌘A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyA, '⌃⌘A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.Alt | KeyCode.KeyA, '⇧⌥A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KeyA, '⌃⇧A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, '⌃⌥A'); // three modifiers - assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, '⇧⌥⌘A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⇧⌘A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⌥⌘A'); - assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⇧⌥A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyA, '⇧⌥⌘A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KeyA, '⌃⇧⌘A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, '⌃⌥⌘A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, '⌃⇧⌥A'); // four modifiers - assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, '⌃⇧⌥⌘A'); + assertUSLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, '⌃⇧⌥⌘A'); // chord - assertUSLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), '⌘A ⌘B'); + assertUSLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), '⌘A ⌘B'); // special keys assertUSLabel(OperatingSystem.Macintosh, KeyCode.LeftArrow, '←'); @@ -119,9 +120,9 @@ suite('KeybindingLabels', () => { assert.strictEqual(usResolvedKeybinding.getAriaLabel(), expected); } - assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A'); - assertAriaLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Super+A'); - assertAriaLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Command+A'); + assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Control+Shift+Alt+Windows+A'); + assertAriaLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Control+Shift+Alt+Super+A'); + assertAriaLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Control+Shift+Alt+Command+A'); }); test('Electron Accelerator label', () => { @@ -130,19 +131,19 @@ suite('KeybindingLabels', () => { assert.strictEqual(usResolvedKeybinding.getElectronAccelerator(), expected); } - assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A'); - assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A'); - assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Cmd+A'); + assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Shift+Alt+Super+A'); + assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Shift+Alt+Super+A'); + assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'Ctrl+Shift+Alt+Cmd+A'); // electron cannot handle chords - assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), null); - assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), null); - assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), null); + assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), null); + assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), null); + assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), null); // electron cannot handle numpad keys - assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyCode.NUMPAD_1, null); - assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyCode.NUMPAD_1, null); - assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyCode.NUMPAD_1, null); + assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyCode.Numpad1, null); + assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyCode.Numpad1, null); + assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyCode.Numpad1, null); // special assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyCode.LeftArrow, 'Left'); @@ -157,14 +158,14 @@ suite('KeybindingLabels', () => { assert.strictEqual(usResolvedKeybinding.getUserSettingsLabel(), expected); } - assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+win+a'); - assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+meta+a'); - assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+cmd+a'); + assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'ctrl+shift+alt+win+a'); + assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'ctrl+shift+alt+meta+a'); + assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KeyA, 'ctrl+shift+alt+cmd+a'); // electron cannot handle chords - assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'ctrl+a ctrl+b'); - assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'ctrl+a ctrl+b'); - assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'cmd+a cmd+b'); + assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), 'ctrl+a ctrl+b'); + assertElectronAcceleratorLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), 'ctrl+a ctrl+b'); + assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KeyA, KeyMod.CtrlCmd | KeyCode.KeyB), 'cmd+a cmd+b'); }); test('issue #91235: Do not end with a +', () => { diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index a6b3db9d1f..f2dff74e6a 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { createKeybinding, createSimpleKeybinding, KeyChord, KeyCode, KeyMod, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { createKeybinding, createSimpleKeybinding, SimpleKeybinding } from 'vs/base/common/keybindings'; import { OS } from 'vs/base/common/platform'; import { ContextKeyExpr, ContextKeyExpression, IContext } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; @@ -38,7 +39,7 @@ suite('KeybindingResolver', () => { } test('resolve key', function () { - let keybinding = KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z; + let keybinding = KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ; let runtimeKeybinding = createSimpleKeybinding(keybinding, OS); let contextRules = ContextKeyExpr.equals('bar', 'baz'); let keybindingItem = kbItem(keybinding, 'yes', null, contextRules, true); @@ -53,7 +54,7 @@ suite('KeybindingResolver', () => { test('resolve key with arguments', function () { let commandArgs = { text: 'no' }; - let keybinding = KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z; + let keybinding = KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ; let runtimeKeybinding = createSimpleKeybinding(keybinding, OS); let contextRules = ContextKeyExpr.equals('bar', 'baz'); let keybindingItem = kbItem(keybinding, 'yes', commandArgs, contextRules, true); @@ -64,131 +65,131 @@ suite('KeybindingResolver', () => { test('KeybindingResolver.combine simple 1', function () { let defaults = [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true) ]; let overrides = [ - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false) + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false), + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false), ]); }); test('KeybindingResolver.combine simple 2', function () { let defaults = [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]; let overrides = [ - kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false) + kbItem(KeyCode.KeyC, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true), - kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false), + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true), + kbItem(KeyCode.KeyC, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false), ]); }); test('KeybindingResolver.combine removal with not matching when', function () { let defaults = [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]; let overrides = [ - kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'b'), false) + kbItem(KeyCode.KeyA, '-yes1', null, ContextKeyExpr.equals('1', 'b'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('KeybindingResolver.combine removal with not matching keybinding', function () { let defaults = [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]; let overrides = [ - kbItem(KeyCode.KEY_B, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) + kbItem(KeyCode.KeyB, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('KeybindingResolver.combine removal with matching keybinding and when', function () { let defaults = [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]; let overrides = [ - kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) + kbItem(KeyCode.KeyA, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('KeybindingResolver.combine removal with unspecified keybinding', function () { let defaults = [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]; let overrides = [ kbItem(0, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('KeybindingResolver.combine removal with unspecified when', function () { let defaults = [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]; let overrides = [ - kbItem(KeyCode.KEY_A, '-yes1', null, null!, false) + kbItem(KeyCode.KeyA, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('KeybindingResolver.combine removal with unspecified when and unspecified keybinding', function () { let defaults = [ - kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]; let overrides = [ kbItem(0, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('issue #612#issuecomment-222109084 cannot remove keybindings for commands with ^', function () { let defaults = [ - kbItem(KeyCode.KEY_A, '^yes1', null, ContextKeyExpr.equals('1', 'a'), true), - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyA, '^yes1', null, ContextKeyExpr.equals('1', 'a'), true), + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]; let overrides = [ - kbItem(KeyCode.KEY_A, '-yes1', null, null!, false) + kbItem(KeyCode.KeyA, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); assert.deepStrictEqual(actual, [ - kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) + kbItem(KeyCode.KeyB, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -245,7 +246,7 @@ suite('KeybindingResolver', () => { let items = [ // This one will never match because its "when" is always overwritten by another one _kbItem( - KeyCode.KEY_X, + KeyCode.KeyX, 'first', ContextKeyExpr.and( ContextKeyExpr.equals('key1', true), @@ -254,31 +255,31 @@ suite('KeybindingResolver', () => { ), // This one always overwrites first _kbItem( - KeyCode.KEY_X, + KeyCode.KeyX, 'second', ContextKeyExpr.equals('key2', true) ), // This one is a secondary mapping for `second` _kbItem( - KeyCode.KEY_Z, + KeyCode.KeyZ, 'second', null! ), // This one sometimes overwrites first _kbItem( - KeyCode.KEY_X, + KeyCode.KeyX, 'third', ContextKeyExpr.equals('key3', true) ), // This one is always overwritten by another one _kbItem( - KeyMod.CtrlCmd | KeyCode.KEY_Y, + KeyMod.CtrlCmd | KeyCode.KeyY, 'fourth', ContextKeyExpr.equals('key4', true) ), // This one overwrites with a chord the previous one _kbItem( - KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z), + KeyChord(KeyMod.CtrlCmd | KeyCode.KeyY, KeyCode.KeyZ), 'fifth', null! ), @@ -289,32 +290,32 @@ suite('KeybindingResolver', () => { null! ), _kbItem( - KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U), + KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyU), 'seventh', null! ), _kbItem( - KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K), + KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyK), 'seventh', null! ), _kbItem( - KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U), + KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyU), 'uncomment lines', null! ), _kbItem( - KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C), + KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyC), 'comment lines', null! ), _kbItem( - KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_G, KeyMod.CtrlCmd | KeyCode.KEY_C), + KeyChord(KeyMod.CtrlCmd | KeyCode.KeyG, KeyMod.CtrlCmd | KeyCode.KeyC), 'unreachablechord', null! ), _kbItem( - KeyMod.CtrlCmd | KeyCode.KEY_G, + KeyMod.CtrlCmd | KeyCode.KeyG, 'eleven', null! ) @@ -325,7 +326,7 @@ suite('KeybindingResolver', () => { let testKey = (commandId: string, expectedKeys: number[]) => { // Test lookup let lookupResult = resolver.lookupKeybindings(commandId); - assert.strictEqual(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t')); + assert.strictEqual(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId); for (let i = 0, len = lookupResult.length; i < len; i++) { const expected = new USLayoutResolvedKeybinding(createKeybinding(expectedKeys[i], OS)!, OS); @@ -359,31 +360,31 @@ suite('KeybindingResolver', () => { testKey('first', []); - testKey('second', [KeyCode.KEY_Z, KeyCode.KEY_X]); - testResolve(createContext({ key2: true }), KeyCode.KEY_X, 'second'); - testResolve(createContext({}), KeyCode.KEY_Z, 'second'); + testKey('second', [KeyCode.KeyZ, KeyCode.KeyX]); + testResolve(createContext({ key2: true }), KeyCode.KeyX, 'second'); + testResolve(createContext({}), KeyCode.KeyZ, 'second'); - testKey('third', [KeyCode.KEY_X]); - testResolve(createContext({ key3: true }), KeyCode.KEY_X, 'third'); + testKey('third', [KeyCode.KeyX]); + testResolve(createContext({ key3: true }), KeyCode.KeyX, 'third'); testKey('fourth', []); - testKey('fifth', [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z)]); - testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z), 'fifth'); + testKey('fifth', [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyY, KeyCode.KeyZ)]); + testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KeyY, KeyCode.KeyZ), 'fifth'); - testKey('seventh', [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K)]); - testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K), 'seventh'); + testKey('seventh', [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyK)]); + testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyK), 'seventh'); - testKey('uncomment lines', [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U)]); - testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U), 'uncomment lines'); + testKey('uncomment lines', [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyU)]); + testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyU), 'uncomment lines'); - testKey('comment lines', [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C)]); - testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C), 'comment lines'); + testKey('comment lines', [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyC)]); + testResolve(createContext({}), KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyC), 'comment lines'); testKey('unreachablechord', []); - testKey('eleven', [KeyMod.CtrlCmd | KeyCode.KEY_G]); - testResolve(createContext({}), KeyMod.CtrlCmd | KeyCode.KEY_G, 'eleven'); + testKey('eleven', [KeyMod.CtrlCmd | KeyCode.KeyG]); + testResolve(createContext({}), KeyMod.CtrlCmd | KeyCode.KeyG, 'eleven'); testKey('sixth', []); }); diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index 9524765b90..7e580b21d2 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keybindings'; import { OS } from 'vs/base/common/platform'; import { ContextKeyExpression, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingEvent, IKeybindingService, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; diff --git a/src/vs/platform/keyboardLayout/common/keyboardLayout.ts b/src/vs/platform/keyboardLayout/common/keyboardLayout.ts index f529367614..dddc16ccf6 100644 --- a/src/vs/platform/keyboardLayout/common/keyboardLayout.ts +++ b/src/vs/platform/keyboardLayout/common/keyboardLayout.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { ScanCode, ScanCodeUtils } from 'vs/base/common/scanCode'; +import { ScanCode, ScanCodeUtils } from 'vs/base/common/keyCodes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { DispatchConfig } from 'vs/platform/keyboardLayout/common/dispatchConfig'; @@ -73,6 +73,7 @@ export interface IWindowsKeyboardLayoutInfo { */ export interface ILinuxKeyboardLayoutInfo { model: string; + group: number; layout: string; variant: string; options: string; diff --git a/src/vs/platform/keyboardLayout/common/keyboardMapper.ts b/src/vs/platform/keyboardLayout/common/keyboardMapper.ts index be3b660b60..af36118508 100644 --- a/src/vs/platform/keyboardLayout/common/keyboardMapper.ts +++ b/src/vs/platform/keyboardLayout/common/keyboardMapper.ts @@ -3,8 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; -import { ScanCodeBinding } from 'vs/base/common/scanCode'; +import { Keybinding, ResolvedKeybinding, SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; export interface IKeyboardMapper { diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 2e93d468ca..4b304f586d 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -19,7 +19,7 @@ import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/common/launch' import { ILogService } from 'vs/platform/log/common/log'; import { IURLService } from 'vs/platform/url/common/url'; import { IWindowSettings } from 'vs/platform/windows/common/windows'; -import { ICodeWindow, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; +import { ICodeWindow, IOpenConfiguration, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; @@ -121,9 +121,17 @@ export class LaunchMainService implements ILaunchMainService { const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; const remoteAuthority = args.remote || undefined; + const baseConfig: IOpenConfiguration = { + context, + cli: args, + userEnv, + waitMarkerFileURI, + remoteAuthority + }; + // Special case extension development if (!!args.extensionDevelopmentPath) { - this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, { context, cli: args, userEnv, waitMarkerFileURI, remoteAuthority }); + this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, baseConfig); } // Start without file/folder arguments @@ -159,13 +167,9 @@ export class LaunchMainService implements ILaunchMainService { // Open new Window if (openNewWindow) { usedWindows = this.windowsMainService.open({ - context, - cli: args, - userEnv, + ...baseConfig, forceNewWindow: true, - forceEmpty: true, - waitMarkerFileURI, - remoteAuthority + forceEmpty: true }); } @@ -173,11 +177,14 @@ export class LaunchMainService implements ILaunchMainService { else { const lastActive = this.windowsMainService.getLastActiveWindow(); if (lastActive) { - lastActive.focus(); + this.windowsMainService.openExistingWindow(lastActive, baseConfig); usedWindows = [lastActive]; } else { - usedWindows = this.windowsMainService.open({ context, cli: args, forceEmpty: true, remoteAuthority }); + usedWindows = this.windowsMainService.open({ + ...baseConfig, + forceEmpty: true + }); } } } @@ -185,18 +192,14 @@ export class LaunchMainService implements ILaunchMainService { // Start with file/folder arguments else { usedWindows = this.windowsMainService.open({ - context, - cli: args, - userEnv, + ...baseConfig, forceNewWindow: args['new-window'], preferNewWindow: !args['reuse-window'] && !args.wait, forceReuseWindow: args['reuse-window'], diffMode: args.diff, addMode: args.add, noRecentEntry: !!args['skip-add-to-recently-opened'], - waitMarkerFileURI, - gotoLineMode: args.goto, - remoteAuthority + gotoLineMode: args.goto }); } diff --git a/src/vs/platform/layout/browser/zIndexRegistry.ts b/src/vs/platform/layout/browser/zIndexRegistry.ts new file mode 100644 index 0000000000..e49fb6bdec --- /dev/null +++ b/src/vs/platform/layout/browser/zIndexRegistry.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 { clearNode, createCSSRule, createStyleSheet } from 'vs/base/browser/dom'; +import { RunOnceScheduler } from 'vs/base/common/async'; + +export enum ZIndex { + Base = 0, + Sash = 35, + SuggestWidget = 40, + Hover = 50, + DragImage = 1000, + MenubarMenuItemsHolder = 2000, // quick-input-widget + ContextView = 2500, + ModalDialog = 2600, + PaneDropOverlay = 10000 +} + +const ZIndexValues = Object.keys(ZIndex).filter(key => !isNaN(Number(key))).map(key => Number(key)).sort((a, b) => b - a); +function findBase(z: number) { + for (const zi of ZIndexValues) { + if (z >= zi) { + return zi; + } + } + + return -1; +} + +class ZIndexRegistry { + private styleSheet: HTMLStyleElement; + private zIndexMap: Map; + private scheduler: RunOnceScheduler; + constructor() { + this.styleSheet = createStyleSheet(); + this.zIndexMap = new Map(); + this.scheduler = new RunOnceScheduler(() => this.updateStyleElement(), 200); + } + + registerZIndex(relativeLayer: ZIndex, z: number, name: string): string { + if (this.zIndexMap.get(name)) { + throw new Error(`z-index with name ${name} has already been registered.`); + } + + const proposedZValue = relativeLayer + z; + if (findBase(proposedZValue) !== relativeLayer) { + throw new Error(`Relative layer: ${relativeLayer} + z-index: ${z} exceeds next layer ${proposedZValue}.`); + } + + this.zIndexMap.set(name, proposedZValue); + this.scheduler.schedule(); + return this.getVarName(name); + } + + private getVarName(name: string): string { + return `--z-index-${name}`; + } + + private updateStyleElement(): void { + clearNode(this.styleSheet); + let ruleBuilder = ''; + this.zIndexMap.forEach((zIndex, name) => { + ruleBuilder += `${this.getVarName(name)}: ${zIndex};\n`; + }); + createCSSRule('*', ruleBuilder, this.styleSheet); + } +} + +const zIndexRegistry = new ZIndexRegistry(); + +export function registerZIndex(relativeLayer: ZIndex, z: number, name: string): string { + return zIndexRegistry.registerZIndex(relativeLayer, z, name); +} diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index c96e0b7cad..e9d36578f6 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -1165,7 +1165,7 @@ class WorkbenchTreeInternals { newOptions = { ...newOptions, renderIndentGuides }; } if (e.affectsConfiguration(listSmoothScrolling)) { - const smoothScrolling = Boolean(!!configurationService.getValue(listSmoothScrolling)); + const smoothScrolling = Boolean(configurationService.getValue(listSmoothScrolling)); newOptions = { ...newOptions, smoothScrolling }; } if (e.affectsConfiguration(keyboardNavigationSettingKey)) { @@ -1175,7 +1175,7 @@ class WorkbenchTreeInternals { newOptions = { ...newOptions, automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }; } if (e.affectsConfiguration(horizontalScrollingKey) && options.horizontalScrolling === undefined) { - const horizontalScrolling = Boolean(!!configurationService.getValue(horizontalScrollingKey)); + const horizontalScrolling = Boolean(configurationService.getValue(horizontalScrollingKey)); newOptions = { ...newOptions, horizontalScrolling }; } if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) { @@ -1286,12 +1286,12 @@ configurationRegistry.registerConfiguration({ [mouseWheelScrollSensitivityKey]: { type: 'number', default: 1, - description: localize('Mouse Wheel Scroll Sensitivity', "A multiplier to be used on the deltaX and deltaY of mouse wheel scroll events.") + description: localize('Mouse Wheel Scroll Sensitivity', "A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.") }, [fastScrollSensitivityKey]: { type: 'number', default: 5, - description: localize('Fast Scroll Sensitivity', "Scrolling speed multiplier when pressing Alt.") + description: localize('Fast Scroll Sensitivity', "Scrolling speed multiplier when pressing `Alt`.") }, [keyboardNavigationSettingKey]: { type: 'string', diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 3ca0dc4485..ac12ab029d 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -108,6 +108,8 @@ export interface ICommonNativeHostService { getOSStatistics(): Promise; getOSVirtualMachineHint(): Promise; + getOSColorScheme(): Promise; + // Process killProcess(pid: number, code: string): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index d289c0f5c5..db5fddb71a 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -72,10 +72,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain // Color Scheme changes nativeTheme.on('updated', () => { - this._onDidChangeColorScheme.fire({ - highContrast: nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors, - dark: nativeTheme.shouldUseDarkColors - }); + this._onDidChangeColorScheme.fire(this.osColorScheme); }); } @@ -596,6 +593,18 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return virtualMachineHint.value(); } + private get osColorScheme(): IColorScheme { + return { + highContrast: nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors, + dark: nativeTheme.shouldUseDarkColors + }; + } + + public async getOSColorScheme(): Promise { + return this.osColorScheme; + } + + //#endregion @@ -649,7 +658,13 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region macOS Touchbar async newWindowTab(): Promise { - this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentMainService.args, forceNewTabbedWindow: true, forceEmpty: true, remoteAuthority: this.environmentMainService.args.remote || undefined }); + this.windowsMainService.open({ + context: OpenContext.API, + cli: this.environmentMainService.args, + forceNewTabbedWindow: true, + forceEmpty: true, + remoteAuthority: this.environmentMainService.args.remote || undefined + }); } async showPreviousWindowTab(): Promise { diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 64711aa030..cc3b663a25 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -3,9 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, EventHelper, EventLike } from 'vs/base/browser/dom'; +import { $, append, EventHelper, EventLike, clearNode } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch'; import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -14,9 +15,10 @@ import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/ import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; export interface ILinkDescriptor { - readonly label: string; + readonly label: string | HTMLElement; readonly href: string; readonly title?: string; + readonly tabIndex?: number; } export interface ILinkOptions { @@ -26,7 +28,7 @@ export interface ILinkOptions { export class Link extends Disposable { - readonly el: HTMLAnchorElement; + readonly el: HTMLAnchorElement; // {{SQL CARBON EDIT}} - make public private _enabled: boolean = true; get enabled(): boolean { @@ -53,18 +55,42 @@ export class Link extends Disposable { this._enabled = enabled; } + set link(link: ILinkDescriptor) { + if (typeof link.label === 'string') { + this.el.textContent = link.label; + } else { + clearNode(this.el); + this.el.appendChild(link.label); + } + + this.el.href = link.href; + + if (typeof link.tabIndex !== 'undefined') { + this.el.tabIndex = link.tabIndex; + } + + if (typeof link.title !== 'undefined') { + this.el.title = link.title; + } + + this._link = link; + } + constructor( - link: ILinkDescriptor, - options: ILinkOptions | undefined = undefined, + container: HTMLElement, + private _link: ILinkDescriptor, + options: ILinkOptions = {}, @IOpenerService openerService: IOpenerService ) { super(); - this.el = $('a.monaco-link', { - tabIndex: 0, - href: link.href, - title: link.title - }, link.label); + this.el = append(container, $('a.monaco-link', { + tabIndex: _link.tabIndex ?? 0, + href: _link.href, + title: _link.title + }, _link.label)); + + this.el.setAttribute('role', 'button'); const onClickEmitter = this._register(new DomEmitter(this.el, 'click')); const onKeyPress = this._register(new DomEmitter(this.el, 'keypress')); @@ -72,7 +98,9 @@ export class Link extends Disposable { .map(e => new StandardKeyboardEvent(e)) .filter(e => e.keyCode === KeyCode.Enter) .event; - const onOpen = Event.any(onClickEmitter.event, onEnterPress); + const onTap = this._register(new DomEmitter(this.el, TouchEventType.Tap)).event; + this._register(Gesture.addTarget(this.el)); + const onOpen = Event.any(onClickEmitter.event, onEnterPress, onTap); this._register(onOpen(e => { if (!this.enabled) { @@ -82,9 +110,9 @@ export class Link extends Disposable { EventHelper.stop(e, true); if (options?.opener) { - options.opener(link.href); + options.opener(this._link.href); } else { - openerService.open(link.href, { allowCommands: true }); + openerService.open(this._link.href, { allowCommands: true }); } })); diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index bfcdc34f93..c39585b237 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { FileAccess } from 'vs/base/common/network'; -import { globals, isWeb } from 'vs/base/common/platform'; +import { globals } from 'vs/base/common/platform'; import { env } from 'vs/base/common/process'; import { IProductConfiguration } from 'vs/base/common/product'; import { dirname, joinPath } from 'vs/base/common/resources'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; +/** + * @deprecated You MUST use `IProductService` if possible. + */ let product: IProductConfiguration; // Native sandbox environment @@ -54,10 +57,10 @@ else { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.33.0-dev', - vscodeVersion: '1.59.0-dev', - nameLong: isWeb ? 'Azure Data Studio Web Dev' : 'Azure Data Studio Dev', - nameShort: isWeb ? 'Azure Data Studio Web Dev' : 'Azure Data Studio Dev', + version: '1.37.0-dev', + vscodeVersion: '1.62.0-dev', + nameLong: 'Azure Data Studio Dev', + nameShort: 'Azure Data Studio Dev', applicationName: 'azuredatastudio-oss', dataFolderName: '.azuredatastudio-oss', urlProtocol: 'azuredatastudio-oss', @@ -76,4 +79,7 @@ else { } } +/** + * @deprecated You MUST use `IProductService` if possible. + */ export default product; diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index a5f78f77ed..a5c2ca623b 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -302,8 +302,15 @@ export abstract class PickerQuickAccessProvider activeItem !== removed[0]); + const keepScrollPositionBefore = picker.keepScrollPosition; + picker.keepScrollPosition = true; picker.items = items; + if (activeItems) { + picker.activeItems = activeItems; + } + picker.keepScrollPosition = keepScrollPositionBefore; } break; } diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index 83b8e01b5f..e4b34941c6 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -39,17 +39,19 @@ function connectionTypeToString(connectionType: ConnectionType): string { export interface AuthRequest { type: 'auth'; auth: string; + data: string; } export interface SignRequest { type: 'sign'; data: string; + signedData: string; } export interface ConnectionTypeRequest { type: 'connectionType'; commit?: string; - signedData?: string; + signedData: string; desiredConnectionType?: ConnectionType; args?: any; } @@ -250,9 +252,12 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio } options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`); + const message = await raceWithTimeoutCancellation(options.signService.createNewMessage(generateUuid()), timeoutCancellationToken); + const authRequest: AuthRequest = { type: 'auth', - auth: options.connectionToken || '00000000000000000000' + auth: options.connectionToken || '00000000000000000000', + data: message.data }; protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest))); @@ -267,6 +272,13 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`); + const isValid = await raceWithTimeoutCancellation(options.signService.validate(message, msg.signedData), timeoutCancellationToken); + if (!isValid) { + const error: any = new Error('Refused to connect to unsupported server'); + error.code = 'VSCODE_CONNECTION_ERROR'; + throw error; + } + const signed = await raceWithTimeoutCancellation(options.signService.sign(msg.data), timeoutCancellationToken); const connTypeRequest: ConnectionTypeRequest = { type: 'connectionType', @@ -354,6 +366,7 @@ async function doConnectRemoteAgentExtensionHost(options: ISimpleConnectionOptio } export interface ITunnelConnectionStartParams { + host: string; port: number; } @@ -427,9 +440,9 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption } } -export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise { +export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number): Promise { const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null); - const protocol = await doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }, CancellationToken.None); + const protocol = await doConnectRemoteAgentTunnel(simpleOptions, { host: tunnelRemoteHost, port: tunnelRemotePort }, CancellationToken.None); return protocol; } diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index cf3407ddb4..0c27cc2256 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -19,6 +19,7 @@ export interface IRemoteAgentEnvironment { workspaceStorageHome: URI; userHome: URI; os: OperatingSystem; + arch: string; marks: performance.PerformanceMark[]; useHostProxy: boolean; } diff --git a/src/vs/platform/remote/common/sharedProcessTunnelService.ts b/src/vs/platform/remote/common/sharedProcessTunnelService.ts new file mode 100644 index 0000000000..a20e95fdae --- /dev/null +++ b/src/vs/platform/remote/common/sharedProcessTunnelService.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection'; + +export const ISharedProcessTunnelService = createDecorator('sharedProcessTunnelService'); + +export const ipcSharedProcessTunnelChannelName = 'sharedProcessTunnel'; + +export interface ISharedProcessTunnel { + tunnelLocalPort: number | undefined; + localAddress: string; +} + +/** + * A service that creates tunnels on the shared process + */ +export interface ISharedProcessTunnelService { + readonly _serviceBrand: undefined; + + /** + * Create a tunnel. + */ + createTunnel(): Promise<{ id: string }>; + /** + * Start a previously created tunnel. + * Can only be called once per created tunnel. + */ + startTunnel(authority: string, id: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort: number | undefined, elevateIfNeeded: boolean | undefined): Promise; + /** + * Set the remote address info for a previously created tunnel. + * Should be called as often as the resolver resolves. + */ + setAddress(id: string, address: IAddress): Promise; + /** + * Destroy a previously created tunnel. + */ + destroyTunnel(id: string): Promise; +} diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index 9200c1ca15..08fd11cee1 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -13,13 +13,14 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; export const ITunnelService = createDecorator('tunnelService'); +export const ISharedTunnelsService = createDecorator('sharedTunnelsService'); export interface RemoteTunnel { readonly tunnelRemotePort: number; readonly tunnelRemoteHost: string; readonly tunnelLocalPort?: number; readonly localAddress: string; - readonly public: boolean; + readonly privacy: string; readonly protocol?: string; dispose(silent?: boolean): Promise; } @@ -29,6 +30,7 @@ export interface TunnelOptions { localAddressPort?: number; label?: string; public?: boolean; + privacy?: string; protocol?: string; } @@ -37,13 +39,29 @@ export enum TunnelProtocol { Https = 'https' } +export enum TunnelPrivacyId { + ConstantPrivate = 'constantPrivate', // private, and changing is unsupported + Private = 'private', + Public = 'public' +} + export interface TunnelCreationOptions { elevationRequired?: boolean; } +export interface TunnelPrivacy { + themeIcon: string; + id: string; + label: string; +} + export interface TunnelProviderFeatures { elevation: boolean; + /** + * @deprecated + */ public: boolean; + privacyOptions: TunnelPrivacy[]; } export interface ITunnelProvider { @@ -76,8 +94,13 @@ export interface ITunnel { */ localAddress: string; + /** + * @deprecated Use privacy instead + */ public?: boolean; + privacy?: string; + protocol?: string; /** @@ -88,11 +111,18 @@ export interface ITunnel { dispose(): Promise | void; } +export interface ISharedTunnelsService { + readonly _serviceBrand: undefined; + + openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise | undefined; +} + export interface ITunnelService { readonly _serviceBrand: undefined; readonly tunnels: Promise; - readonly canMakePublic: boolean; + readonly canChangePrivacy: boolean; + readonly privacyOptions: TunnelPrivacy[]; readonly onTunnelOpened: Event; readonly onTunnelClosed: Event<{ host: string, port: number; }>; readonly canElevate: boolean; @@ -100,7 +130,7 @@ export interface ITunnelService { readonly onAddedTunnelProvider: Event; canTunnel(uri: URI): boolean; - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean, protocol?: string): Promise | undefined; + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise | undefined; closeTunnel(remoteHost: string, remotePort: number): Promise; setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable; } @@ -149,7 +179,7 @@ export abstract class AbstractTunnelService implements ITunnelService { protected readonly _tunnels = new Map; }>>(); protected _tunnelProvider: ITunnelProvider | undefined; protected _canElevate: boolean = false; - private _canMakePublic: boolean = false; + private _privacyOptions: TunnelPrivacy[] = []; public constructor( @ILogService protected readonly logService: ILogService @@ -164,20 +194,20 @@ export abstract class AbstractTunnelService implements ITunnelService { if (!provider) { // clear features this._canElevate = false; - this._canMakePublic = false; + this._privacyOptions = []; this._onAddedTunnelProvider.fire(); return { dispose: () => { } }; } this._canElevate = features.elevation; - this._canMakePublic = features.public; + this._privacyOptions = features.privacyOptions; this._onAddedTunnelProvider.fire(); return { dispose: () => { this._tunnelProvider = undefined; this._canElevate = false; - this._canMakePublic = false; + this._privacyOptions = []; } }; } @@ -186,25 +216,31 @@ export abstract class AbstractTunnelService implements ITunnelService { return this._canElevate; } - public get canMakePublic() { - return this._canMakePublic; + public get canChangePrivacy() { + return this._privacyOptions.length > 0; + } + + public get privacyOptions() { + return this._privacyOptions; } public get tunnels(): Promise { - return new Promise(async (resolve) => { - const tunnels: RemoteTunnel[] = []; - const tunnelArray = Array.from(this._tunnels.values()); - for (let portMap of tunnelArray) { - const portArray = Array.from(portMap.values()); - for (let x of portArray) { - const tunnelValue = await x.value; - if (tunnelValue) { - tunnels.push(tunnelValue); - } + return this.getTunnels(); + } + + private async getTunnels(): Promise { + const tunnels: RemoteTunnel[] = []; + const tunnelArray = Array.from(this._tunnels.values()); + for (let portMap of tunnelArray) { + const portArray = Array.from(portMap.values()); + for (let x of portArray) { + const tunnelValue = await x.value; + if (tunnelValue) { + tunnels.push(tunnelValue); } } - resolve(tunnels); - }); + } + return tunnels; } async dispose(): Promise { @@ -217,7 +253,7 @@ export abstract class AbstractTunnelService implements ITunnelService { this._tunnels.clear(); } - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false, protocol?: string): Promise | undefined { + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, privacy: string = TunnelPrivacyId.Private, protocol?: string): Promise | undefined { this.logService.trace(`ForwardedPorts: (TunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`); if (!addressProvider) { return undefined; @@ -227,7 +263,7 @@ export abstract class AbstractTunnelService implements ITunnelService { remoteHost = 'localhost'; } - const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic, protocol); + const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); if (!resolvedTunnel) { this.logService.trace(`ForwardedPorts: (TunnelService) Tunnel was not created.`); return resolvedTunnel; @@ -255,7 +291,7 @@ export abstract class AbstractTunnelService implements ITunnelService { tunnelRemoteHost: tunnel.tunnelRemoteHost, tunnelLocalPort: tunnel.tunnelLocalPort, localAddress: tunnel.localAddress, - public: tunnel.public, + privacy: tunnel.privacy, protocol: tunnel.protocol, dispose: async () => { this.logService.trace(`ForwardedPorts: (TunnelService) dispose request for ${tunnel.tunnelRemoteHost}:${tunnel.tunnelRemotePort} `); @@ -344,14 +380,14 @@ export abstract class AbstractTunnelService implements ITunnelService { return !!extractLocalHostUriMetaDataForPortMapping(uri); } - protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean, protocol?: string): Promise | undefined; + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise | undefined; - protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean, protocol?: string): Promise | undefined { + protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise | undefined { this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`); const preferredLocalPort = localPort === undefined ? remotePort : localPort; const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; - const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic, protocol }; + const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, privacy, public: privacy !== TunnelPrivacyId.Private, protocol }; const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo); this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created by provider.'); if (tunnel) { diff --git a/src/vs/platform/remote/electron-sandbox/sharedProcessTunnelService.ts b/src/vs/platform/remote/electron-sandbox/sharedProcessTunnelService.ts new file mode 100644 index 0000000000..b6c7edecf9 --- /dev/null +++ b/src/vs/platform/remote/electron-sandbox/sharedProcessTunnelService.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; +import { ISharedProcessTunnelService, ipcSharedProcessTunnelChannelName } from 'vs/platform/remote/common/sharedProcessTunnelService'; + +registerSharedProcessRemoteService(ISharedProcessTunnelService, ipcSharedProcessTunnelChannelName, { supportsDelayedInstantiation: true }); diff --git a/src/vs/platform/remote/node/sharedProcessTunnelService.ts b/src/vs/platform/remote/node/sharedProcessTunnelService.ts new file mode 100644 index 0000000000..96956c7571 --- /dev/null +++ b/src/vs/platform/remote/node/sharedProcessTunnelService.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILogService } from 'vs/platform/log/common/log'; +import { ISharedProcessTunnel, ISharedProcessTunnelService } from 'vs/platform/remote/common/sharedProcessTunnelService'; +import { ISharedTunnelsService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { IAddress, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { canceled } from 'vs/base/common/errors'; +import { DeferredPromise } from 'vs/base/common/async'; + +class TunnelData extends Disposable implements IAddressProvider { + + private _address: IAddress | null; + private _addressPromise: DeferredPromise | null; + + constructor() { + super(); + this._address = null; + this._addressPromise = null; + } + + async getAddress(): Promise { + if (this._address) { + // address is resolved + return this._address; + } + if (!this._addressPromise) { + this._addressPromise = new DeferredPromise(); + } + return this._addressPromise.p; + } + + setAddress(address: IAddress): void { + this._address = address; + if (this._addressPromise) { + this._addressPromise.complete(address); + this._addressPromise = null; + } + } + + setTunnel(tunnel: RemoteTunnel): void { + this._register(tunnel); + } +} + +export class SharedProcessTunnelService extends Disposable implements ISharedProcessTunnelService { + _serviceBrand: undefined; + + private static _lastId = 0; + + private readonly _tunnels: Map = new Map(); + private readonly _disposedTunnels: Set = new Set(); + + constructor( + @ISharedTunnelsService private readonly _tunnelService: ISharedTunnelsService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + } + + public override dispose(): void { + super.dispose(); + this._tunnels.forEach((tunnel) => tunnel.dispose()); + } + + async createTunnel(): Promise<{ id: string }> { + const id = String(++SharedProcessTunnelService._lastId); + return { id }; + } + + async startTunnel(authority: string, id: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort: number | undefined, elevateIfNeeded: boolean | undefined): Promise { + const tunnelData = new TunnelData(); + + const tunnel = await Promise.resolve(this._tunnelService.openTunnel(authority, tunnelData, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort, elevateIfNeeded)); + if (!tunnel) { + this._logService.info(`[SharedProcessTunnelService] Could not create a tunnel to ${tunnelRemoteHost}:${tunnelRemotePort} (remote).`); + tunnelData.dispose(); + throw new Error(`Could not create tunnel`); + } + + if (this._disposedTunnels.has(id)) { + // This tunnel was disposed in the meantime + this._disposedTunnels.delete(id); + tunnelData.dispose(); + await tunnel.dispose(); + throw canceled(); + } + + tunnelData.setTunnel(tunnel); + this._tunnels.set(id, tunnelData); + + this._logService.info(`[SharedProcessTunnelService] Created tunnel ${id}: ${tunnel.localAddress} (local) to ${tunnelRemoteHost}:${tunnelRemotePort} (remote).`); + const result: ISharedProcessTunnel = { + tunnelLocalPort: tunnel.tunnelLocalPort, + localAddress: tunnel.localAddress + }; + return result; + } + + async setAddress(id: string, address: IAddress): Promise { + const tunnel = this._tunnels.get(id); + if (!tunnel) { + return; + } + tunnel.setAddress(address); + } + + async destroyTunnel(id: string): Promise { + const tunnel = this._tunnels.get(id); + if (tunnel) { + this._logService.info(`[SharedProcessTunnelService] Disposing tunnel ${id}.`); + this._tunnels.delete(id); + await tunnel.dispose(); + return; + } + + // Looks like this tunnel is still starting, mark the id as disposed + this._disposedTunnels.add(id); + } +} diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 84db72bcbb..9f1dca63e6 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as net from 'net'; -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 { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; + +import { Barrier } from 'vs/base/common/async'; +import { Disposable } from 'vs/base/common/lifecycle'; 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, IAddressProvider, IConnectionOptions, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; -import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; -import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; +import { AbstractTunnelService, isAllInterfaces, ISharedTunnelsService as ISharedTunnelsService, isLocalhost, ITunnelService, RemoteTunnel, TunnelPrivacyId } from 'vs/platform/remote/common/tunnel'; import { ISignService } from 'vs/platform/sign/common/sign'; async function createRemoteTunnel(options: IConnectionOptions, defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { @@ -27,7 +28,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { public tunnelLocalPort!: number; public tunnelRemoteHost: string; public localAddress!: string; - public readonly public = false; + public readonly privacy = TunnelPrivacyId.Private; private readonly _options: IConnectionOptions; private readonly _server: net.Server; @@ -98,7 +99,8 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { // pause reading on the socket until we have a chance to forward its data localSocket.pause(); - const protocol = await connectRemoteAgentTunnel(this._options, this.tunnelRemotePort); + const tunnelRemoteHost = (isLocalhost(this.tunnelRemoteHost) || isAllInterfaces(this.tunnelRemoteHost)) ? 'localhost' : this.tunnelRemoteHost; + const protocol = await connectRemoteAgentTunnel(this._options, tunnelRemoteHost, this.tunnelRemotePort); const remoteSocket = (protocol.getSocket()).socket; const dataChunk = protocol.readEntireBuffer(); protocol.dispose(); @@ -145,10 +147,11 @@ export class BaseTunnelService extends AbstractTunnelService { } private get defaultTunnelHost(): string { - return (this.configurationService.getValue('remote.localPortHost') === 'localhost') ? '127.0.0.1' : '0.0.0.0'; + const settingValue = this.configurationService.getValue('remote.localPortHost'); + return (!settingValue || settingValue === 'localhost') ? '127.0.0.1' : '0.0.0.0'; } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean, protocol?: string): Promise | undefined { + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; @@ -156,7 +159,7 @@ export class BaseTunnelService extends AbstractTunnelService { } if (this._tunnelProvider) { - return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic, protocol); + return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); } else { this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`); const options: IConnectionOptions = { @@ -186,3 +189,33 @@ export class TunnelService extends BaseTunnelService { super(nodeSocketFactory, logService, signService, productService, configurationService); } } + +export class SharedTunnelsService extends Disposable implements ISharedTunnelsService { + declare readonly _serviceBrand: undefined; + private readonly _tunnelServices: Map = new Map(); + + public constructor( + @ILogService protected readonly logService: ILogService, + @IProductService private readonly productService: IProductService, + @ISignService private readonly signService: ISignService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + } + + async openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise { + this.logService.trace(`ForwardedPorts: (SharedTunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`); + if (!this._tunnelServices.has(authority)) { + const tunnelService = new TunnelService(this.logService, this.signService, this.productService, this.configurationService); + this._register(tunnelService); + this._tunnelServices.set(authority, tunnelService); + tunnelService.onTunnelClosed(async () => { + if ((await tunnelService.tunnels).length === 0) { + tunnelService.dispose(); + this._tunnelServices.delete(authority); + } + }); + } + return this._tunnelServices.get(authority)!.openTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); + } +} diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 89046c4000..6b733b123f 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -73,9 +73,7 @@ export function updateProxyConfigurationsScope(scope: ConfigurationScope): void let proxyConfiguration: IConfigurationNode | undefined; function registerProxyConfigurations(scope: ConfigurationScope): void { const configurationRegistry = Registry.as(Extensions.Configuration); - if (proxyConfiguration) { - configurationRegistry.deregisterConfigurations([proxyConfiguration]); - } + const oldProxyConfiguration = proxyConfiguration; proxyConfiguration = { id: 'http', order: 15, @@ -122,7 +120,7 @@ function registerProxyConfigurations(scope: ConfigurationScope): void { } } }; - configurationRegistry.registerConfiguration(proxyConfiguration); + configurationRegistry.updateConfigurations({ add: [proxyConfiguration], remove: oldProxyConfiguration ? [oldProxyConfiguration] : [] }); } registerProxyConfigurations(ConfigurationScope.MACHINE); diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 88c6577922..a71bb19109 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -6,6 +6,7 @@ import * as http from 'http'; import * as https from 'https'; import { parse as parseUrl } from 'url'; +import { Promises } from 'vs/base/common/async'; import { streamToBufferReadableStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; @@ -63,9 +64,17 @@ export class RequestService extends Disposable implements IRequestService { this.logService.trace('RequestService#request', options.url); const { proxyUrl, strictSSL } = this; + + let shellEnv: typeof process.env | undefined = undefined; + try { + shellEnv = await resolveShellEnv(this.logService, this.environmentService.args, process.env); + } catch (error) { + this.logService.error('RequestService#request resolving shell environment failed', error); + } + const env = { ...process.env, - ...(await resolveShellEnv(this.logService, this.environmentService.args, process.env)), + ...shellEnv }; const agent = options.agent ? options.agent : await getProxyAgent(options.url || '', env, { proxyUrl, strictSSL }); @@ -90,7 +99,7 @@ export class RequestService extends Disposable implements IRequestService { private _request(options: NodeRequestOptions, token: CancellationToken): Promise { - return new Promise(async (c, e) => { + return Promises.withAsyncBody(async (c, e) => { let req: http.ClientRequest; const endpoint = parseUrl(options.url!); diff --git a/src/vs/platform/sharedProcess/common/sharedProcessWorkerService.ts b/src/vs/platform/sharedProcess/common/sharedProcessWorkerService.ts new file mode 100644 index 0000000000..43b11d741e --- /dev/null +++ b/src/vs/platform/sharedProcess/common/sharedProcessWorkerService.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { hash as hashObject } from 'vs/base/common/hash'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export interface ISharedProcessWorkerProcess { + + /** + * The module to load as child process into the worker. + */ + moduleId: string; + + /** + * The type of the process appears in the arguments of the + * forked process to identify it easier. + */ + type: string; +} + +export interface IOnDidTerminateSharedProcessWorkerProcess { + + /** + * More information around how the shared process worker + * process terminated. Will be `undefined` in case the + * worker process was terminated normally via APIs + * and will be defined in case the worker process + * terminated on its own, either unexpectedly or + * because it finished. + */ + reason?: ISharedProcessWorkerProcessExit; +} + +export interface ISharedProcessWorkerProcessExit { + + /** + * The shared process worker process exit code if known. + */ + code?: number; + + /** + * The shared process worker process exit signal if known. + */ + signal?: string; +} + +export interface ISharedProcessWorkerConfiguration { + + /** + * Configuration specific to the process to fork. + */ + process: ISharedProcessWorkerProcess; + + /** + * Configuration specific for how to respond with the + * communication message port to the receiver window. + */ + reply: { + windowId: number; + channel?: string; + nonce?: string; + }; +} + +/** + * Converts the process configuration into a hash to + * identify processes of the same kind by taking those + * components that make the process and reply unique. + */ +export function hash(configuration: ISharedProcessWorkerConfiguration): number { + return hashObject({ + moduleId: configuration.process.moduleId, + windowId: configuration.reply.windowId + }); +} + +export const ISharedProcessWorkerService = createDecorator('sharedProcessWorkerService'); + +export const ipcSharedProcessWorkerChannelName = 'sharedProcessWorker'; + +export interface ISharedProcessWorkerService { + + readonly _serviceBrand: undefined; + + /** + * Will fork a new process with the provided module identifier off the shared + * process and establishes a message port connection to that process. The other + * end of the message port connection will be sent back to the calling window + * as identified by the `reply` configuration. + * + * Requires the forked process to be AMD module that uses our IPC channel framework + * to respond to the provided `channelName` as a server. + * + * The process will be automatically terminated when the receiver window closes, + * crashes or loads/reloads. It can also explicitly be terminated by calling + * `disposeWorker`. + * + * Note on affinity: repeated calls to `createWorker` with the same `moduleId` from + * the same window will result in any previous forked process to get terminated. + * In other words, it is not possible, nor intended to create multiple workers of + * the same process from one window. The intent of these workers is to be reused per + * window and the communication channel allows to dynamically update the processes + * after the fact. + * + * @returns a promise that resolves then the worker terminated. Provides more details + * about the termination that can be used to figure out if the termination was unexpected + * or not and whether the worker needs to be restarted. + */ + createWorker(configuration: ISharedProcessWorkerConfiguration): Promise; + + /** + * Terminates the process for the provided configuration if any. + */ + disposeWorker(configuration: ISharedProcessWorkerConfiguration): Promise; +} diff --git a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorker.ts b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorker.ts new file mode 100644 index 0000000000..7a3ddfd706 --- /dev/null +++ b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorker.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ISharedProcessWorkerConfiguration } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; + +export enum SharedProcessWorkerMessages { + + // Process + Spawn = 'vscode:shared-process->shared-process-worker=spawn', + Terminate = 'vscode:shared-process->shared-process-worker=terminate', + SelfTerminated = 'vscode:shared-process-worker->shared-process=selfTerminated', + + // Lifecycle + Ready = 'vscode:shared-process-worker->shared-process=ready', + Ack = 'vscode:shared-process-worker->shared-process=ack', + + // Diagnostics + Trace = 'vscode:shared-process-worker->shared-process=trace', + Info = 'vscode:shared-process-worker->shared-process=info', + Warn = 'vscode:shared-process-worker->shared-process=warn', + Error = 'vscode:shared-process-worker->shared-process=error' +} + +export interface ISharedProcessWorkerEnvironment { + + /** + * Full absolute path to our `bootstrap-fork.js` file. + */ + bootstrapPath: string; +} + +interface IBaseMessage { + id: string; + nonce?: string; +} + +export interface ISharedProcessToWorkerMessage extends IBaseMessage { + configuration: ISharedProcessWorkerConfiguration; + environment?: ISharedProcessWorkerEnvironment; +} + +export interface IWorkerToSharedProcessMessage extends IBaseMessage { + configuration?: ISharedProcessWorkerConfiguration; + message?: string; +} diff --git a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts new file mode 100644 index 0000000000..3c439d05d7 --- /dev/null +++ b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ChildProcess, fork } from 'child_process'; +import { log } from 'console'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { isRemoteConsoleLog } from 'vs/base/common/console'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { deepClone } from 'vs/base/common/objects'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { hash, ISharedProcessWorkerConfiguration, ISharedProcessWorkerProcessExit } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; +import { SharedProcessWorkerMessages, ISharedProcessToWorkerMessage, ISharedProcessWorkerEnvironment, IWorkerToSharedProcessMessage } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorker'; + +/** + * The `create` function needs to be there by convention because + * we are loaded via the `vs/base/worker/workerMain` utility. + */ +export function create(): { onmessage: (message: ISharedProcessToWorkerMessage, transfer?: Transferable[]) => void } { + const sharedProcessWorkerMain = new SharedProcessWorkerMain(); + + // Signal we are ready + send({ id: SharedProcessWorkerMessages.Ready }); + + return { + onmessage: (message, transfer) => sharedProcessWorkerMain.onMessage(message, transfer) + }; +} + +class SharedProcessWorkerMain { + + private readonly processes = new Map(); + + onMessage(message: ISharedProcessToWorkerMessage, transfer?: Transferable[]): void { + + // Handle message from shared process + switch (message.id) { + + // Spawn new process + case SharedProcessWorkerMessages.Spawn: + if (transfer && transfer[0] instanceof MessagePort && message.environment) { + this.spawn(transfer[0], message.configuration, message.environment); + } + break; + + // Terminate existing process + case SharedProcessWorkerMessages.Terminate: + this.terminate(message.configuration); + break; + + default: + Logger.warn(`Unexpected shared process message '${message}'`); + } + + // Acknowledge message processed if we have a nonce + if (message.nonce) { + send({ + id: SharedProcessWorkerMessages.Ack, + nonce: message.nonce + }); + } + } + + private spawn(port: MessagePort, configuration: ISharedProcessWorkerConfiguration, environment: ISharedProcessWorkerEnvironment): void { + try { + + // Ensure to terminate any existing process for config + this.terminate(configuration); + + // Spawn a new worker process with given configuration + const process = new SharedProcessWorkerProcess(port, configuration, environment); + process.spawn(); + + // Handle self termination of the child process + const listener = Event.once(process.onDidProcessSelfTerminate)(reason => { + send({ + id: SharedProcessWorkerMessages.SelfTerminated, + configuration, + message: JSON.stringify(reason) + }); + }); + + // Remember in map for lifecycle + const configurationHash = hash(configuration); + this.processes.set(configurationHash, toDisposable(() => { + listener.dispose(); + + // Terminate process + process.dispose(); + + // Remove from processes + this.processes.delete(configurationHash); + })); + } catch (error) { + Logger.error(`Unexpected error forking worker process: ${toErrorMessage(error)}`); + } + } + + private terminate(configuration: ISharedProcessWorkerConfiguration): void { + const processDisposable = this.processes.get(hash(configuration)); + if (processDisposable) { + processDisposable.dispose(); + } + } +} + +class SharedProcessWorkerProcess extends Disposable { + + private readonly _onDidProcessSelfTerminate = this._register(new Emitter()); + readonly onDidProcessSelfTerminate = this._onDidProcessSelfTerminate.event; + + private child: ChildProcess | undefined = undefined; + + constructor( + private readonly port: MessagePort, + private readonly configuration: ISharedProcessWorkerConfiguration, + private readonly environment: ISharedProcessWorkerEnvironment + ) { + super(); + } + + spawn(): void { + Logger.trace('Forking worker process'); + + // Fork module via bootstrap-fork for AMD support + this.child = fork( + this.environment.bootstrapPath, + [`--type=${this.configuration.process.type}`], + { env: this.getEnv() } + ); + + Logger.info(`Starting worker process with pid ${this.child.pid} (type: ${this.configuration.process.type}, window: ${this.configuration.reply.windowId}).`); + + // Re-emit errors to outside + const onError = Event.fromNodeEventEmitter(this.child, 'error'); + this._register(onError(error => Logger.warn(`Error from child process: ${toErrorMessage(error)}`))); + + // Handle termination that happens from the process + // itself. This can either be a crash or the process + // not being long running. + const onExit = Event.fromNodeEventEmitter<{ code: number | null, signal: NodeJS.Signals | null }>(this.child, 'exit', (code: number | null, signal: NodeJS.Signals | null) => ({ code, signal })); + this._register(onExit(({ code, signal }) => { + const logMsg = `Worker process with pid ${this.child?.pid} terminated by itself with code ${code}, signal: ${signal} (type: ${this.configuration.process.type}, window: ${this.configuration.reply.windowId})`; + if (code !== 0 && signal !== 'SIGTERM') { + Logger.error(logMsg); + } else { + Logger.info(logMsg); + } + + this.child = undefined; + + this._onDidProcessSelfTerminate.fire({ + code: withNullAsUndefined(code), + signal: withNullAsUndefined(signal) + }); + })); + + const onMessageEmitter = this._register(new Emitter()); + const onRawMessage = Event.fromNodeEventEmitter(this.child, 'message', msg => msg); + this._register(onRawMessage(msg => { + + // Handle remote console logs specially + if (isRemoteConsoleLog(msg)) { + log(msg, `SharedProcess worker`); + } + + // Anything else goes to the outside + else { + onMessageEmitter.fire(VSBuffer.wrap(Buffer.from(msg, 'base64'))); + } + })); + + const send = (buffer: VSBuffer) => { + if (this.child?.connected) { + this.child.send((buffer.buffer).toString('base64')); + } else { + Logger.warn('Unable to deliver message to disconnected child'); + } + }; + + // Re-emit messages from the process via the port + const onMessage = onMessageEmitter.event; + this._register(onMessage(message => this.port.postMessage(message.buffer))); + + // Relay message from the port into the process + this.port.onmessage = (e => send(VSBuffer.wrap(e.data))); + this._register(toDisposable(() => this.port.onmessage = null)); + } + + private getEnv(): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { + ...deepClone(process.env), + VSCODE_AMD_ENTRYPOINT: this.configuration.process.moduleId, + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true', + VSCODE_PARENT_PID: String(process.pid) + }; + + // Sanitize environment + removeDangerousEnvVariables(env); + + return env; + } + + override dispose(): void { + super.dispose(); + + if (!this.child) { + return; + } + + this.child.kill(); + Logger.info(`Worker process with pid ${this.child?.pid} terminated normally (type: ${this.configuration.process.type}, window: ${this.configuration.reply.windowId}).`); + } +} + +/** + * Helper for logging messages from the worker. + */ +namespace Logger { + + export function error(message: string): void { + send({ id: SharedProcessWorkerMessages.Error, message }); + } + + export function warn(message: string): void { + send({ id: SharedProcessWorkerMessages.Warn, message }); + } + + export function info(message: string): void { + send({ id: SharedProcessWorkerMessages.Info, message }); + } + + export function trace(message: string): void { + send({ id: SharedProcessWorkerMessages.Trace, message }); + } +} + +/** + * Helper for typed `postMessage` usage. + */ +function send(message: IWorkerToSharedProcessMessage): void { + postMessage(message); +} diff --git a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService.ts b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService.ts new file mode 100644 index 0000000000..2f9d641148 --- /dev/null +++ b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ipcRenderer } from 'electron'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ILogService } from 'vs/platform/log/common/log'; +import { hash, IOnDidTerminateSharedProcessWorkerProcess, ISharedProcessWorkerConfiguration, ISharedProcessWorkerProcessExit, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; +import { SharedProcessWorkerMessages, ISharedProcessToWorkerMessage, IWorkerToSharedProcessMessage } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorker'; + +export class SharedProcessWorkerService implements ISharedProcessWorkerService { + + declare readonly _serviceBrand: undefined; + + private readonly workers = new Map>(); + + private readonly processeDisposables = new Map void>(); + private readonly processResolvers = new Map void>(); + + constructor( + @ILogService private readonly logService: ILogService + ) { + } + + async createWorker(configuration: ISharedProcessWorkerConfiguration): Promise { + const workerLogId = `window: ${configuration.reply.windowId}, moduleId: ${configuration.process.moduleId}`; + this.logService.trace(`SharedProcess: createWorker (${workerLogId})`); + + // Ensure to dispose any existing process for config + const configurationHash = hash(configuration); + if (this.processeDisposables.has(configurationHash)) { + this.logService.warn(`SharedProcess: createWorker found an existing worker that will be terminated (${workerLogId})`); + + this.doDisposeWorker(configuration); + } + + const cts = new CancellationTokenSource(); + + let worker: SharedProcessWebWorker | undefined = undefined; + let windowPort: MessagePort | undefined = undefined; + let workerPort: MessagePort | undefined = undefined; + + // Store as process for termination support + this.processeDisposables.set(configurationHash, (reason?: ISharedProcessWorkerProcessExit) => { + + // Signal to token + cts.dispose(true); + + // Terminate process + worker?.terminate(configuration, CancellationToken.None /* we want to deliver this message */); + + // Close ports + windowPort?.close(); + workerPort?.close(); + + // Remove from processes + this.processeDisposables.delete(configurationHash); + + // Release process resolvers if any + const processResolver = this.processResolvers.get(configurationHash); + if (processResolver) { + this.processResolvers.delete(configurationHash); + processResolver({ reason }); + } + }); + + // Acquire a worker for the configuration + worker = await this.getOrCreateWebWorker(configuration); + + // Keep a promise that will resolve in the future when the + // underlying process terminates. + const onDidTerminate = new Promise(resolve => { + this.processResolvers.set(configurationHash, resolve); + }); + + if (cts.token.isCancellationRequested) { + return onDidTerminate; + } + + // Create a `MessageChannel` with 2 ports: + // `windowPort`: send back to the requesting window + // `workerPort`: send into a new worker to use + const { port1, port2 } = new MessageChannel(); + windowPort = port1; + workerPort = port2; + + // Spawn in worker and pass over port + await worker.spawn(configuration, workerPort, cts.token); + + if (cts.token.isCancellationRequested) { + return onDidTerminate; + } + + // We cannot just send the `MessagePort` through our protocol back + // because the port can only be sent via `postMessage`. So we need + // to send it through the main process back to the window. + this.logService.trace(`SharedProcess: createWorker sending message port back to window (${workerLogId})`); + ipcRenderer.postMessage('vscode:relaySharedProcessWorkerMessageChannel', configuration, [windowPort]); + + return onDidTerminate; + } + + private getOrCreateWebWorker(configuration: ISharedProcessWorkerConfiguration): Promise { + + // keep 1 web-worker per process module id to reduce + // the overall number of web workers while still + // keeping workers for separate processes around. + let webWorkerPromise = this.workers.get(configuration.process.moduleId); + + // create a new web worker if this is the first time + // for the given process + if (!webWorkerPromise) { + this.logService.trace(`SharedProcess: creating new web worker (${configuration.process.moduleId})`); + + const sharedProcessWorker = new SharedProcessWebWorker(configuration.process.type, this.logService); + webWorkerPromise = sharedProcessWorker.init(); + + // Make sure to run through our normal `disposeWorker` call + // when the process terminates by itself. + sharedProcessWorker.onDidProcessSelfTerminate(({ configuration, reason }) => { + this.doDisposeWorker(configuration, reason); + }); + + this.workers.set(configuration.process.moduleId, webWorkerPromise); + } + + return webWorkerPromise; + } + + async disposeWorker(configuration: ISharedProcessWorkerConfiguration): Promise { + return this.doDisposeWorker(configuration); + } + + private doDisposeWorker(configuration: ISharedProcessWorkerConfiguration, reason?: ISharedProcessWorkerProcessExit): void { + const processDisposable = this.processeDisposables.get(hash(configuration)); + if (processDisposable) { + this.logService.trace(`SharedProcess: disposeWorker (window: ${configuration.reply.windowId}, moduleId: ${configuration.process.moduleId})`); + + processDisposable(reason); + } + } +} + +class SharedProcessWebWorker extends Disposable { + + private readonly _onDidProcessSelfTerminate = this._register(new Emitter<{ configuration: ISharedProcessWorkerConfiguration, reason: ISharedProcessWorkerProcessExit }>()); + readonly onDidProcessSelfTerminate = this._onDidProcessSelfTerminate.event; + + private readonly workerReady: Promise = this.doInit(); + private readonly mapMessageNonceToPendingMessageResolve = new Map void>(); + + constructor( + private readonly type: string, + private readonly logService: ILogService + ) { + super(); + } + + async init(): Promise { + await this.workerReady; + + return this; + } + + private doInit(): Promise { + let readyResolve: (result: Worker) => void; + const readyPromise = new Promise(resolve => readyResolve = resolve); + + const worker = new Worker('../../../base/worker/workerMain.js', { + name: `Shared Process Worker (${this.type})` + }); + + worker.onerror = event => { + this.logService.error(`SharedProcess: worker error (${this.type})`, event.message); + }; + + worker.onmessageerror = event => { + this.logService.error(`SharedProcess: worker message error (${this.type})`, event); + }; + + worker.onmessage = event => { + const { id, message, configuration, nonce } = event.data as IWorkerToSharedProcessMessage; + + switch (id) { + + // Lifecycle: Ready + case SharedProcessWorkerMessages.Ready: + readyResolve(worker); + break; + + // Lifecycle: Ack + case SharedProcessWorkerMessages.Ack: + if (nonce) { + const messageAwaiter = this.mapMessageNonceToPendingMessageResolve.get(nonce); + if (messageAwaiter) { + this.mapMessageNonceToPendingMessageResolve.delete(nonce); + messageAwaiter(); + } + } + break; + + // Lifecycle: self termination + case SharedProcessWorkerMessages.SelfTerminated: + if (configuration && message) { + this._onDidProcessSelfTerminate.fire({ configuration, reason: JSON.parse(message) }); + } + break; + + // Diagostics: trace + case SharedProcessWorkerMessages.Trace: + this.logService.trace(`SharedProcess (worker, ${this.type}):`, message); + break; + + // Diagostics: info + case SharedProcessWorkerMessages.Info: + if (message) { + this.logService.info(message); // take as is + } + break; + + // Diagostics: warn + case SharedProcessWorkerMessages.Warn: + this.logService.warn(`SharedProcess (worker, ${this.type}):`, message); + break; + + // Diagnostics: error + case SharedProcessWorkerMessages.Error: + this.logService.error(`SharedProcess (worker, ${this.type}):`, message); + break; + + // Any other message + default: + this.logService.warn(`SharedProcess: unexpected worker message (${this.type})`, event); + } + }; + + // First message triggers the load of the worker + worker.postMessage('vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain'); + + return readyPromise; + } + + private async send(message: ISharedProcessToWorkerMessage, token: CancellationToken, port?: MessagePort): Promise { + const worker = await this.workerReady; + + if (token.isCancellationRequested) { + return; + } + + return new Promise(resolve => { + + // Store the awaiter for resolving when message + // is received with the given nonce + const nonce = generateUuid(); + this.mapMessageNonceToPendingMessageResolve.set(nonce, resolve); + + // Post message into worker + const workerMessage: ISharedProcessToWorkerMessage = { ...message, nonce }; + if (port) { + worker.postMessage(workerMessage, [port]); + } else { + worker.postMessage(workerMessage); + } + + // Release on cancellation if still pending + token.onCancellationRequested(() => { + if (this.mapMessageNonceToPendingMessageResolve.delete(nonce)) { + resolve(); + } + }); + }); + } + + spawn(configuration: ISharedProcessWorkerConfiguration, port: MessagePort, token: CancellationToken): Promise { + const workerMessage: ISharedProcessToWorkerMessage = { + id: SharedProcessWorkerMessages.Spawn, + configuration, + environment: { + bootstrapPath: FileAccess.asFileUri('bootstrap-fork', require).fsPath + } + }; + + return this.send(workerMessage, token, port); + } + + terminate(configuration: ISharedProcessWorkerConfiguration, token: CancellationToken): Promise { + const workerMessage: ISharedProcessToWorkerMessage = { + id: SharedProcessWorkerMessages.Terminate, + configuration + }; + + return this.send(workerMessage, token); + } +} diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index c70dfd5d11..948d68feb3 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -6,7 +6,7 @@ import { BrowserWindow, Event as ElectronEvent, ipcMain, IpcMainEvent, MessagePortMain } from 'electron'; import { Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { assertIsDefined } from 'vs/base/common/types'; @@ -17,8 +17,10 @@ import { ILogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; import { ISharedProcess, ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { ISharedProcessWorkerConfiguration } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { WindowError } from 'vs/platform/windows/electron-main/windows'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; export class SharedProcess extends Disposable implements ISharedProcess { @@ -46,11 +48,14 @@ export class SharedProcess extends Disposable implements ISharedProcess { private registerListeners(): void { + // Shared process connections from workbench windows + ipcMain.on('vscode:createSharedProcessMessageChannel', (e, nonce: string) => this.onWindowConnection(e, nonce)); + + // Shared process worker relay + ipcMain.on('vscode:relaySharedProcessWorkerMessageChannel', (e, configuration: ISharedProcessWorkerConfiguration) => this.onWorkerConnection(e, configuration)); + // Lifecycle this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown())); - - // Shared process connections from workbench windows - ipcMain.on('vscode:createSharedProcessMessageChannel', async (e, nonce: string) => this.onWindowConnection(e, nonce)); } private async onWindowConnection(e: IpcMainEvent, nonce: string): Promise { @@ -81,6 +86,43 @@ export class SharedProcess extends Disposable implements ISharedProcess { e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]); } + private onWorkerConnection(e: IpcMainEvent, configuration: ISharedProcessWorkerConfiguration): void { + this.logService.trace('SharedProcess: onWorkerConnection', configuration); + + const disposables = new DisposableStore(); + + const disposeWorker = (reason: string) => { + if (!this.isAlive()) { + return; // the shared process is already gone, no need to dispose anything + } + + this.logService.trace(`SharedProcess: disposing worker (reason: '${reason}')`, configuration); + + // Only once! + disposables.dispose(); + + // Send this into the shared process who owns workers + this.send('vscode:electron-main->shared-process=disposeWorker', configuration); + }; + + // Ensure the sender is a valid target to send to + const receiverWindow = BrowserWindow.fromId(configuration.reply.windowId); + if (!receiverWindow || receiverWindow.isDestroyed() || receiverWindow.webContents.isDestroyed() || !configuration.reply.channel) { + disposeWorker('unavailable'); + + return; + } + + // Attach to lifecycle of receiver to manage worker lifecycle + disposables.add(Event.filter(this.lifecycleMainService.onWillLoadWindow, e => e.window.win === receiverWindow)(() => disposeWorker('load'))); + disposables.add(Event.fromNodeEventEmitter(receiverWindow, 'closed')(() => disposeWorker('closed'))); + + // The shared process window asks us to relay a `MessagePort` + // from a shared process worker to the target window. It needs + // to be send via `postMessage` to transfer the port. + receiverWindow.webContents.postMessage(configuration.reply.channel, configuration.reply.nonce, e.ports); + } + private onWillShutdown(): void { const window = this.window; if (!window) { @@ -88,9 +130,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { } // Signal exit to shared process when shutting down - if (!window.isDestroyed() && !window.webContents.isDestroyed()) { - window.webContents.send('vscode:electron-main->shared-process=exit'); - } + this.send('vscode:electron-main->shared-process=exit'); // Shut the shared process down when we are quitting // @@ -115,6 +155,19 @@ export class SharedProcess extends Disposable implements ISharedProcess { }, 0); } + private send(channel: string, ...args: any[]): void { + if (!this.isAlive()) { + this.logService.warn(`Sending IPC message to channel '${channel}' for shared process window that is destroyed`); + return; + } + + try { + this.window?.webContents.send(channel, ...args); + } catch (error) { + this.logService.warn(`Error sending IPC message to channel '${channel}' of shared process: ${toErrorMessage(error)}`); + } + } + private _whenReady: Promise | undefined = undefined; whenReady(): Promise { if (!this._whenReady) { @@ -168,6 +221,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { additionalArguments: [`--vscode-window-config=${configObjectUrl.resource.toString()}`], v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none', nodeIntegration: true, + nodeIntegrationInWorker: true, contextIsolation: false, enableWebSQL: false, spellcheck: false, @@ -254,4 +308,13 @@ export class SharedProcess extends Disposable implements ISharedProcess { isVisible(): boolean { return this.window?.isVisible() ?? false; } + + private isAlive(): boolean { + const window = this.window; + if (!window) { + return false; + } + + return !window.isDestroyed() && !window.webContents.isDestroyed(); + } } diff --git a/src/vs/platform/sign/browser/signService.ts b/src/vs/platform/sign/browser/signService.ts index 534f8b0678..27ed0f83f3 100644 --- a/src/vs/platform/sign/browser/signService.ts +++ b/src/vs/platform/sign/browser/signService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISignService } from 'vs/platform/sign/common/sign'; +import { IMessage, ISignService } from 'vs/platform/sign/common/sign'; export class SignService implements ISignService { @@ -15,6 +15,12 @@ export class SignService implements ISignService { this._tkn = token || null; } + async createNewMessage(value: string): Promise { + return { id: '', data: value }; + } + async validate(message: IMessage, value: string): Promise { + return true; + } async sign(value: string): Promise { return this._tkn || ''; } diff --git a/src/vs/platform/sign/common/sign.ts b/src/vs/platform/sign/common/sign.ts index 7e6f946862..df84794212 100644 --- a/src/vs/platform/sign/common/sign.ts +++ b/src/vs/platform/sign/common/sign.ts @@ -8,8 +8,15 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const SIGN_SERVICE_ID = 'signService'; export const ISignService = createDecorator(SIGN_SERVICE_ID); +export interface IMessage { + id: string; + data: string; +} + export interface ISignService { readonly _serviceBrand: undefined; + createNewMessage(value: string): Promise; + validate(message: IMessage, value: string): Promise; sign(value: string): Promise; } diff --git a/src/vs/platform/sign/node/signService.ts b/src/vs/platform/sign/node/signService.ts index 18659b1056..19fcd0a2e2 100644 --- a/src/vs/platform/sign/node/signService.ts +++ b/src/vs/platform/sign/node/signService.ts @@ -3,23 +3,68 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISignService } from 'vs/platform/sign/common/sign'; +import { IMessage, ISignService } from 'vs/platform/sign/common/sign'; declare module vsda { // the signer is a native module that for historical reasons uses a lower case class name // eslint-disable-next-line @typescript-eslint/naming-convention export class signer { - sign(arg: any): any; + sign(arg: string): string; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + export class validator { + createNewMessage(arg: string): string; + validate(arg: string): 'ok' | 'error'; } } export class SignService implements ISignService { declare readonly _serviceBrand: undefined; + private static _nextId = 1; + private readonly validators = new Map(); + private vsda(): Promise { return new Promise((resolve, reject) => require(['vsda'], resolve, reject)); } + async createNewMessage(value: string): Promise { + try { + const vsda = await this.vsda(); + const validator = new vsda.validator(); + if (validator) { + const id = String(SignService._nextId++); + this.validators.set(id, validator); + return { + id: id, + data: validator.createNewMessage(value) + }; + } + } catch (e) { + // ignore errors silently + } + return { id: '', data: value }; + } + + async validate(message: IMessage, value: string): Promise { + if (!message.id) { + return true; + } + + const validator = this.validators.get(message.id); + if (!validator) { + return false; + } + this.validators.delete(message.id); + try { + return (validator.validate(value) === 'ok'); + } catch (e) { + // ignore errors silently + return false; + } + } + async sign(value: string): Promise { try { const vsda = await this.vsda(); diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 2a6929fc84..bede61914f 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -5,9 +5,9 @@ import { Promises } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { Event } from 'vs/base/common/event'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { InMemoryStorageDatabase, IStorage, IStorageDatabase, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { InMemoryStorageDatabase, isStorageItemsChangeEvent, IStorage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractStorageService, IS_NEW_KEY, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; @@ -41,8 +41,8 @@ export class BrowserStorageService extends AbstractStorageService { // 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) + IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.WORKSPACE) }, this.logService), + IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.GLOBAL), broadcastChanges: true /* only for global storage */ }, this.logService) ]); // Workspace Storage @@ -163,16 +163,21 @@ class InMemoryIndexedDBStorageDatabase extends InMemoryStorageDatabase implement } } +interface IndexedDBStorageDatabaseOptions { + id: string; + broadcastChanges?: boolean; +} + export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBStorageDatabase { - static async create(id: string, logService: ILogService): Promise { + static async create(options: IndexedDBStorageDatabaseOptions, logService: ILogService): Promise { try { - const database = new IndexedDBStorageDatabase(id, logService); + const database = new IndexedDBStorageDatabase(options, logService); await database.whenConnected; return database; } catch (error) { - logService.error(`[IndexedDB Storage ${id}] create(): ${toErrorMessage(error, true)}`); + logService.error(`[IndexedDB Storage ${options.id}] create(): ${toErrorMessage(error, true)}`); return new InMemoryIndexedDBStorageDatabase(); } @@ -181,47 +186,114 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt 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 static readonly STORAGE_BROADCAST_CHANNEL = 'vscode.web.state.changes'; - private pendingUpdate: Promise | undefined = undefined; + private readonly _onDidChangeItemsExternal = this._register(new Emitter()); + readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; + + private broadcastChannel: BroadcastChannel | undefined; + + private pendingUpdate: Promise | undefined = undefined; get hasPendingUpdate(): boolean { return !!this.pendingUpdate; } private readonly name: string; private readonly whenConnected: Promise; private constructor( - id: string, + options: IndexedDBStorageDatabaseOptions, private readonly logService: ILogService ) { super(); - this.name = `${IndexedDBStorageDatabase.STORAGE_DATABASE_PREFIX}${id}`; + this.name = `${IndexedDBStorageDatabase.STORAGE_DATABASE_PREFIX}${options.id}`; + this.broadcastChannel = options.broadcastChanges && ('BroadcastChannel' in window) ? new BroadcastChannel(IndexedDBStorageDatabase.STORAGE_BROADCAST_CHANNEL) : undefined; + this.whenConnected = this.connect(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Check for global storage change events from other + // windows/tabs via `BroadcastChannel` mechanisms. + if (this.broadcastChannel) { + const listener = (event: MessageEvent) => { + if (isStorageItemsChangeEvent(event.data)) { + this._onDidChangeItemsExternal.fire(event.data); + } + }; + + this.broadcastChannel.addEventListener('message', listener); + this._register(toDisposable(() => { + this.broadcastChannel?.removeEventListener('message', listener); + this.broadcastChannel?.close(); + })); + } } private connect(): Promise { + return this.doConnect(true /* retry once on error */); + } + + private doConnect(retryOnError: boolean): Promise { return new Promise((resolve, reject) => { const request = window.indexedDB.open(this.name); - // Create `ItemTable` object-store when this DB is new + // Create `ItemTable` object-store in case this DB is new request.onupgradeneeded = () => { request.result.createObjectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE); }; // IndexedDB opened successfully - request.onsuccess = () => resolve(request.result); + request.onsuccess = () => { + const db = request.result; + + // It is still possible though that the object store + // we expect is not there (seen in Safari). As such, + // we validate the store is there and otherwise attempt + // once to re-create. + if (!db.objectStoreNames.contains(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE)) { + this.logService.error(`[IndexedDB Storage ${this.name}] onsuccess(): ${IndexedDBStorageDatabase.STORAGE_OBJECT_STORE} does not exist.`); + + if (retryOnError) { + this.logService.info(`[IndexedDB Storage ${this.name}] onsuccess(): Attempting to recreate the DB once.`); + + // Close any opened connections + db.close(); + + // Try to delete the db + const deleteRequest = window.indexedDB.deleteDatabase(this.name); + deleteRequest.onsuccess = () => this.doConnect(false /* do not retry anymore from here */).then(resolve, reject); + deleteRequest.onerror = () => { + this.logService.error(`[IndexedDB Storage ${this.name}] deleteDatabase(): ${deleteRequest.error}`); + + reject(deleteRequest.error); + }; + + return; + } + } + + return resolve(db); + }; // Fail on error (we will then fallback to in-memory DB) - request.onerror = () => reject(request.error); + request.onerror = () => { + this.logService.error(`[IndexedDB Storage ${this.name}] onerror(): ${request.error}`); + + reject(request.error); + }; }); } - getItems(): Promise> { - return new Promise>(async resolve => { + async getItems(): Promise> { + const db = await this.whenConnected; + + return new Promise>(resolve => { const items = new Map(); // 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(); @@ -258,29 +330,42 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt } async updateItems(request: IUpdateRequest): Promise { + + // Run the update + let didUpdate = false; this.pendingUpdate = this.doUpdateItems(request); try { - await this.pendingUpdate; + didUpdate = await this.pendingUpdate; } finally { this.pendingUpdate = undefined; } + + // Broadcast changes to other windows/tabs if enabled + // and only if we actually did update storage items. + if (this.broadcastChannel && didUpdate) { + const event: IStorageItemsChangeEvent = { + changed: request.insert, + deleted: request.delete + }; + + this.broadcastChannel.postMessage(event); + } } - private async doUpdateItems(request: IUpdateRequest): Promise { + 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; + return false; } // Update `ItemTable` with inserts and/or deletes - return new Promise(async (resolve, reject) => { - const db = await this.whenConnected; - + const db = await this.whenConnected; + return new Promise((resolve, reject) => { const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite'); - transaction.oncomplete = () => resolve(); + transaction.oncomplete = () => resolve(true); transaction.onerror = () => reject(transaction.error); const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE); @@ -311,10 +396,10 @@ export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBSt return db.close(); } - clear(): Promise { - return new Promise(async (resolve, reject) => { - const db = await this.whenConnected; + async clear(): Promise { + const db = await this.whenConnected; + return new Promise((resolve, reject) => { const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite'); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index c07f03ce8a..73df42fbec 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -6,6 +6,7 @@ import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { Disposable, dispose, MutableDisposable } from 'vs/base/common/lifecycle'; +import { mark } from 'vs/base/common/performance'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { InMemoryStorageDatabase, IStorage, Storage } from 'vs/base/parts/storage/common/storage'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -271,8 +272,14 @@ export abstract class AbstractStorageService extends Disposable implements IStor if (!this.initializationPromise) { this.initializationPromise = (async () => { - // Ask subclasses to initialize storage - await this.doInitialize(); + // Init all storage locations + mark('code/willInitStorage'); + try { + // Ask subclasses to initialize storage + await this.doInitialize(); + } finally { + mark('code/didInitStorage'); + } // On some OS we do not get enough time to persist state on shutdown (e.g. when // Windows restarts after applying updates). In other cases, VSCode might crash, diff --git a/src/vs/platform/storage/electron-sandbox/storageService.ts b/src/vs/platform/storage/electron-sandbox/storageService.ts index 292a8c44d0..3f1114347a 100644 --- a/src/vs/platform/storage/electron-sandbox/storageService.ts +++ b/src/vs/platform/storage/electron-sandbox/storageService.ts @@ -5,7 +5,6 @@ import { Promises } from 'vs/base/common/async'; import { MutableDisposable } from 'vs/base/common/lifecycle'; -import { mark } from 'vs/base/common/performance'; import { joinPath } from 'vs/base/common/resources'; import { IStorage, Storage } from 'vs/base/parts/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -67,17 +66,11 @@ export class NativeStorageService extends AbstractStorageService { } protected async doInitialize(): Promise { - // Init all storage locations - mark('code/willInitStorage'); - try { - await Promises.settled([ - this.globalStorage.init(), - this.workspaceStorage?.init() ?? Promise.resolve() - ]); - } finally { - mark('code/didInitStorage'); - } + await Promises.settled([ + this.globalStorage.init(), + this.workspaceStorage?.init() ?? Promise.resolve() + ]); } protected getStorage(scope: StorageScope): IStorage | undefined { diff --git a/src/vs/platform/storage/test/browser/storageService.test.ts b/src/vs/platform/storage/test/browser/storageService.test.ts index d6d11f9675..faae28ffbb 100644 --- a/src/vs/platform/storage/test/browser/storageService.test.ts +++ b/src/vs/platform/storage/test/browser/storageService.test.ts @@ -8,6 +8,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { Storage } from 'vs/base/parts/storage/common/storage'; import { flakySuite } from 'vs/base/test/common/testUtils'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { FileService } from 'vs/platform/files/common/fileService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -66,20 +67,22 @@ flakySuite('StorageService (browser specific)', () => { disposables.clear(); }); - 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); + test('clear', () => { + return runWithFakedTimers({ useFakeTimers: true }, 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); - await storageService.clear(); + 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); + 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); + } } - } + }); }); }); @@ -89,12 +92,12 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { const logService = new NullLogService(); teardown(async () => { - const storage = await IndexedDBStorageDatabase.create(id, logService); + const storage = await IndexedDBStorageDatabase.create({ id }, logService); await storage.clear(); }); test('Basics', async () => { - let storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); @@ -116,7 +119,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); @@ -139,7 +142,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); @@ -167,7 +170,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); @@ -180,7 +183,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { }); test('Clear', async () => { - let storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); @@ -190,13 +193,13 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - const db = await IndexedDBStorageDatabase.create(id, logService); + 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)); + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); @@ -209,7 +212,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { }); test('Inserts and Deletes at the same time', async () => { - let storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); @@ -219,7 +222,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); @@ -231,7 +234,7 @@ flakySuite('IndexDBStorageDatabase (browser)', () => { await storage.close(); - storage = new Storage(await IndexedDBStorageDatabase.create(id, logService)); + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); await storage.init(); diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 74ca8058ea..e7b9808e8c 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -43,13 +43,11 @@ export interface ITelemetryService { publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck): Promise; - setEnabled(value: boolean): void; - getTelemetryInfo(): Promise; setExperimentProperty(name: string, value: string): void; - isOptedIn: boolean; + telemetryLevel: TelemetryLevel; } export interface ITelemetryEndpoint { @@ -73,3 +71,22 @@ export const currentSessionDateStorageKey = 'telemetry.currentSessionDate'; export const firstSessionDateStorageKey = 'telemetry.firstSessionDate'; export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; export const machineIdKey = 'telemetry.machineId'; + +// Configuration Keys +export const TELEMETRY_SECTION_ID = 'telemetry'; +export const TELEMETRY_SETTING_ID = 'telemetry.telemetryLevel'; +export const TELEMETRY_OLD_SETTING_ID = 'telemetry.enableTelemetry'; + +export const enum TelemetryLevel { + NONE = 0, + CRASH = 1, + ERROR = 2, + USAGE = 3 +} + +export const enum TelemetryConfiguration { + OFF = 'off', + CRASH = 'crash', + ERROR = 'error', + ON = 'all' +} diff --git a/src/vs/platform/telemetry/common/telemetryIpc.ts b/src/vs/platform/telemetry/common/telemetryIpc.ts index 60c414ac9a..8260b39af5 100644 --- a/src/vs/platform/telemetry/common/telemetryIpc.ts +++ b/src/vs/platform/telemetry/common/telemetryIpc.ts @@ -14,14 +14,14 @@ export interface ITelemetryLog { export class TelemetryAppenderChannel implements IServerChannel { - constructor(private appender: ITelemetryAppender) { } + constructor(private appenders: ITelemetryAppender[]) { } listen(_: unknown, event: string): Event { throw new Error(`Event not found: ${event}`); } call(_: unknown, command: string, { eventName, data }: ITelemetryLog): Promise { - this.appender.log(eventName, data); + this.appenders.forEach(a => a.log(eventName, data)); return Promise.resolve(null); } } diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 08037e726f..ac08e4799b 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -5,19 +5,19 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { cloneAndChange, mixin } from 'vs/base/common/objects'; +import { isWeb } from 'vs/base/common/platform'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; import product from 'vs/platform/product/common/product'; import { Registry } from 'vs/platform/registry/common/platform'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; -import { ITelemetryData, ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryData, ITelemetryInfo, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; +import { getTelemetryLevel, ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; export interface ITelemetryServiceConfig { - appender: ITelemetryAppender; + appenders: ITelemetryAppender[]; sendErrorTelemetry?: boolean; commonProperties?: Promise<{ [name: string]: any }>; piiPaths?: string[]; @@ -30,12 +30,11 @@ export class TelemetryService implements ITelemetryService { declare readonly _serviceBrand: undefined; - private _appender: ITelemetryAppender; + private _appenders: ITelemetryAppender[]; private _commonProperties: Promise<{ [name: string]: any; }>; private _experimentProperties: { [name: string]: string } = {}; private _piiPaths: string[]; - private _userOptIn: boolean; - private _enabled: boolean; + private _telemetryLevel: TelemetryLevel; public readonly sendErrorTelemetry: boolean; private readonly _disposables = new DisposableStore(); @@ -43,13 +42,12 @@ export class TelemetryService implements ITelemetryService { constructor( config: ITelemetryServiceConfig, - @optional(IConfigurationService) private _configurationService: IConfigurationService + @IConfigurationService private _configurationService: IConfigurationService ) { - this._appender = config.appender; + this._appenders = config.appenders; this._commonProperties = config.commonProperties || Promise.resolve({}); this._piiPaths = config.piiPaths || []; - this._userOptIn = true; - this._enabled = true; + this._telemetryLevel = TelemetryLevel.USAGE; this.sendErrorTelemetry = !!config.sendErrorTelemetry; // static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information` @@ -59,43 +57,37 @@ export class TelemetryService implements ITelemetryService { this._cleanupPatterns.push(new RegExp(escapeRegExpCharacters(piiPath), 'gi')); } - if (this._configurationService) { - this._updateUserOptIn(); - this._configurationService.onDidChangeConfiguration(this._updateUserOptIn, this, this._disposables); - type OptInClassification = { - optIn: { classification: 'SystemMetaData', purpose: 'BusinessInsight', isMeasurement: true }; - }; - type OptInEvent = { - optIn: boolean; - }; - this.publicLog2('optInStatus', { optIn: this._userOptIn }); - this._commonProperties.then(values => { - const isHashedId = /^[a-f0-9]+$/i.test(values['common.machineId']); + this._updateTelemetryLevel(); + this._configurationService.onDidChangeConfiguration(this._updateTelemetryLevel, this, this._disposables); + type OptInClassification = { + optIn: { classification: 'SystemMetaData', purpose: 'BusinessInsight', isMeasurement: true }; + }; + type OptInEvent = { + optIn: boolean; + }; + this.publicLog2('optInStatus', { optIn: this._telemetryLevel === TelemetryLevel.USAGE }); - type MachineIdFallbackClassification = { - usingFallbackGuid: { classification: 'SystemMetaData', purpose: 'BusinessInsight', isMeasurement: true }; - }; - this.publicLog2<{ usingFallbackGuid: boolean }, MachineIdFallbackClassification>('machineIdFallback', { usingFallbackGuid: !isHashedId }); - }); - } + this._commonProperties.then(values => { + const isHashedId = /^[a-f0-9]+$/i.test(values['common.machineId']); + + type MachineIdFallbackClassification = { + usingFallbackGuid: { classification: 'SystemMetaData', purpose: 'BusinessInsight', isMeasurement: true }; + }; + this.publicLog2<{ usingFallbackGuid: boolean }, MachineIdFallbackClassification>('machineIdFallback', { usingFallbackGuid: !isHashedId }); + }); } setExperimentProperty(name: string, value: string): void { this._experimentProperties[name] = value; } - setEnabled(value: boolean): void { - this._enabled = value; + private _updateTelemetryLevel(): void { + this._telemetryLevel = getTelemetryLevel(this._configurationService); } - private _updateUserOptIn(): void { - const config = this._configurationService?.getValue(TELEMETRY_SECTION_ID); - this._userOptIn = config ? config.enableTelemetry : this._userOptIn; - } - - get isOptedIn(): boolean { - return this._userOptIn && this._enabled; + get telemetryLevel(): TelemetryLevel { + return this._telemetryLevel; } async getTelemetryInfo(): Promise { @@ -115,9 +107,9 @@ export class TelemetryService implements ITelemetryService { this._disposables.dispose(); } - publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { + private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { // don't send events when the user is optout - if (!this.isOptedIn) { + if (this.telemetryLevel < eventLevel) { return Promise.resolve(undefined); } @@ -137,7 +129,8 @@ export class TelemetryService implements ITelemetryService { return undefined; }); - this._appender.log(eventName, data); + // Log to the appenders of sufficient level + this._appenders.forEach(a => a.log(eventName, data)); }, err => { // unsure what to do now... @@ -145,6 +138,10 @@ export class TelemetryService implements ITelemetryService { }); } + publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { + return this._log(eventName, TelemetryLevel.USAGE, data, anonymizeFilePaths); + } + publicLog2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean): Promise { return this.publicLog(eventName, data as ITelemetryData, anonymizeFilePaths); } @@ -155,60 +152,130 @@ export class TelemetryService implements ITelemetryService { } // Send error event and anonymize paths - return this.publicLog(errorEventName, data, true); + return this._log(errorEventName, TelemetryLevel.ERROR, data, true); } publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck): Promise { return this.publicLogError(eventName, data as ITelemetryData); } - private _cleanupInfo(stack: string, anonymizeFilePaths?: boolean): string { + private _anonymizeFilePaths(stack: string): string { let updatedStack = stack; - if (anonymizeFilePaths) { - const cleanUpIndexes: [number, number][] = []; - for (let regexp of this._cleanupPatterns) { - while (true) { - const result = regexp.exec(stack); - if (!result) { - break; - } - cleanUpIndexes.push([result.index, regexp.lastIndex]); - } - } - - const nodeModulesRegex = /^[\\\/]?(node_modules|node_modules\.asar)[\\\/]/; - const fileRegex = /(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w-\._]+(\\\\|\\|\/))+[\w-\._]*/g; - let lastIndex = 0; - updatedStack = ''; - + const cleanUpIndexes: [number, number][] = []; + for (let regexp of this._cleanupPatterns) { while (true) { - const result = fileRegex.exec(stack); + const result = regexp.exec(stack); if (!result) { break; } - // Anoynimize user file paths that do not need to be retained or cleaned up. - if (!nodeModulesRegex.test(result[0]) && cleanUpIndexes.every(([x, y]) => result.index < x || result.index >= y)) { - updatedStack += stack.substring(lastIndex, result.index) + ''; - lastIndex = fileRegex.lastIndex; - } + cleanUpIndexes.push([result.index, regexp.lastIndex]); } - if (lastIndex < stack.length) { - updatedStack += stack.substr(lastIndex); + } + + const nodeModulesRegex = /^[\\\/]?(node_modules|node_modules\.asar)[\\\/]/; + const fileRegex = /(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w-\._]+(\\\\|\\|\/))+[\w-\._]*/g; + let lastIndex = 0; + updatedStack = ''; + + while (true) { + const result = fileRegex.exec(stack); + if (!result) { + break; } + // Anoynimize user file paths that do not need to be retained or cleaned up. + if (!nodeModulesRegex.test(result[0]) && cleanUpIndexes.every(([x, y]) => result.index < x || result.index >= y)) { + updatedStack += stack.substring(lastIndex, result.index) + ''; + lastIndex = fileRegex.lastIndex; + } + } + if (lastIndex < stack.length) { + updatedStack += stack.substr(lastIndex); + } + + return updatedStack; + } + + private _removePropertiesWithPossibleUserInfo(property: string): string { + // If for some reason it is undefined we skip it (this shouldn't be possible); + if (!property) { + return property; + } + + const value = property.toLowerCase(); + + // Regex which matches @*.site + const emailRegex = /@[a-zA-Z0-9-.]+/; + const secretRegex = /\S*(key|token|sig|password|passwd|pwd)[="':\s]+\S*/; + + // Check for common user data in the telemetry events + if (secretRegex.test(value)) { + return ''; + } else if (emailRegex.test(value)) { + return ''; + } + + return property; + } + + + private _cleanupInfo(property: string, anonymizeFilePaths?: boolean): string { + let updatedProperty = property; + + // anonymize file paths + if (anonymizeFilePaths) { + updatedProperty = this._anonymizeFilePaths(updatedProperty); } // sanitize with configured cleanup patterns for (let regexp of this._cleanupPatterns) { - updatedStack = updatedStack.replace(regexp, ''); + updatedProperty = updatedProperty.replace(regexp, ''); } - return updatedStack; + + // remove possible user info + updatedProperty = this._removePropertiesWithPossibleUserInfo(updatedProperty); + + return updatedProperty; } } +function getTelemetryLevelSettingDescription(): string { + const telemetryText = localize('telemetry.telemetryLevelMd', "Controls all core and first party extension telemetry. This helps us to better understand how {0} is performing, where improvements need to be made, and how features are being used.", product.nameLong); + const externalLinksStatement = !product.privacyStatementUrl ? + localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') : + localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl); + const restartString = !isWeb ? localize('telemetry.restart', 'A full restart of the application is necessary for crash reporting changes to take effect.') : ''; -const TELEMETRY_SECTION_ID = 'telemetry'; + const crashReportsHeader = localize('telemetry.crashReports', "Crash Reports"); + const errorsHeader = localize('telemetry.errors', "Error Telemetry"); + const usageHeader = localize('telemetry.usage', "Usage Data"); + const telemetryTableDescription = localize('telemetry.telemetryLevel.tableDescription', "The following table outlines the data sent with each setting:"); + const telemetryTable = ` +| | ${crashReportsHeader} | ${errorsHeader} | ${usageHeader} | +|:------|:---------------------:|:---------------:|:--------------:| +| all | ✓ | ✓ | ✓ | +| error | ✓ | ✓ | - | +| crash | ✓ | - | - | +| off | - | - | - | +`; + + const deprecatedSettingNote = localize('telemetry.telemetryLevel.deprecated', "****Note:*** If this setting is 'off', no telemetry will be sent regardless of other telemetry settings. If this setting is set to anything except 'off' and telemetry is disabled with deprecated settings, no telemetry will be sent.*"); + const telemetryDescription = ` +${telemetryText} ${externalLinksStatement} ${restartString} + +  + +${telemetryTableDescription} +${telemetryTable} + +  + +${deprecatedSettingNote} +`; + + return telemetryDescription; +} Registry.as(Extensions.Configuration).registerConfiguration({ 'id': TELEMETRY_SECTION_ID, @@ -216,7 +283,32 @@ Registry.as(Extensions.Configuration).registerConfigurat 'type': 'object', 'title': localize('telemetryConfigurationTitle', "Telemetry"), 'properties': { - 'telemetry.enableTelemetry': { + [TELEMETRY_SETTING_ID]: { + 'type': 'string', + 'enum': [TelemetryConfiguration.ON, TelemetryConfiguration.ERROR, TelemetryConfiguration.CRASH, TelemetryConfiguration.OFF], + 'enumDescriptions': [ + localize('telemetry.telemetryLevel.default', "Sends usage data, errors, and crash reports."), + localize('telemetry.telemetryLevel.error', "Sends general error telemetry and crash reports."), + localize('telemetry.telemetryLevel.crash', "Sends OS level crash reports."), + localize('telemetry.telemetryLevel.off', "Disables all product telemetry.") + ], + 'markdownDescription': getTelemetryLevelSettingDescription(), + 'default': TelemetryConfiguration.ON, + 'restricted': true, + 'scope': ConfigurationScope.APPLICATION, + 'tags': ['usesOnlineServices', 'telemetry'] + } + } +}); + +// Deprecated telemetry setting +Registry.as(Extensions.Configuration).registerConfiguration({ + 'id': TELEMETRY_SECTION_ID, + 'order': 110, + 'type': 'object', + 'title': localize('telemetryConfigurationTitle', "Telemetry"), + 'properties': { + [TELEMETRY_OLD_SETTING_ID]: { 'type': 'boolean', 'markdownDescription': !product.privacyStatementUrl ? @@ -224,8 +316,10 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('telemetry.enableTelemetryMd', "Enable diagnostic data to be collected. This helps us to better understand how {0} is performing and where improvements need to be made. [Read more]({1}) about what we collect and our privacy statement.", product.nameLong, product.privacyStatementUrl), 'default': true, 'restricted': true, + 'markdownDeprecationMessage': localize('enableTelemetryDeprecated', "If this setting is false, no telemetry will be sent regardless of the new setting's value. Deprecated in favor of the {0} setting.", `\`#${TELEMETRY_SETTING_ID}#\``), 'scope': ConfigurationScope.APPLICATION, 'tags': ['usesOnlineServices', 'telemetry'] } } }); + diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 140f23f132..afe4549f87 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -3,15 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Promises } from 'vs/base/common/async'; import { IDisposable } from 'vs/base/common/lifecycle'; import { safeStringify } from 'vs/base/common/objects'; import { isObject } from 'vs/base/common/types'; import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IProductService } from 'vs/platform/product/common/productService'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; -import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryInfo, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; -export const NullTelemetryService = new class implements ITelemetryService { +export class NullTelemetryServiceShape implements ITelemetryService { declare readonly _serviceBrand: undefined; readonly sendErrorTelemetry = false; @@ -29,8 +30,7 @@ export const NullTelemetryService = new class implements ITelemetryService { } setExperimentProperty() { } - setEnabled() { } - isOptedIn = true; + telemetryLevel = TelemetryLevel.NONE; getTelemetryInfo(): Promise { return Promise.resolve({ instanceId: 'someValue.instanceId', @@ -39,7 +39,9 @@ export const NullTelemetryService = new class implements ITelemetryService { firstSessionDate: 'someValue.firstSessionDate' }); } -}; +} + +export const NullTelemetryService = new NullTelemetryServiceShape(); export class NullEndpointTelemetryService implements ICustomEndpointTelemetryService { _serviceBrand: undefined; @@ -58,13 +60,6 @@ export interface ITelemetryAppender { flush(): Promise; } -export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetryAppender { - return { - log: (e, d) => appenders.forEach(a => a.log(e, d)), - flush: () => Promises.settled(appenders.map(a => a.flush())), - }; -} - export const NullAppender: ITelemetryAppender = { log: () => null, flush: () => Promise.resolve(null) }; @@ -102,6 +97,49 @@ export function configurationTelemetry(telemetryService: ITelemetryService, conf }); } +/** + * Determines how telemetry is handled based on the current running configuration. + * To log telemetry locally, the client must not disable telemetry via the CLI + * If client is a built product and telemetry is enabled via the product.json, telemetry is supported + * This function is only used to determine if telemetry contructs should occur, but is not impacted by user configuration + * + * @param productService + * @param environmentService + * @returns false - telemetry is completely disabled, true - telemetry is logged locally, but may not be sent + */ +export function supportsTelemetry(productService: IProductService, environmentService: IEnvironmentService): boolean { + return !(environmentService.disableTelemetry || !productService.enableTelemetry); +} + +/** + * Determines how telemetry is handled based on the user's configuration. + * + * @param configurationService + * @returns OFF, ERROR, ON + */ +export function getTelemetryLevel(configurationService: IConfigurationService): TelemetryLevel { + const newConfig = configurationService.getValue(TELEMETRY_SETTING_ID); + const crashReporterConfig = configurationService.getValue('telemetry.enableCrashReporter'); + const oldConfig = configurationService.getValue(TELEMETRY_OLD_SETTING_ID); + + // If `telemetry.enableCrashReporter` is false or `telemetry.enableTelemetry' is false, disable telemetry + if (oldConfig === false || crashReporterConfig === false) { + return TelemetryLevel.NONE; + } + + // Maps new telemetry setting to a telemetry level + switch (newConfig ?? TelemetryConfiguration.ON) { + case TelemetryConfiguration.ON: + return TelemetryLevel.USAGE; + case TelemetryConfiguration.ERROR: + return TelemetryLevel.ERROR; + case TelemetryConfiguration.CRASH: + return TelemetryLevel.CRASH; + case TelemetryConfiguration.OFF: + return TelemetryLevel.NONE; + } +} + export interface Properties { [key: string]: string; } diff --git a/src/vs/platform/telemetry/node/appInsightsAppender.ts b/src/vs/platform/telemetry/node/appInsightsAppender.ts index 14a1e987c0..e1c7ea0af2 100644 --- a/src/vs/platform/telemetry/node/appInsightsAppender.ts +++ b/src/vs/platform/telemetry/node/appInsightsAppender.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as appInsights from 'applicationinsights'; +import type { TelemetryClient } from 'applicationinsights'; import { onUnexpectedError } from 'vs/base/common/errors'; import { mixin } from 'vs/base/common/objects'; import { ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils'; -async function getClient(aiKey: string): Promise { +async function getClient(aiKey: string): Promise { const appInsights = await import('applicationinsights'); - let client: appInsights.TelemetryClient; + let client: TelemetryClient; if (appInsights.defaultClient) { client = new appInsights.TelemetryClient(aiKey); client.channel.setUseDiskRetryCaching(true); @@ -37,13 +37,13 @@ async function getClient(aiKey: string): Promise { export class AppInsightsAppender implements ITelemetryAppender { - private _aiClient: string | appInsights.TelemetryClient | undefined; - private _asyncAIClient: Promise | null; + private _aiClient: string | TelemetryClient | undefined; + private _asyncAIClient: Promise | null; constructor( private _eventPrefix: string, private _defaultData: { [key: string]: any } | null, - aiKeyOrClientFactory: string | (() => appInsights.TelemetryClient), // allow factory function for testing + aiKeyOrClientFactory: string | (() => TelemetryClient), // allow factory function for testing ) { if (!this._defaultData) { this._defaultData = Object.create(null); @@ -57,7 +57,7 @@ export class AppInsightsAppender implements ITelemetryAppender { this._asyncAIClient = null; } - private _withAIClient(callback: (aiClient: appInsights.TelemetryClient) => void): void { + private _withAIClient(callback: (aiClient: TelemetryClient) => void): void { if (!this._aiClient) { return; } diff --git a/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts b/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts index e989f72dca..476c78ecb9 100644 --- a/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts +++ b/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts @@ -12,8 +12,6 @@ import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, IT import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { combinedAppender } from 'vs/platform/telemetry/common/telemetryUtils'; - export class CustomEndpointTelemetryService implements ICustomEndpointTelemetryService { declare readonly _serviceBrand: undefined; @@ -48,13 +46,13 @@ export class CustomEndpointTelemetryService implements ICustomEndpointTelemetryS ); const channel = client.getChannel('telemetryAppender'); - const appender = combinedAppender( + const appenders = [ new TelemetryAppenderClient(channel), new TelemetryLogAppender(this.loggerService, this.environmentService, `[${endpoint.id}] `), - ); + ]; this.customTelemetryServices.set(endpoint.id, new TelemetryService({ - appender, + appenders, sendErrorTelemetry: endpoint.sendErrorTelemetry }, this.configurationService)); } diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 14fe780351..2a17358874 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -7,10 +7,10 @@ import * as sinon from 'sinon'; import * as sinonTest from 'sinon-test'; import * as Errors from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; -import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { ITelemetryData, TelemetryConfiguration, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -90,10 +90,10 @@ suite('TelemetryService', () => { test('Disposing', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined!); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); return service.publicLog('testPrivateEvent').then(() => { - assert.strictEqual(testAppender.getEventsCount(), 1); + assert.strictEqual(testAppender.getEventsCount(), 3); service.dispose(); assert.strictEqual(!testAppender.isDisposed, true); @@ -103,12 +103,14 @@ suite('TelemetryService', () => { // event reporting test('Simple event', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined!); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); return service.publicLog('testEvent').then(_ => { - assert.strictEqual(testAppender.getEventsCount(), 1); - assert.strictEqual(testAppender.events[0].eventName, 'testEvent'); - assert.notStrictEqual(testAppender.events[0].data, null); + assert.strictEqual(testAppender.getEventsCount(), 3); + assert.strictEqual(testAppender.events[0].eventName, 'optInStatus'); + assert.strictEqual(testAppender.events[1].eventName, 'testEvent'); + assert.notStrictEqual(testAppender.events[1].data, null); + assert.strictEqual(testAppender.events[2].eventName, 'machineIdFallback'); service.dispose(); }); @@ -116,7 +118,7 @@ suite('TelemetryService', () => { test('Event with data', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined!); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); return service.publicLog('testEvent', { 'stringProp': 'property', @@ -126,13 +128,14 @@ suite('TelemetryService', () => { 'value': 0 } }).then(() => { - assert.strictEqual(testAppender.getEventsCount(), 1); - assert.strictEqual(testAppender.events[0].eventName, 'testEvent'); - assert.notStrictEqual(testAppender.events[0].data, null); - assert.strictEqual(testAppender.events[0].data['stringProp'], 'property'); - assert.strictEqual(testAppender.events[0].data['numberProp'], 1); - assert.strictEqual(testAppender.events[0].data['booleanProp'], true); - assert.strictEqual(testAppender.events[0].data['complexProp'].value, 0); + assert.strictEqual(testAppender.getEventsCount(), 3); + assert.strictEqual(testAppender.events[0].eventName, 'optInStatus'); + assert.strictEqual(testAppender.events[1].eventName, 'testEvent'); + assert.notStrictEqual(testAppender.events[1].data, null); + assert.strictEqual(testAppender.events[1].data['stringProp'], 'property'); + assert.strictEqual(testAppender.events[1].data['numberProp'], 1); + assert.strictEqual(testAppender.events[1].data['booleanProp'], true); + assert.strictEqual(testAppender.events[1].data['complexProp'].value, 0); service.dispose(); }); @@ -142,16 +145,16 @@ suite('TelemetryService', () => { test('common properties added to *all* events, simple event', function () { let testAppender = new TestTelemetryAppender(); let service = new TelemetryService({ - appender: testAppender, + appenders: [testAppender], commonProperties: Promise.resolve({ foo: 'JA!', get bar() { return Math.random(); } }) - }, undefined!); + }, new TestConfigurationService()); return service.publicLog('testEvent').then(_ => { - let [first] = testAppender.events; + let [, second] = testAppender.events; // first is optInStatus-event - assert.strictEqual(Object.keys(first.data).length, 2); - assert.strictEqual(typeof first.data['foo'], 'string'); - assert.strictEqual(typeof first.data['bar'], 'number'); + assert.strictEqual(Object.keys(second.data).length, 2); + assert.strictEqual(typeof second.data['foo'], 'string'); + assert.strictEqual(typeof second.data['bar'], 'number'); service.dispose(); }); @@ -160,18 +163,18 @@ suite('TelemetryService', () => { test('common properties added to *all* events, event with data', function () { let testAppender = new TestTelemetryAppender(); let service = new TelemetryService({ - appender: testAppender, + appenders: [testAppender], commonProperties: Promise.resolve({ foo: 'JA!', get bar() { return Math.random(); } }) - }, undefined!); + }, new TestConfigurationService()); return service.publicLog('testEvent', { hightower: 'xl', price: 8000 }).then(_ => { - let [first] = testAppender.events; + let [, second] = testAppender.events; // first is optInStatus-event - assert.strictEqual(Object.keys(first.data).length, 4); - assert.strictEqual(typeof first.data['foo'], 'string'); - assert.strictEqual(typeof first.data['bar'], 'number'); - assert.strictEqual(typeof first.data['hightower'], 'string'); - assert.strictEqual(typeof first.data['price'], 'number'); + assert.strictEqual(Object.keys(second.data).length, 4); + assert.strictEqual(typeof second.data['foo'], 'string'); + assert.strictEqual(typeof second.data['bar'], 'number'); + assert.strictEqual(typeof second.data['hightower'], 'string'); + assert.strictEqual(typeof second.data['price'], 'number'); service.dispose(); }); @@ -179,13 +182,13 @@ suite('TelemetryService', () => { test('TelemetryInfo comes from properties', function () { let service = new TelemetryService({ - appender: NullAppender, + appenders: [NullAppender], commonProperties: Promise.resolve({ sessionID: 'one', ['common.instanceId']: 'two', ['common.machineId']: 'three', }) - }, undefined!); + }, new TestConfigurationService()); return service.getTelemetryInfo().then(info => { assert.strictEqual(info.sessionId, 'one'); @@ -196,13 +199,14 @@ suite('TelemetryService', () => { }); }); - test('enableTelemetry on by default', sinonTestFn(function () { + test('telemetry on by default', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined!); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); return service.publicLog('testEvent').then(() => { - assert.strictEqual(testAppender.getEventsCount(), 1); - assert.strictEqual(testAppender.events[0].eventName, 'testEvent'); + assert.strictEqual(testAppender.getEventsCount(), 3); + assert.strictEqual(testAppender.events[0].eventName, 'optInStatus'); + assert.strictEqual(testAppender.events[1].eventName, 'testEvent'); service.dispose(); }); @@ -210,10 +214,12 @@ suite('TelemetryService', () => { class JoinableTelemetryService extends TelemetryService { - private readonly promises: Promise[] = []; + private promises: Promise[] = []; - constructor(config: ITelemetryServiceConfig, configurationService: IConfigurationService) { - super({ ...config, sendErrorTelemetry: true }, configurationService); + constructor(config: ITelemetryServiceConfig) { + super({ ...config, sendErrorTelemetry: true }, new TestConfigurationService); + this.promises = this.promises ?? []; + this.promises = this.promises ?? []; } join(): Promise { @@ -222,9 +228,23 @@ suite('TelemetryService', () => { override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { let p = super.publicLog(eventName, data, anonymizeFilePaths); + // publicLog is called from the ctor and therefore promises can be undefined + this.promises = this.promises ?? []; this.promises.push(p); return p; } + + override publicLogError(errorEventName: string, data?: ITelemetryData): Promise { + let p = super.publicLogError(errorEventName, data); + // publicLogError is called from the ctor and therefore promises can be undefined + this.promises = this.promises ?? []; + this.promises.push(p); + return p; + } + + override publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck): Promise { + return this.publicLogError(eventName, data as ITelemetryData); + } } test.skip('Error events', sinonTestFn(async function (this: any) { // {{SQL CARBON EDIT}} skip test @@ -234,7 +254,7 @@ suite('TelemetryService', () => { try { let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); @@ -248,9 +268,10 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); await service.join(); - assert.strictEqual(testAppender.getEventsCount(), 1); - assert.strictEqual(testAppender.events[0].eventName, 'UnhandledError'); - assert.strictEqual(testAppender.events[0].data.msg, 'This is a test.'); + assert.strictEqual(testAppender.getEventsCount(), 3); + assert.strictEqual(testAppender.events[0].eventName, 'optInStatus'); + assert.strictEqual(testAppender.events[1].eventName, 'UnhandledError'); + assert.strictEqual(testAppender.events[1].data.msg, 'This is a test.'); errorTelemetry.dispose(); service.dispose(); @@ -293,7 +314,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let testError = new Error('test'); @@ -304,13 +325,14 @@ suite('TelemetryService', () => { assert.strictEqual(errorStub.alwaysCalledWithExactly('Error Message', 'file.js', 2, 42, testError), true); assert.strictEqual(errorStub.callCount, 1); - assert.strictEqual(testAppender.getEventsCount(), 1); - assert.strictEqual(testAppender.events[0].eventName, 'UnhandledError'); - assert.strictEqual(testAppender.events[0].data.msg, 'Error Message'); - assert.strictEqual(testAppender.events[0].data.file, 'file.js'); - assert.strictEqual(testAppender.events[0].data.line, 2); - assert.strictEqual(testAppender.events[0].data.column, 42); - assert.strictEqual(testAppender.events[0].data.uncaught_error_msg, 'test'); + assert.strictEqual(testAppender.getEventsCount(), 3); + assert.strictEqual(testAppender.events[0].eventName, 'optInStatus'); + assert.strictEqual(testAppender.events[1].eventName, 'UnhandledError'); + assert.strictEqual(testAppender.events[1].data.msg, 'Error Message'); + assert.strictEqual(testAppender.events[1].data.file, 'file.js'); + assert.strictEqual(testAppender.events[1].data.line, 2); + assert.strictEqual(testAppender.events[1].data.column, 42); + assert.strictEqual(testAppender.events[1].data.uncaught_error_msg, 'test'); errorTelemetry.dispose(); service.dispose(); @@ -321,7 +343,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let personInfoWithSpaces = settings.personalInfo.slice(0, 2) + ' ' + settings.personalInfo.slice(2); @@ -332,8 +354,8 @@ suite('TelemetryService', () => { await service.join(); assert.strictEqual(errorStub.callCount, 1); - assert.strictEqual(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo.replace(settings.personalInfo, personInfoWithSpaces)), -1); - assert.strictEqual(testAppender.events[0].data.file, settings.importantInfo + '/test.js'); + assert.strictEqual(testAppender.events[1].data.file.indexOf(settings.dangerousPathWithImportantInfo.replace(settings.personalInfo, personInfoWithSpaces)), -1); + assert.strictEqual(testAppender.events[1].data.file, settings.importantInfo + '/test.js'); errorTelemetry.dispose(); service.dispose(); @@ -345,7 +367,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousFilenameError: any = new Error('dangerousFilename'); @@ -354,7 +376,7 @@ suite('TelemetryService', () => { clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); return service.join().then(() => { assert.strictEqual(errorStub.callCount, 1); - assert.strictEqual(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); + assert.strictEqual(testAppender.events[1].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); dangerousFilenameError = new Error('dangerousFilename'); dangerousFilenameError.stack = settings.stack; @@ -363,8 +385,8 @@ suite('TelemetryService', () => { return service.join(); }).then(() => { assert.strictEqual(errorStub.callCount, 2); - assert.strictEqual(testAppender.events[0].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); - assert.strictEqual(testAppender.events[0].data.file, settings.importantInfo + '/test.js'); + assert.strictEqual(testAppender.events[1].data.file.indexOf(settings.dangerousPathWithImportantInfo), -1); + assert.strictEqual(testAppender.events[1].data.file, settings.importantInfo + '/test.js'); errorTelemetry.dispose(); service.dispose(); @@ -377,7 +399,7 @@ suite('TelemetryService', () => { try { let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousPathWithoutImportantInfoError: any = new Error(settings.dangerousPathWithoutImportantInfo); @@ -386,13 +408,13 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); await service.join(); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -407,7 +429,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousPathWithoutImportantInfoError: any = new Error('dangerousPathWithoutImportantInfo'); @@ -418,12 +440,12 @@ suite('TelemetryService', () => { assert.strictEqual(errorStub.callCount, 1); // Test that no file information remains, esp. personal info - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -437,7 +459,7 @@ suite('TelemetryService', () => { try { let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); @@ -448,14 +470,14 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); await service.join(); - assert.notStrictEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.notStrictEqual(testAppender.events[1].data.msg.indexOf(settings.importantInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.importantInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -470,7 +492,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); @@ -481,14 +503,14 @@ suite('TelemetryService', () => { assert.strictEqual(errorStub.callCount, 1); // Test that important information remains but personal info does not - assert.notStrictEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.notStrictEqual(testAppender.events[1].data.msg.indexOf(settings.importantInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.importantInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -502,7 +524,7 @@ suite('TelemetryService', () => { try { let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); @@ -513,10 +535,10 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); await service.join(); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); errorTelemetry.dispose(); service.dispose(); @@ -531,7 +553,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); @@ -542,10 +564,10 @@ suite('TelemetryService', () => { assert.strictEqual(errorStub.callCount, 1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf('(' + settings.nodeModuleAsarPathToRetain), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf('(' + settings.nodeModulePathToRetain), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf('(/' + settings.nodeModuleAsarPathToRetain), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf('(/' + settings.nodeModulePathToRetain), -1); errorTelemetry.dispose(); service.dispose(); @@ -560,7 +582,7 @@ suite('TelemetryService', () => { try { let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender, piiPaths: [settings.personalInfo + '/resources/app/'] }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender], piiPaths: [settings.personalInfo + '/resources/app/'] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousPathWithImportantInfoError: any = new Error(settings.dangerousPathWithImportantInfo); @@ -571,14 +593,14 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); await service.join(); - assert.notStrictEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.notStrictEqual(testAppender.events[1].data.msg.indexOf(settings.importantInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.importantInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -593,7 +615,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender, piiPaths: [settings.personalInfo + '/resources/app/'] }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender], piiPaths: [settings.personalInfo + '/resources/app/'] }); const errorTelemetry = new ErrorTelemetry(service); let dangerousPathWithImportantInfoError: any = new Error('dangerousPathWithImportantInfo'); @@ -604,14 +626,14 @@ suite('TelemetryService', () => { assert.strictEqual(errorStub.callCount, 1); // Test that important information remains but personal info does not - assert.notStrictEqual(testAppender.events[0].data.msg.indexOf(settings.importantInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.importantInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.notStrictEqual(testAppender.events[1].data.msg.indexOf(settings.importantInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.importantInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -625,7 +647,7 @@ suite('TelemetryService', () => { try { let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let missingModelError: any = new Error(settings.missingModelMessage); @@ -637,14 +659,14 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); await service.join(); - assert.notStrictEqual(testAppender.events[0].data.msg.indexOf(settings.missingModelPrefix), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.missingModelPrefix), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.notStrictEqual(testAppender.events[1].data.msg.indexOf(settings.missingModelPrefix), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.missingModelPrefix), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -658,7 +680,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let missingModelError: any = new Error('missingModelMessage'); @@ -670,14 +692,14 @@ suite('TelemetryService', () => { assert.strictEqual(errorStub.callCount, 1); // Test that no file information remains, but this particular // error message does (Received model events for missing model) - assert.notStrictEqual(testAppender.events[0].data.msg.indexOf(settings.missingModelPrefix), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.missingModelPrefix), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.notStrictEqual(testAppender.events[1].data.msg.indexOf(settings.missingModelPrefix), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.missingModelPrefix), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -691,7 +713,7 @@ suite('TelemetryService', () => { try { let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let noSuchFileError: any = new Error(settings.noSuchFileMessage); @@ -703,14 +725,14 @@ suite('TelemetryService', () => { this.clock.tick(ErrorTelemetry.ERROR_FLUSH_TIMEOUT); await service.join(); - assert.notStrictEqual(testAppender.events[0].data.msg.indexOf(settings.noSuchFilePrefix), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.noSuchFilePrefix), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.notStrictEqual(testAppender.events[1].data.msg.indexOf(settings.noSuchFilePrefix), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.noSuchFilePrefix), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -727,7 +749,7 @@ suite('TelemetryService', () => { window.onerror = errorStub; let settings = new ErrorTestingSettings(); let testAppender = new TestTelemetryAppender(); - let service = new JoinableTelemetryService({ appender: testAppender }, undefined!); + let service = new JoinableTelemetryService({ appenders: [testAppender] }); const errorTelemetry = new ErrorTelemetry(service); let noSuchFileError: any = new Error('noSuchFileMessage'); noSuchFileError.stack = settings.stack; @@ -739,14 +761,14 @@ suite('TelemetryService', () => { // Test that no file information remains, but this particular // error message does (ENOENT: no such file or directory) Errors.onUnexpectedError(noSuchFileError); - assert.notStrictEqual(testAppender.events[0].data.msg.indexOf(settings.noSuchFilePrefix), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.msg.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.noSuchFilePrefix), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.personalInfo), -1); - assert.strictEqual(testAppender.events[0].data.callstack.indexOf(settings.filePrefix), -1); - assert.notStrictEqual(testAppender.events[0].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); - assert.strictEqual(testAppender.events[0].data.callstack.split('\n').length, settings.stack.length); + assert.notStrictEqual(testAppender.events[1].data.msg.indexOf(settings.noSuchFilePrefix), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.msg.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.noSuchFilePrefix), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.personalInfo), -1); + assert.strictEqual(testAppender.events[1].data.callstack.indexOf(settings.filePrefix), -1); + assert.notStrictEqual(testAppender.events[1].data.callstack.indexOf(settings.stack[4].replace(settings.randomUserFile, settings.anonymizedRandomUserFile)), -1); + assert.strictEqual(testAppender.events[1].data.callstack.split('\n').length, settings.stack.length); errorTelemetry.dispose(); service.dispose(); @@ -755,42 +777,40 @@ suite('TelemetryService', () => { } })); - test('Telemetry Service sends events when enableTelemetry is on', sinonTestFn(function () { + test('Telemetry Service sends events when telemetry is on', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appender: testAppender }, undefined!); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); return service.publicLog('testEvent').then(() => { - assert.strictEqual(testAppender.getEventsCount(), 1); + assert.strictEqual(testAppender.getEventsCount(), 3); service.dispose(); }); })); test('Telemetry Service checks with config service', function () { - let enableTelemetry = false; + let telemetryLevel = TelemetryConfiguration.OFF; let emitter = new Emitter(); let testAppender = new TestTelemetryAppender(); let service = new TelemetryService({ - appender: testAppender + appenders: [testAppender] }, new class extends TestConfigurationService { override onDidChangeConfiguration = emitter.event; override getValue() { - return { - enableTelemetry: enableTelemetry - } as any; + return telemetryLevel as any; } }()); - assert.strictEqual(service.isOptedIn, false); + assert.strictEqual(service.telemetryLevel, TelemetryLevel.NONE); - enableTelemetry = true; + telemetryLevel = TelemetryConfiguration.ON; emitter.fire({}); - assert.strictEqual(service.isOptedIn, true); + assert.strictEqual(service.telemetryLevel, TelemetryLevel.USAGE); - enableTelemetry = false; + telemetryLevel = TelemetryConfiguration.ERROR; emitter.fire({}); - assert.strictEqual(service.isOptedIn, false); + assert.strictEqual(service.telemetryLevel, TelemetryLevel.ERROR); service.dispose(); }); diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index bf088ba949..b28e664fff 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -25,6 +25,9 @@ export const enum TerminalSettingId { AutomationShellLinux = 'terminal.integrated.automationShell.linux', AutomationShellMacOs = 'terminal.integrated.automationShell.osx', AutomationShellWindows = 'terminal.integrated.automationShell.windows', + AutomationProfileLinux = 'terminal.integrated.automationProfile.linux', + AutomationProfileMacOs = 'terminal.integrated.automationProfile.osx', + AutomationProfileWindows = 'terminal.integrated.automationProfile.windows', ShellArgsLinux = 'terminal.integrated.shellArgs.linux', ShellArgsMacOs = 'terminal.integrated.shellArgs.osx', ShellArgsWindows = 'terminal.integrated.shellArgs.windows', @@ -64,6 +67,9 @@ export const enum TerminalSettingId { DetectLocale = 'terminal.integrated.detectLocale', DefaultLocation = 'terminal.integrated.defaultLocation', GpuAcceleration = 'terminal.integrated.gpuAcceleration', + TerminalTitleSeparator = 'terminal.integrated.tabs.separator', + TerminalTitle = 'terminal.integrated.tabs.title', + TerminalDescription = 'terminal.integrated.tabs.description', RightClickBehavior = 'terminal.integrated.rightClickBehavior', Cwd = 'terminal.integrated.cwd', ConfirmOnExit = 'terminal.integrated.confirmOnExit', @@ -81,7 +87,6 @@ export const enum TerminalSettingId { 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', @@ -89,11 +94,12 @@ export const enum TerminalSettingId { LocalEchoExcludePrograms = 'terminal.integrated.localEchoExcludePrograms', LocalEchoStyle = 'terminal.integrated.localEchoStyle', EnablePersistentSessions = 'terminal.integrated.enablePersistentSessions', + PersistentSessionReviveProcess = 'terminal.integrated.persistentSessionReviveProcess', CustomGlyphs = 'terminal.integrated.customGlyphs', PersistentSessionScrollback = 'terminal.integrated.persistentSessionScrollback', - PersistentSessionExperimentalSerializer = 'terminal.integrated.persistentSessionExperimentalSerializer', InheritEnv = 'terminal.integrated.inheritEnv', ShowLinkHover = 'terminal.integrated.showLinkHover', + IgnoreProcessNames = 'terminal.integrated.ignoreProcessNames', } export enum WindowsShellType { @@ -132,6 +138,7 @@ export interface IPtyHostAttachTarget { workspaceName: string; isOrphan: boolean; icon: TerminalIcon | undefined; + fixedDimensions: IFixedTerminalDimensions | undefined; } export enum TitleEventSource { @@ -140,7 +147,9 @@ export enum TitleEventSource { /** From the process name property*/ Process, /** From the VT sequence */ - Sequence + Sequence, + /** Config changed */ + Config } export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo; @@ -171,6 +180,47 @@ export enum TerminalIpcChannels { } export const IPtyService = createDecorator('ptyService'); + +export const enum ProcessPropertyType { + Cwd = 'cwd', + InitialCwd = 'initialCwd', + FixedDimensions = 'fixedDimensions', + Title = 'title', + ShellType = 'shellType', + HasChildProcesses = 'hasChildProcesses', + ResolvedShellLaunchConfig = 'resolvedShellLaunchConfig', + OverrideDimensions = 'overrideDimensions' +} + +export interface IProcessProperty { + type: T, + value: IProcessPropertyMap[T] +} + +export interface IProcessPropertyMap { + [ProcessPropertyType.Cwd]: string, + [ProcessPropertyType.InitialCwd]: string, + [ProcessPropertyType.FixedDimensions]: IFixedTerminalDimensions, + [ProcessPropertyType.Title]: string + [ProcessPropertyType.ShellType]: TerminalShellType | undefined, + [ProcessPropertyType.HasChildProcesses]: boolean, + [ProcessPropertyType.ResolvedShellLaunchConfig]: IShellLaunchConfig, + [ProcessPropertyType.OverrideDimensions]: ITerminalDimensionsOverride | undefined +} + +export interface IFixedTerminalDimensions { + /** + * The fixed columns of the terminal. + */ + cols?: number; + + /** + * The fixed rows of the terminal. + */ + rows?: number; +} + + export interface IPtyService { readonly _serviceBrand: undefined; @@ -181,16 +231,12 @@ export interface IPtyService { 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 } }>; - readonly onProcessTitleChanged: Event<{ id: number, event: string }>; - readonly onProcessShellTypeChanged: Event<{ id: number, event: TerminalShellType }>; - readonly onProcessOverrideDimensions: Event<{ id: number, event: ITerminalDimensionsOverride | undefined }>; - readonly onProcessResolvedShellLaunchConfig: Event<{ id: number, event: IShellLaunchConfig }>; + readonly onProcessReady: Event<{ id: number, event: IProcessReadyEvent }>; readonly onProcessReplay: Event<{ id: number, event: IPtyHostProcessReplayEvent }>; readonly onProcessOrphanQuestion: Event<{ id: number }>; readonly onDidRequestDetach: Event<{ requestId: number, workspaceId: string, instanceId: number }>; - readonly onProcessDidChangeHasChildProcesses: Event<{ id: number, event: boolean }>; + readonly onDidChangeProperty: Event<{ id: number, property: IProcessProperty }> + readonly onProcessExit: Event<{ id: number, event: number | undefined }>; restartPtyHost?(): Promise; shutdownAll?(): Promise; @@ -240,6 +286,20 @@ export interface IPtyService { reduceConnectionGraceTime(): Promise; requestDetachInstance(workspaceId: string, instanceId: number): Promise; acceptDetachInstanceReply(requestId: number, persistentProcessId?: number): Promise; + /** + * Serializes and returns terminal state. + * @param ids The persistent terminal IDs to serialize. + */ + serializeTerminalState(ids: number[]): Promise; + /** + * Revives a workspaces terminal processes, these can then be reconnected to using the normal + * flow for restoring terminals after reloading. + */ + reviveTerminalProcesses(state: string): Promise; + refreshProperty(id: number, property: ProcessPropertyType): Promise; + updateProperty(id: number, property: ProcessPropertyType, value: any): Promise; + + refreshIgnoreProcessNames?(names: string[]): Promise; } export interface IRequestResolveVariablesEvent { @@ -341,7 +401,7 @@ export interface IShellLaunchConfig { /** * This is a terminal that attaches to an already running terminal. */ - attachPersistentProcess?: { id: number; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string, hasChildProcesses?: boolean }; + attachPersistentProcess?: { id: number; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string, hasChildProcesses?: boolean, fixedDimensions?: IFixedTerminalDimensions }; /** * Whether the terminal process environment should be exactly as provided in @@ -389,17 +449,30 @@ export interface IShellLaunchConfig { * The color ID to use for this terminal. If not specified it will use the default fallback */ color?: string; + + /** + * When a parent terminal is provided via API, the group needs + * to find the index in order to place the child + * directly to the right of its parent. + */ + parentTerminalId?: number; + + /** + * The dimensions for the instance as set by the user + * or via Size to Content Width + */ + fixedDimensions?: IFixedTerminalDimensions; } export interface ICreateContributedTerminalProfileOptions { icon?: URI | string | { light: URI, dark: URI }; color?: string; - splitActiveTerminal?: boolean; + location?: TerminalLocation | { viewColumn: number, preserveState?: boolean } | { splitActiveTerminal: boolean }; } export enum TerminalLocation { - Panel = 0, - Editor = 1 + Panel = 1, + Editor = 2 } export const enum TerminalLocationString { @@ -431,9 +504,14 @@ export interface ITerminalLaunchError { export interface IProcessReadyEvent { pid: number, cwd: string, + capabilities: ProcessCapability[], requiresWindowsMode?: boolean } +export const enum ProcessCapability { + CwdDetection = 'cwdDetection' +} + /** * An interface representing a raw terminal child process, this contains a subset of the * child_process.ChildProcess node.js interface. @@ -451,14 +529,15 @@ export interface ITerminalChildProcess { */ shouldPersist: boolean; + /** + * Capabilities of the process, designated when it starts + */ + capabilities: ProcessCapability[]; + onProcessData: Event; - onProcessExit: Event; onProcessReady: Event; - onProcessTitleChanged: Event; - onProcessShellTypeChanged: Event; - onProcessOverrideDimensions?: Event; - onProcessResolvedShellLaunchConfig?: Event; - onDidChangeHasChildProcesses?: Event; + onDidChangeProperty: Event>; + onProcessExit: Event; /** * Starts the process. @@ -501,13 +580,14 @@ export interface ITerminalChildProcess { getInitialCwd(): Promise; getCwd(): Promise; getLatency(): Promise; + refreshProperty(property: ProcessPropertyType): Promise; + updateProperty(property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise; } export interface IReconnectConstants { graceTime: number; shortGraceTime: number; scrollback: number; - useExperimentalSerialization: boolean; } export const enum LocalReconnectConstants { diff --git a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts index 776428a03d..f28acfcaa3 100644 --- a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts +++ b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts @@ -73,6 +73,9 @@ const terminalProfileSchema: IJSONSchema = { 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 automationShellDeprecationMessageLinux = localize('terminal.integrated.automationShell.linux.deprecation', "This is deprecated, the new recommended way to configure your automation shell is by creating a terminal automation profile with {0}. This will currently take priority over the new automation profile settings but that will change in the future.", '`#terminal.integrated.automationProfile.linux#`'); +const automationShellDeprecationMessageOsx = localize('terminal.integrated.automationShell.osx.deprecation', "This is deprecated, the new recommended way to configure your automation shell is by creating a terminal automation profile with {0}. This will currently take priority over the new automation profile settings but that will change in the future.", '`#terminal.integrated.automationProfile.osx#`'); +const automationShellDeprecationMessageWindows = localize('terminal.integrated.automationShell.windows.deprecation', "This is deprecated, the new recommended way to configure your automation shell is by creating a terminal automation profile with {0}. This will currently take priority over the new automation profile settings but that will change in the future.", '`#terminal.integrated.automationProfile.windows#`'); const terminalPlatformConfiguration: IConfigurationNode = { id: 'terminal', @@ -87,7 +90,8 @@ const terminalPlatformConfiguration: IConfigurationNode = { 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 + default: null, + markdownDeprecationMessage: automationShellDeprecationMessageLinux }, [TerminalSettingId.AutomationShellMacOs]: { restricted: true, @@ -96,7 +100,8 @@ const terminalPlatformConfiguration: IConfigurationNode = { 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 + default: null, + markdownDeprecationMessage: automationShellDeprecationMessageOsx }, [TerminalSettingId.AutomationShellWindows]: { restricted: true, @@ -105,7 +110,38 @@ const terminalPlatformConfiguration: IConfigurationNode = { 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 + default: null, + markdownDeprecationMessage: automationShellDeprecationMessageWindows + }, + [TerminalSettingId.AutomationProfileLinux]: { + restricted: true, + markdownDescription: localize('terminal.integrated.automationProfile.linux', "The terminal profile to use on Linux for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.linux#'), + type: ['object', 'null'], + default: null, + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] + }, + [TerminalSettingId.AutomationProfileMacOs]: { + restricted: true, + description: localize('terminal.integrated.automationProfile.osx', "The terminal profile to use on macOS for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.osx#'), + type: ['object', 'null'], + default: null, + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] + }, + [TerminalSettingId.AutomationProfileWindows]: { + restricted: true, + description: localize('terminal.integrated.automationProfile.windows', "The terminal profile to use for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.windows#'), + type: ['object', 'null'], + default: null, + 'anyOf': [ + { type: 'null' }, + terminalProfileSchema + ] }, [TerminalSettingId.ShellLinux]: { restricted: true, @@ -177,7 +213,7 @@ const terminalPlatformConfiguration: IConfigurationNode = { 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`.' + "The Windows profiles to present when creating a new terminal via the terminal dropdown. Use the {0} property to automatically detect the shell's location. Or set the {1} property manually with an optional {2}.\n\nSet an existing profile to {3} to hide the profile from the list, for example: {4}.", '`source`', '`path`', '`args`', '`null`', '`"Ubuntu-20.04 (WSL)": null`' ), type: 'object', default: { @@ -241,7 +277,7 @@ const terminalPlatformConfiguration: IConfigurationNode = { 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`.' + "The macOS profiles to present when creating a new terminal via the terminal dropdown. Set the {0} property manually with an optional {1}.\n\nSet an existing profile to {2} to hide the profile from the list, for example: {3}.", '`path`', '`args`', '`null`', '`"bash": null`' ), type: 'object', default: { @@ -300,7 +336,7 @@ const terminalPlatformConfiguration: IConfigurationNode = { 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`.' + "The Linux profiles to present when creating a new terminal via the terminal dropdown. Set the {0} property manually with an optional {1}.\n\nSet an existing profile to {2} to hide the profile from the list, for example: {3}.", '`path`', '`args`', '`null`', '`"bash": null`' ), type: 'object', default: { @@ -366,17 +402,27 @@ const terminalPlatformConfiguration: IConfigurationNode = { type: 'number', default: 100 }, - [TerminalSettingId.PersistentSessionExperimentalSerializer]: { - scope: ConfigurationScope.APPLICATION, - description: localize('terminal.integrated.persistentSessionExperimentalSerializer', "Whether to use a more efficient experimental approach for restoring the terminal's buffer. This setting requires a restart to take effect."), - 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 + }, + [TerminalSettingId.IgnoreProcessNames]: { + description: localize('terminal.integrated.confirmIgnoreProcesses', "Configurable to provide a custom setting to ignore processes"), + type: 'array', + items: { + type: 'string', + uniqueItems: true + }, + default: [ + // Popular prompt programs, these should not count as child processes + 'starship', + 'oh-my-posh', + // Git bash may runs a subprocess of itself (bin\bash.exe -> usr\bin\bash.exe) + 'bash', + 'zsh', + ] } } }; @@ -389,17 +435,15 @@ export function registerTerminalPlatformConfiguration() { registerTerminalDefaultProfileConfiguration(); } -let lastDefaultProfilesConfiguration: IConfigurationNode | undefined; +let defaultProfilesConfiguration: IConfigurationNode | undefined; export function registerTerminalDefaultProfileConfiguration(detectedProfiles?: { os: OperatingSystem, profiles: ITerminalProfile[] }, extensionContributedProfiles?: readonly IExtensionTerminalProfile[]) { const registry = Registry.as(Extensions.Configuration); - if (lastDefaultProfilesConfiguration) { - registry.deregisterConfigurations([lastDefaultProfilesConfiguration]); - } let profileEnum; if (detectedProfiles) { profileEnum = createProfileSchemaEnums(detectedProfiles?.profiles, extensionContributedProfiles); } - lastDefaultProfilesConfiguration = { + const oldDefaultProfilesConfiguration = defaultProfilesConfiguration; + defaultProfilesConfiguration = { id: 'terminal', order: 100, title: localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"), @@ -431,5 +475,5 @@ export function registerTerminalDefaultProfileConfiguration(detectedProfiles?: { }, } }; - registry.registerConfiguration(lastDefaultProfilesConfiguration); + registry.updateConfigurations({ add: [defaultProfilesConfiguration], remove: oldDefaultProfilesConfiguration ? [oldDefaultProfilesConfiguration] : [] }); } diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts index 95b1a5df49..d91a029bf2 100644 --- a/src/vs/platform/terminal/common/terminalProcess.ts +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -5,7 +5,7 @@ import { UriComponents } from 'vs/base/common/uri'; import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; -import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IFixedTerminalDimensions, IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal'; export interface ISingleTerminalConfiguration { userValue: T | undefined; @@ -58,6 +58,7 @@ export interface IProcessDetails { isOrphan: boolean; icon: TerminalIcon | undefined; color: string | undefined; + fixedDimensions: IFixedTerminalDimensions | undefined; } export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo; diff --git a/src/vs/platform/terminal/common/terminalRecorder.ts b/src/vs/platform/terminal/common/terminalRecorder.ts index dcaee3f9d7..2e35c8ce02 100644 --- a/src/vs/platform/terminal/common/terminalRecorder.ts +++ b/src/vs/platform/terminal/common/terminalRecorder.ts @@ -17,14 +17,7 @@ export interface IRemoteTerminalProcessReplayEvent { events: ReplayEntry[]; } -export interface ITerminalSerializer { - handleData(data: string): void; - handleResize(cols: number, rows: number): void; - generateReplayEvent(): Promise; - setUnicodeVersion?(version: '6' | '11'): void; -} - -export class TerminalRecorder implements ITerminalSerializer { +export class TerminalRecorder { private _entries: RecorderEntry[]; private _totalDataLength: number = 0; diff --git a/src/vs/platform/terminal/node/childProcessMonitor.ts b/src/vs/platform/terminal/node/childProcessMonitor.ts index 9a7fdbee08..6d7191adb8 100644 --- a/src/vs/platform/terminal/node/childProcessMonitor.ts +++ b/src/vs/platform/terminal/node/childProcessMonitor.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { parse } from 'path'; +import { parse } from 'vs/base/common/path'; import { debounce, throttle } from 'vs/base/common/decorators'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -22,13 +22,7 @@ const enum Constants { ActiveDebounceDuration = 1000, } -const ignoreProcessNames = [ - // Popular prompt programs, these should not count as child processes - 'starship', - 'oh-my-posh', - // Git bash may runs a subprocess of itself (bin\bash.exe -> usr\bin\bash.exe) - 'bash', -]; +export const ignoreProcessNames: string[] = []; /** * Monitors a process for child processes, checking at differing times depending on input and output diff --git a/src/vs/platform/terminal/node/ptyHostMain.ts b/src/vs/platform/terminal/node/ptyHostMain.ts index d1d489dd86..50fd68681e 100644 --- a/src/vs/platform/terminal/node/ptyHostMain.ts +++ b/src/vs/platform/terminal/node/ptyHostMain.ts @@ -26,13 +26,11 @@ server.registerChannel(TerminalIpcChannels.Heartbeat, ProxyChannel.fromService(h const reconnectConstants: IReconnectConstants = { graceTime: parseInt(process.env.VSCODE_RECONNECT_GRACE_TIME || '0'), shortGraceTime: parseInt(process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME || '0'), - scrollback: parseInt(process.env.VSCODE_RECONNECT_SCROLLBACK || '100'), - useExperimentalSerialization: !!parseInt(process.env.VSCODE_RECONNECT_EXPERIMENTAL_SERIALIZATION || '1') + scrollback: parseInt(process.env.VSCODE_RECONNECT_SCROLLBACK || '100') }; delete process.env.VSCODE_RECONNECT_GRACE_TIME; delete process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME; delete process.env.VSCODE_RECONNECT_SCROLLBACK; -delete process.env.VSCODE_RECONNECT_EXPERIMENTAL_SERIALIZATION; const ptyService = new PtyService(lastPtyId, logService, reconnectConstants); server.registerChannel(TerminalIpcChannels.PtyHost, ProxyChannel.fromService(ptyService)); diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index e762859794..b0389192da 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -8,14 +8,16 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { IProcessEnvironment, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; +import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { parsePtyHostPort } from 'vs/platform/environment/common/environmentService'; import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv'; import { ILogService } from 'vs/platform/log/common/log'; import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RequestStore } from 'vs/platform/terminal/common/requestStore'; -import { HeartbeatConstants, IHeartbeatService, IProcessDataEvent, IPtyService, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, TerminalIcon, TerminalIpcChannels, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { HeartbeatConstants, IHeartbeatService, IProcessDataEvent, IPtyService, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, TerminalIcon, TerminalIpcChannels, IProcessProperty, TitleEventSource, ProcessPropertyType, ProcessCapability, IProcessPropertyMap, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { registerTerminalPlatformConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { detectAvailableProfiles } from 'vs/platform/terminal/node/terminalProfiles'; @@ -46,7 +48,6 @@ export class PtyHostService extends Disposable implements IPtyService { private _restartCount = 0; private _isResponsive = true; private _isDisposed = false; - private _heartbeatFirstTimeout?: NodeJS.Timeout; private _heartbeatSecondTimeout?: NodeJS.Timeout; @@ -63,30 +64,23 @@ export class PtyHostService extends Disposable implements IPtyService { 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 }>()); - readonly onProcessExit = this._onProcessExit.event; - private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string } }>()); + private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string, capabilities: ProcessCapability[] } }>()); readonly onProcessReady = this._onProcessReady.event; private readonly _onProcessReplay = this._register(new Emitter<{ id: number, event: IPtyHostProcessReplayEvent }>()); readonly onProcessReplay = this._onProcessReplay.event; - private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<{ id: number, event: TerminalShellType }>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>()); readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; private readonly _onDidRequestDetach = this._register(new Emitter<{ requestId: number, workspaceId: string, instanceId: number }>()); readonly onDidRequestDetach = this._onDidRequestDetach.event; - private readonly _onProcessDidChangeHasChildProcesses = this._register(new Emitter<{ id: number, event: boolean }>()); - readonly onProcessDidChangeHasChildProcesses = this._onProcessDidChangeHasChildProcesses.event; + private readonly _onDidChangeProperty = this._register(new Emitter<{ id: number, property: IProcessProperty }>()); + readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); + readonly onProcessExit = this._onProcessExit.event; constructor( private readonly _reconnectConstants: IReconnectConstants, @IConfigurationService private readonly _configurationService: IConfigurationService, + @IEnvironmentService private readonly _environmentService: INativeEnvironmentService, @ILogService private readonly _logService: ILogService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { @@ -96,7 +90,7 @@ export class PtyHostService extends Disposable implements IPtyService { // remote server). registerTerminalPlatformConfiguration(); - this._shellEnv = isWindows ? Promise.resolve(process.env) : resolveShellEnv(this._logService, { _: [] }, process.env); + this._shellEnv = this._resolveShellEnv(); this._register(toDisposable(() => this._disposePtyHost())); @@ -104,26 +98,65 @@ export class PtyHostService extends Disposable implements IPtyService { this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables); [this._client, this._proxy] = this._startPtyHost(); + + this._register(this._configurationService.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration(TerminalSettingId.IgnoreProcessNames)) { + await this._refreshIgnoreProcessNames(); + } + })); + } + + async initialize(): Promise { + await this._refreshIgnoreProcessNames(); + } + + private get _ignoreProcessNames(): string[] { + return this._configurationService.getValue(TerminalSettingId.IgnoreProcessNames); + } + + private async _refreshIgnoreProcessNames(): Promise { + return this._proxy.refreshIgnoreProcessNames?.(this._ignoreProcessNames); + } + + private async _resolveShellEnv(): Promise { + if (isWindows) { + return process.env; + } + + try { + return await resolveShellEnv(this._logService, { _: [] }, process.env); + } catch (error) { + this._logService.error('ptyHost was unable to resolve shell environment', error); + + return {}; + } } private _startPtyHost(): [Client, IPtyService] { - const client = new Client( - FileAccess.asFileUri('bootstrap-fork', require).fsPath, - { - serverName: 'Pty Host', - args: ['--type=ptyHost'], - env: { - 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_RECONNECT_GRACE_TIME: this._reconnectConstants.graceTime, - VSCODE_RECONNECT_SHORT_GRACE_TIME: this._reconnectConstants.shortGraceTime, - VSCODE_RECONNECT_SCROLLBACK: this._reconnectConstants.scrollback, - VSCODE_RECONNECT_EXPERIMENTAL_SERIALIZATION: this._reconnectConstants.useExperimentalSerialization ? 1 : 0 - } + const opts: IIPCOptions = { + serverName: 'Pty Host', + args: ['--type=ptyHost'], + env: { + 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_RECONNECT_GRACE_TIME: this._reconnectConstants.graceTime, + VSCODE_RECONNECT_SHORT_GRACE_TIME: this._reconnectConstants.shortGraceTime, + VSCODE_RECONNECT_SCROLLBACK: this._reconnectConstants.scrollback } - ); + }; + + const ptyHostDebug = parsePtyHostPort(this._environmentService.args, this._environmentService.isBuilt); + if (ptyHostDebug) { + if (ptyHostDebug.break && ptyHostDebug.port) { + opts.debugBrk = ptyHostDebug.port; + } else if (!ptyHostDebug.break && ptyHostDebug.port) { + opts.debug = ptyHostDebug.port; + } + } + + const client = new Client(FileAccess.asFileUri('bootstrap-fork', require).fsPath, opts); this._onPtyHostStart.fire(); // Setup heartbeat service and trigger a heartbeat immediately to reset the timeouts @@ -159,13 +192,9 @@ export class PtyHostService extends Disposable implements IPtyService { // Create proxy and forward events const proxy = ProxyChannel.toService(client.getChannel(TerminalIpcChannels.PtyHost)); this._register(proxy.onProcessData(e => this._onProcessData.fire(e))); - this._register(proxy.onProcessExit(e => this._onProcessExit.fire(e))); this._register(proxy.onProcessReady(e => this._onProcessReady.fire(e))); - this._register(proxy.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e))); - this._register(proxy.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e))); - this._register(proxy.onProcessOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e))); - this._register(proxy.onProcessResolvedShellLaunchConfig(e => this._onProcessResolvedShellLaunchConfig.fire(e))); - this._register(proxy.onProcessDidChangeHasChildProcesses(e => this._onProcessDidChangeHasChildProcesses.fire(e))); + this._register(proxy.onProcessExit(e => this._onProcessExit.fire(e))); + this._register(proxy.onDidChangeProperty(e => this._onDidChangeProperty.fire(e))); this._register(proxy.onProcessReplay(e => this._onProcessReplay.fire(e))); this._register(proxy.onProcessOrphanQuestion(e => this._onProcessOrphanQuestion.fire(e))); this._register(proxy.onDidRequestDetach(e => this._onDidRequestDetach.fire(e))); @@ -266,6 +295,22 @@ export class PtyHostService extends Disposable implements IPtyService { return this._proxy.acceptDetachInstanceReply(requestId, persistentProcessId); } + async serializeTerminalState(ids: number[]): Promise { + return this._proxy.serializeTerminalState(ids); + } + + async reviveTerminalProcesses(state: string) { + return this._proxy.reviveTerminalProcesses(state); + } + + async refreshProperty(id: number, property: ProcessPropertyType): Promise { + return this._proxy.refreshProperty(id, property); + + } + async updateProperty(id: number, property: ProcessPropertyType, value: any): Promise { + return this._proxy.updateProperty(id, property, value); + } + async restartPtyHost(): Promise { /* __GDPR__ "ptyHost/restart" : {} @@ -277,9 +322,7 @@ export class PtyHostService extends Disposable implements IPtyService { } private _disposePtyHost(): void { - if (this._proxy.shutdownAll) { - this._proxy.shutdownAll(); - } + this._proxy.shutdownAll?.(); this._client.dispose(); } diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 1e0e96b9f2..bf511c2861 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { execFile } from 'child_process'; -import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async'; +import { AutoOpenBarrier, ProcessTimeRunOnceScheduler, Promises, Queue } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; @@ -12,16 +12,17 @@ import { URI } from 'vs/base/common/uri'; import { getSystemShell } from 'vs/base/node/shell'; import { ILogService } from 'vs/platform/log/common/log'; import { RequestStore } from 'vs/platform/terminal/common/requestStore'; -import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, ProcessCapability } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; import { Terminal as XtermTerminal } from 'xterm-headless'; -import type { SerializeAddon as XtermSerializeAddon } from 'xterm-addon-serialize'; +import type { ISerializeOptions, SerializeAddon as XtermSerializeAddon } from 'xterm-addon-serialize'; import type { Unicode11Addon as XtermUnicode11Addon } from 'xterm-addon-unicode11'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto } from 'vs/platform/terminal/common/terminalProcess'; -import { ITerminalSerializer, TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; import { getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess'; +import { localize } from 'vs/nls'; +import { ignoreProcessNames } from 'vs/platform/terminal/node/childProcessMonitor'; type WorkspaceId = string; @@ -34,6 +35,7 @@ export class PtyService extends Disposable implements IPtyService { private readonly _ptys: Map = new Map(); private readonly _workspaceLayoutInfos = new Map(); private readonly _detachInstanceRequestStore: RequestStore; + private readonly _revivedPtyIdMap: Map = new Map(); private readonly _onHeartbeat = this._register(new Emitter()); readonly onHeartbeat = this._onHeartbeat.event; @@ -42,24 +44,16 @@ export class PtyService extends Disposable implements IPtyService { readonly onProcessData = this._onProcessData.event; private readonly _onProcessReplay = this._register(new Emitter<{ id: number, event: IPtyHostProcessReplayEvent }>()); readonly onProcessReplay = this._onProcessReplay.event; + private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string, capabilities: ProcessCapability[] } }>()); + readonly onProcessReady = this._onProcessReady.event; private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); readonly onProcessExit = this._onProcessExit.event; - private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string } }>()); - readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter<{ id: number, event: TerminalShellType }>()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>()); readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; private readonly _onDidRequestDetach = this._register(new Emitter<{ requestId: number, workspaceId: string, instanceId: number }>()); readonly onDidRequestDetach = this._onDidRequestDetach.event; - private readonly _onProcessDidChangeHasChildProcesses = this._register(new Emitter<{ id: number, event: boolean }>()); - readonly onProcessDidChangeHasChildProcesses = this._onProcessDidChangeHasChildProcesses.event; + private readonly _onDidChangeProperty = this._register(new Emitter<{ id: number, property: IProcessProperty }>()); + readonly onDidChangeProperty = this._onDidChangeProperty.event; constructor( private _lastPtyId: number, @@ -78,6 +72,12 @@ export class PtyService extends Disposable implements IPtyService { this._detachInstanceRequestStore = this._register(new RequestStore(undefined, this._logService)); this._detachInstanceRequestStore.onCreateRequest(this._onDidRequestDetach.fire, this._onDidRequestDetach); } + + async refreshIgnoreProcessNames(names: string[]): Promise { + ignoreProcessNames.length = 0; + ignoreProcessNames.push(...names); + } + onPtyHostExit?: Event | undefined; onPtyHostStart?: Event | undefined; onPtyHostUnresponsive?: Event | undefined; @@ -97,6 +97,70 @@ export class PtyService extends Disposable implements IPtyService { this._detachInstanceRequestStore.acceptReply(requestId, processDetails); } + async serializeTerminalState(ids: number[]): Promise { + const promises: Promise[] = []; + for (const [persistentProcessId, persistentProcess] of this._ptys.entries()) { + if (ids.indexOf(persistentProcessId) !== -1) { + promises.push(Promises.withAsyncBody(async r => { + r({ + id: persistentProcessId, + shellLaunchConfig: persistentProcess.shellLaunchConfig, + processDetails: await this._buildProcessDetails(persistentProcessId, persistentProcess), + processLaunchOptions: persistentProcess.processLaunchOptions, + unicodeVersion: persistentProcess.unicodeVersion, + replayEvent: await persistentProcess.serializeNormalBuffer(), + timestamp: Date.now() + }); + })); + } + } + const serialized: ICrossVersionSerializedTerminalState = { + version: 1, + state: await Promise.all(promises) + }; + return JSON.stringify(serialized); + } + + async reviveTerminalProcesses(state: string) { + const parsedUnknown = JSON.parse(state); + if (!('version' in parsedUnknown) || !('state' in parsedUnknown) || !Array.isArray(parsedUnknown.state)) { + this._logService.warn('Could not revive serialized processes, wrong format', parsedUnknown); + return; + } + const parsedCrossVersion = parsedUnknown as ICrossVersionSerializedTerminalState; + if (parsedCrossVersion.version !== 1) { + this._logService.warn(`Could not revive serialized processes, wrong version "${parsedCrossVersion.version}"`, parsedCrossVersion); + return; + } + const parsed = parsedCrossVersion.state as ISerializedTerminalState[]; + for (const state of parsed) { + const restoreMessage = localize({ + key: 'terminal-session-restore', + comment: ['date the snapshot was taken', 'time the snapshot was taken'] + }, "Session contents restored from {0} at {1}", new Date(state.timestamp).toLocaleDateString(), new Date(state.timestamp).toLocaleTimeString()); + const newId = await this.createProcess( + { + ...state.shellLaunchConfig, + cwd: state.processDetails.cwd, + initialText: state.replayEvent.events[0].data + '\x1b[0m\n\n\r\x1b[1;48;5;252;38;5;234m ' + restoreMessage + ' \x1b[K\x1b[0m\n\r' + }, + state.processDetails.cwd, + state.replayEvent.events[0].cols, + state.replayEvent.events[0].rows, + state.unicodeVersion, + state.processLaunchOptions.env, + state.processLaunchOptions.executableEnv, + state.processLaunchOptions.windowsEnableConpty, + true, + state.processDetails.workspaceId, + state.processDetails.workspaceName, + true + ); + // Don't start the process here as there's no terminal to answer CPR + this._revivedPtyIdMap.set(state.id, { newId, state }); + } + } + async shutdownAll(): Promise { this.dispose(); } @@ -112,7 +176,8 @@ export class PtyService extends Disposable implements IPtyService { windowsEnableConpty: boolean, shouldPersist: boolean, workspaceId: string, - workspaceName: string + workspaceName: string, + isReviving?: boolean ): Promise { if (shellLaunchConfig.attachPersistentProcess) { throw new Error('Attempt to create a process when attach object was provided'); @@ -120,26 +185,22 @@ export class PtyService extends Disposable implements IPtyService { const id = ++this._lastPtyId; const process = new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty, this._logService); process.onProcessData(event => this._onProcessData.fire({ id, event })); - process.onProcessExit(event => this._onProcessExit.fire({ id, event })); - if (process.onProcessOverrideDimensions) { - process.onProcessOverrideDimensions(event => this._onProcessOverrideDimensions.fire({ id, event })); - } - if (process.onProcessResolvedShellLaunchConfig) { - process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event })); - } - if (process.onDidChangeHasChildProcesses) { - process.onDidChangeHasChildProcesses(event => this._onProcessDidChangeHasChildProcesses.fire({ id, event })); - } - const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, unicodeVersion, this._reconnectConstants, this._logService, shellLaunchConfig.icon); - process.onProcessExit(() => { + const processLaunchOptions: IPersistentTerminalProcessLaunchOptions = { + env, + executableEnv, + windowsEnableConpty + }; + const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, processLaunchOptions, unicodeVersion, this._reconnectConstants, this._logService, isReviving ? shellLaunchConfig.initialText : undefined, shellLaunchConfig.icon, shellLaunchConfig.color, shellLaunchConfig.fixedDimensions); + process.onDidChangeProperty(property => this._onDidChangeProperty.fire({ id, property })); + process.onProcessExit(event => { persistentProcess.dispose(); this._ptys.delete(id); + this._onProcessExit.fire({ id, event }); }); persistentProcess.onProcessReplay(event => this._onProcessReplay.fire({ id, event })); persistentProcess.onProcessReady(event => this._onProcessReady.fire({ id, event })); - persistentProcess.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event })); - persistentProcess.onProcessShellTypeChanged(event => this._onProcessShellTypeChanged.fire({ id, event })); persistentProcess.onProcessOrphanQuestion(() => this._onProcessOrphanQuestion.fire({ id })); + persistentProcess.onDidChangeProperty(property => this._onDidChangeProperty.fire({ id, property })); this._ptys.set(id, persistentProcess); return id; } @@ -161,6 +222,14 @@ export class PtyService extends Disposable implements IPtyService { this._throwIfNoPty(id).setIcon(icon, color); } + async refreshProperty(id: number, type: ProcessPropertyType): Promise { + return this._throwIfNoPty(id).refreshProperty(type); + } + + async updateProperty(id: number, type: ProcessPropertyType, value: any): Promise { + return this._throwIfNoPty(id).updateProperty(type, value); + } + async detachFromProcess(id: number): Promise { this._throwIfNoPty(id).detach(); } @@ -270,10 +339,11 @@ export class PtyService extends Disposable implements IPtyService { private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise> { try { - const persistentProcess = this._throwIfNoPty(t.terminal); + const persistentProcessId = this._revivedPtyIdMap.get(t.terminal)?.newId ?? t.terminal; + const persistentProcess = this._throwIfNoPty(persistentProcessId); const processDetails = persistentProcess && await this._buildProcessDetails(t.terminal, persistentProcess); return { - terminal: processDetails ?? null, + terminal: { ...processDetails, id: persistentProcessId } ?? null, relativeSize: t.relativeSize }; } catch (e) { @@ -298,7 +368,8 @@ export class PtyService extends Disposable implements IPtyService { cwd, isOrphan, icon: persistentProcess.icon, - color: persistentProcess.color + color: persistentProcess.color, + fixedDimensions: persistentProcess.fixedDimensions }; } @@ -311,6 +382,13 @@ export class PtyService extends Disposable implements IPtyService { } } + +interface IPersistentTerminalProcessLaunchOptions { + env: IProcessEnvironment; + executableEnv: IProcessEnvironment; + windowsEnableConpty: boolean; +} + export class PersistentTerminalProcess extends Disposable { private readonly _bufferer: TerminalDataBufferer; @@ -322,23 +400,19 @@ export class PersistentTerminalProcess extends Disposable { private _orphanQuestionBarrier: AutoOpenBarrier | null; private _orphanQuestionReplyTime: number; private _orphanRequestQueue = new Queue(); - private _disconnectRunner1: RunOnceScheduler; - private _disconnectRunner2: RunOnceScheduler; + private _disconnectRunner1: ProcessTimeRunOnceScheduler; + private _disconnectRunner2: ProcessTimeRunOnceScheduler; private readonly _onProcessReplay = this._register(new Emitter()); readonly onProcessReplay = this._onProcessReplay.event; private readonly _onProcessReady = this._register(new Emitter()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData = this._onProcessData.event; private readonly _onProcessOrphanQuestion = this._register(new Emitter()); readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; + private readonly _onDidChangeProperty = this._register(new Emitter>()); + readonly onDidChangeProperty = this._onDidChangeProperty.event; private _inReplay = false; @@ -347,12 +421,16 @@ export class PersistentTerminalProcess extends Disposable { private _title: string | undefined; private _titleSource: TitleEventSource = TitleEventSource.Process; private _serializer: ITerminalSerializer; + private _wasRevived: boolean; + private _fixedDimensions: IFixedTerminalDimensions | undefined; get pid(): number { return this._pid; } + get shellLaunchConfig(): IShellLaunchConfig { return this._terminalProcess.shellLaunchConfig; } 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; } + get fixedDimensions(): IFixedTerminalDimensions | undefined { return this._fixedDimensions; } setTitle(title: string, titleSource: TitleEventSource): void { this._title = title; @@ -364,6 +442,10 @@ export class PersistentTerminalProcess extends Disposable { this._color = color; } + private _setFixedDimensions(fixedDimensions?: IFixedTerminalDimensions): void { + this._fixedDimensions = fixedDimensions; + } + constructor( private _persistentProcessId: number, private readonly _terminalProcess: TerminalProcess, @@ -372,48 +454,49 @@ export class PersistentTerminalProcess extends Disposable { readonly shouldPersistTerminal: boolean, cols: number, rows: number, - unicodeVersion: '6' | '11', + readonly processLaunchOptions: IPersistentTerminalProcessLaunchOptions, + public unicodeVersion: '6' | '11', reconnectConstants: IReconnectConstants, private readonly _logService: ILogService, + reviveBuffer: string | undefined, private _icon?: TerminalIcon, - private _color?: string + private _color?: string, + fixedDimensions?: IFixedTerminalDimensions ) { super(); this._logService.trace('persistentTerminalProcess#ctor', _persistentProcessId, arguments); - - if (reconnectConstants.useExperimentalSerialization) { - this._serializer = new XtermSerializer( - cols, - rows, - reconnectConstants.scrollback, - unicodeVersion - ); - } else { - this._serializer = new TerminalRecorder(cols, rows); - } + this._wasRevived = reviveBuffer !== undefined; + this._serializer = new XtermSerializer( + cols, + rows, + reconnectConstants.scrollback, + unicodeVersion, + reviveBuffer + ); + this._fixedDimensions = fixedDimensions; this._orphanQuestionBarrier = null; this._orphanQuestionReplyTime = 0; - this._disconnectRunner1 = this._register(new RunOnceScheduler(() => { + this._disconnectRunner1 = this._register(new ProcessTimeRunOnceScheduler(() => { 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); }, reconnectConstants.graceTime)); - this._disconnectRunner2 = this._register(new RunOnceScheduler(() => { + this._disconnectRunner2 = this._register(new ProcessTimeRunOnceScheduler(() => { 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); }, reconnectConstants.shortGraceTime)); - + this._register(this._terminalProcess.onProcessExit(() => this._bufferer.stopBuffering(this._persistentProcessId))); this._register(this._terminalProcess.onProcessReady(e => { this._pid = e.pid; this._cwd = e.cwd; this._onProcessReady.fire(e); })); - this._register(this._terminalProcess.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e))); - this._register(this._terminalProcess.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e))); + this._register(this._terminalProcess.onDidChangeProperty(e => { + this._onDidChangeProperty.fire(e); + })); // Data buffering to reduce the amount of messages going to the renderer this._bufferer = new TerminalDataBufferer((_, data) => this._onProcessData.fire(data)); this._register(this._bufferer.startBuffering(this._persistentProcessId, this._terminalProcess.onProcessData)); - this._register(this._terminalProcess.onProcessExit(() => this._bufferer.stopBuffering(this._persistentProcessId))); // Data recording for reconnect this._register(this.onProcessData(e => this._serializer.handleData(e))); @@ -434,6 +517,20 @@ export class PersistentTerminalProcess extends Disposable { } } + serializeNormalBuffer(): Promise { + return this._serializer.generateReplayEvent(true); + } + + async refreshProperty(type: ProcessPropertyType): Promise { + return this._terminalProcess.refreshProperty(type); + } + + async updateProperty(type: ProcessPropertyType, value: any): Promise { + if (type === ProcessPropertyType.FixedDimensions) { + this._setFixedDimensions(value); + } + } + async start(): Promise { this._logService.trace('persistentTerminalProcess#start', this._persistentProcessId, this._isStarted); if (!this._isStarted) { @@ -443,10 +540,19 @@ export class PersistentTerminalProcess extends Disposable { return result; } this._isStarted = true; + + // If the process was revived, trigger a replay on first start. An alternative approach + // could be to start it on the pty host before attaching but this fails on Windows as + // conpty's inherit cursor option which is required, ends up sending DSR CPR which + // causes conhost to hang when no response is received from the terminal (which wouldn't + // be attached yet). https://github.com/microsoft/terminal/issues/11213 + if (this._wasRevived) { + this.triggerReplay(); + } } else { - 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._onProcessReady.fire({ pid: this._pid, cwd: this._cwd, capabilities: this._terminalProcess.capabilities, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: this._terminalProcess.currentTitle }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: this._terminalProcess.shellType }); this.triggerReplay(); } return undefined; @@ -474,6 +580,7 @@ export class PersistentTerminalProcess extends Disposable { return this._terminalProcess.resize(cols, rows); } setUnicodeVersion(version: '6' | '11'): void { + this.unicodeVersion = version; this._serializer.setUnicodeVersion?.(version); // TODO: Pass in unicode version in ctor } @@ -563,9 +670,13 @@ class XtermSerializer implements ITerminalSerializer { cols: number, rows: number, scrollback: number, - unicodeVersion: '6' | '11' + unicodeVersion: '6' | '11', + reviveBuffer: string | undefined ) { this._xterm = new XtermTerminal({ cols, rows, scrollback }); + if (reviveBuffer) { + this._xterm.writeln(reviveBuffer); + } this.setUnicodeVersion(unicodeVersion); } @@ -577,10 +688,15 @@ class XtermSerializer implements ITerminalSerializer { this._xterm.resize(cols, rows); } - async generateReplayEvent(): Promise { + async generateReplayEvent(normalBufferOnly?: boolean): Promise { const serialize = new (await this._getSerializeConstructor()); this._xterm.loadAddon(serialize); - const serialized = serialize.serialize(this._xterm.getOption('scrollback')); + const options: ISerializeOptions = { scrollback: this._xterm.getOption('scrollback') }; + if (normalBufferOnly) { + options.excludeAltBuffer = true; + options.excludeModes = true; + } + const serialized = serialize.serialize(options); return { events: [ { @@ -643,3 +759,29 @@ function printTime(ms: number): string { const _ms = ms ? `${ms}ms` : ``; return `${_h}${_m}${_s}${_ms}`; } + +/** + * Serialized terminal state matching the interface that can be used across versions, the version + * should be verified before using the state payload. + */ +export interface ICrossVersionSerializedTerminalState { + version: number; + state: unknown; +} + +export interface ISerializedTerminalState { + id: number; + shellLaunchConfig: IShellLaunchConfig; + processDetails: IProcessDetails; + processLaunchOptions: IPersistentTerminalProcessLaunchOptions; + unicodeVersion: '6' | '11'; + replayEvent: IPtyHostProcessReplayEvent; + timestamp: number; +} + +export interface ITerminalSerializer { + handleData(data: string): void; + handleResize(cols: number, rows: number): void; + generateReplayEvent(normalBufferOnly?: boolean): Promise; + setUnicodeVersion?(version: '6' | '11'): void; +} diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 78d5e93751..7a328d9649 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -5,7 +5,6 @@ import { exec } from 'child_process'; import type * as pty from 'node-pty'; -import * as os from 'os'; import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -15,7 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { Promises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; -import { FlowControlConstants, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { FlowControlConstants, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap as IProcessPropertyMap, ProcessPropertyType, TerminalShellType, ProcessCapability, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; import { ChildProcessMonitor } from 'vs/platform/terminal/node/childProcessMonitor'; import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper'; @@ -77,8 +76,17 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess readonly id = 0; readonly shouldPersist = false; + private _properties: IProcessPropertyMap = { + cwd: '', + initialCwd: '', + fixedDimensions: { cols: undefined, rows: undefined }, + title: '', + shellType: undefined, + hasChildProcesses: true, + resolvedShellLaunchConfig: {}, + overrideDimensions: undefined + }; private static _lastKillOrStart = 0; - private _exitCode: number | undefined; private _exitMessage: string | undefined; private _closeTimeout: any; @@ -94,6 +102,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _delayedResizer: DelayedResizer | undefined; private readonly _initialCwd: string; private readonly _ptyOptions: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions; + private _capabilities: ProcessCapability[] = []; private _isPtyPaused: boolean = false; private _unacknowledgedCharCount: number = 0; @@ -102,24 +111,19 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; } get shellType(): TerminalShellType { return this._windowsShellHelper ? this._windowsShellHelper.shellType : undefined; } + get capabilities(): ProcessCapability[] { return this._capabilities; } + private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter()); - readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onDidChangeHasChildProcesses = this._register(new Emitter()); - readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; - - onProcessOverrideDimensions?: Event | undefined; - onProcessResolvedShellLaunchConfig?: Event | undefined; + private readonly _onDidChangeProperty = this._register(new Emitter>()); + readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter()); + readonly onProcessExit = this._onProcessExit.event; constructor( - private readonly _shellLaunchConfig: IShellLaunchConfig, + readonly shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, @@ -134,13 +138,15 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess super(); let name: string; if (isWindows) { - name = path.basename(this._shellLaunchConfig.executable || ''); + name = path.basename(this.shellLaunchConfig.executable || ''); } else { // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a // color prompt as defined in the default ~/.bashrc file. name = 'xterm-256color'; } this._initialCwd = cwd; + this._properties[ProcessPropertyType.InitialCwd] = this._initialCwd; + this._properties[ProcessPropertyType.Cwd] = this._initialCwd; const useConpty = windowsEnableConpty && process.platform === 'win32' && getWindowsBuildNumber() >= 18309; this._ptyOptions = { name, @@ -151,11 +157,11 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess rows, useConpty, // This option will force conpty to not redraw the whole viewport on launch - conptyInheritCursor: useConpty && !!_shellLaunchConfig.initialText + conptyInheritCursor: useConpty && !!shellLaunchConfig.initialText }; // Delay resizes to avoid conpty not respecting very early resize calls if (isWindows) { - if (useConpty && cols === 0 && rows === 0 && this._shellLaunchConfig.executable?.endsWith('Git\\bin\\bash.exe')) { + if (useConpty && cols === 0 && rows === 0 && this.shellLaunchConfig.executable?.endsWith('Git\\bin\\bash.exe')) { this._delayedResizer = new DelayedResizer(); this._register(this._delayedResizer.onTrigger(dimensions => { this._delayedResizer?.dispose(); @@ -168,10 +174,14 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess // WindowsShellHelper is used to fetch the process title and shell type this.onProcessReady(e => { this._windowsShellHelper = this._register(new WindowsShellHelper(e.pid)); - this._register(this._windowsShellHelper.onShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e))); - this._register(this._windowsShellHelper.onShellNameChanged(e => this._onProcessTitleChanged.fire(e))); + this._register(this._windowsShellHelper.onShellTypeChanged(e => this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: e }))); + this._register(this._windowsShellHelper.onShellNameChanged(e => this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: e }))); }); } + // Enable the cwd detection capability if the process supports it + if (isLinux || isMacintosh) { + this.capabilities.push(ProcessCapability.CwdDetection); + } } async start(): Promise { @@ -182,7 +192,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } try { - await this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions); + await this.setupPtyProcess(this.shellLaunchConfig, this._ptyOptions); return undefined; } catch (err) { this._logService.trace('IPty#spawn native exception', err); @@ -201,11 +211,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return { message: localize('launchFail.cwdDoesNotExist', "Starting directory (cwd) \"{0}\" does not exist", this._initialCwd.toString()) }; } } + this._onDidChangeProperty.fire({ type: ProcessPropertyType.InitialCwd, value: this._initialCwd }); return undefined; } private async _validateExecutable(): Promise { - const slc = this._shellLaunchConfig; + const slc = this.shellLaunchConfig; if (!slc.executable) { throw new Error('IShellLaunchConfig.executable not set'); } @@ -238,7 +249,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess const ptyProcess = (await import('node-pty')).spawn(shellLaunchConfig.executable!, args, options); this._ptyProcess = ptyProcess; this._childProcessMonitor = this._register(new ChildProcessMonitor(ptyProcess.pid, this._logService)); - this._childProcessMonitor.onDidChangeHasChildProcesses(this._onDidChangeHasChildProcesses.fire, this._onDidChangeHasChildProcesses); + this._childProcessMonitor.onDidChangeHasChildProcesses(value => this._onDidChangeProperty.fire({ type: ProcessPropertyType.HasChildProcesses, value })); this._processStartupComplete = new Promise(c => { this.onProcessReady(() => c()); }); @@ -337,7 +348,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } private _sendProcessId(pid: number) { - this._onProcessReady.fire({ pid, cwd: this._initialCwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); + this._onProcessReady.fire({ pid, cwd: this._initialCwd, capabilities: this.capabilities, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); } private _sendProcessTitle(ptyProcess: pty.IPty): void { @@ -345,7 +356,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return; } this._currentTitle = ptyProcess.process; - this._onProcessTitleChanged.fire(this._currentTitle); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: this._currentTitle }); } shutdown(immediate: boolean): void { @@ -386,6 +397,36 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this.input(data, true); } + async refreshProperty(type: ProcessPropertyType): Promise { + switch (type) { + case ProcessPropertyType.Cwd: + const newCwd = await this.getCwd(); + if (newCwd !== this._properties.cwd) { + this._properties.cwd = newCwd; + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Cwd, value: this._properties.cwd }); + } + return newCwd as IProcessPropertyMap[T]; + case ProcessPropertyType.InitialCwd: + const initialCwd = await this.getInitialCwd(); + if (initialCwd !== this._properties.initialCwd) { + this._properties.initialCwd = initialCwd; + this._onDidChangeProperty.fire({ type: ProcessPropertyType.InitialCwd, value: this._properties.initialCwd }); + } + return initialCwd as IProcessPropertyMap[T]; + case ProcessPropertyType.Title: + return this.currentTitle as IProcessPropertyMap[T]; + default: + return this.shellType as IProcessPropertyMap[T]; + } + } + + async updateProperty(type: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise { + //TODO: why is the type check necessary? + if (type === ProcessPropertyType.FixedDimensions && typeof value !== 'string' && value && ('cols' in value || 'rows' in value)) { + this._properties.fixedDimensions = value; + } + } + private _startWrite(): void { // Don't write if it's already queued of is there is nothing to write if (this._writeTimeout !== undefined || this._writeQueue.length === 0) { @@ -480,26 +521,24 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess 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 - const osRelease = os.release().split('.'); - if (osRelease.length > 0 && parseInt(osRelease[0]) < 20) { - return new Promise(resolve => { - if (!this._ptyProcess) { + // From Big Sur (darwin v20) there is a spawn blocking thread issue on Electron, + // this is fixed in VS Code's internal Electron. + // https://github.com/Microsoft/vscode/issues/105446 + return new Promise(resolve => { + if (!this._ptyProcess) { + resolve(this._initialCwd); + return; + } + this._logService.trace('IPty#pid'); + exec('lsof -OPln -p ' + this._ptyProcess.pid + ' | grep cwd', (error, stdout, stderr) => { + if (!error && stdout !== '') { + resolve(stdout.substring(stdout.indexOf('/'), stdout.length - 1)); + } else { + this._logService.error('lsof did not run successfully, it may not be on the $PATH?', error, stdout, stderr); resolve(this._initialCwd); - return; } - this._logService.trace('IPty#pid'); - exec('lsof -OPln -p ' + this._ptyProcess.pid + ' | grep cwd', (error, stdout, stderr) => { - if (!error && stdout !== '') { - resolve(stdout.substring(stdout.indexOf('/'), stdout.length - 1)); - } else { - this._logService.error('lsof did not run successfully, it may not be on the $PATH?', error, stdout, stderr); - resolve(this._initialCwd); - } - }); }); - } + }); } if (isLinux) { diff --git a/src/vs/platform/terminal/node/terminalProfiles.ts b/src/vs/platform/terminal/node/terminalProfiles.ts index 58e9f0b6ed..5a15e2316f 100644 --- a/src/vs/platform/terminal/node/terminalProfiles.ts +++ b/src/vs/platform/terminal/node/terminalProfiles.ts @@ -17,6 +17,7 @@ import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node import { ThemeIcon } from 'vs/platform/theme/common/themeService'; let profileSources: Map | undefined; +let logIfWslNotInstalled: boolean = true; export function detectAvailableProfiles( profiles: unknown, @@ -27,7 +28,7 @@ export function detectAvailableProfiles( fsProvider?: IFsProvider, logService?: ILogService, variableResolver?: (text: string[]) => Promise, - testPwshSourcePaths?: string[], + testPwshSourcePaths?: string[] ): Promise { fsProvider = fsProvider || { existsFile: pfs.SymlinkSupport.existsFile, @@ -130,7 +131,10 @@ async function detectAvailableWindowsProfiles( } } } catch (e) { - logService?.info('WSL is not installed, so could not detect WSL profiles'); + if (logIfWslNotInstalled) { + logService?.info('WSL is not installed, so could not detect WSL profiles'); + logIfWslNotInstalled = false; + } } } diff --git a/src/vs/platform/terminal/node/windowsShellHelper.ts b/src/vs/platform/terminal/node/windowsShellHelper.ts index 21b582f5db..f5439e7d4c 100644 --- a/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -120,7 +120,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe /** * Returns the innermost shell executable running in the terminal */ - getShellName(): Promise { + async getShellName(): Promise { if (this._isDisposed) { return Promise.resolve(''); } @@ -128,10 +128,10 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe if (this._currentRequest) { return this._currentRequest; } - this._currentRequest = new Promise(async resolve => { - if (!windowsProcessTree) { - windowsProcessTree = await import('windows-process-tree'); - } + if (!windowsProcessTree) { + windowsProcessTree = await import('windows-process-tree'); + } + this._currentRequest = new Promise(resolve => { windowsProcessTree.getProcessTree(this._rootProcessId, (tree) => { const name = this.traverseTree(tree); this._currentRequest = undefined; diff --git a/src/vs/platform/theme/browser/iconsStyleSheet.ts b/src/vs/platform/theme/browser/iconsStyleSheet.ts index 579fc06d7e..0bf335df3e 100644 --- a/src/vs/platform/theme/browser/iconsStyleSheet.ts +++ b/src/vs/platform/theme/browser/iconsStyleSheet.ts @@ -53,7 +53,7 @@ export function getIconsStyleSheet(): IIconsStyleSheet { for (let id in usedFontIds) { const fontContribution = usedFontIds[id]; const src = fontContribution.definition.src.map(l => `${asCSSUrl(l.location)} format('${l.format}')`).join(', '); - rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(id)}; }`); + rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(id)}; font-display: block; }`); } return rules.join('\n'); } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 2c19f8535e..47bf7c40d8 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -270,7 +270,7 @@ export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.ac export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hc: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); export const editorErrorBackground = registerColor('editorError.background', { dark: null, light: null, hc: null }, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F48771', light: '#E51400', hc: null }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); +export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F14C4C', light: '#E51400', hc: null }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); export const editorErrorBorder = registerColor('editorError.border', { dark: null, light: null, hc: Color.fromHex('#E47777').transparent(0.8) }, nls.localize('errorBorder', 'Border color of error boxes in the editor.')); export const editorWarningBackground = registerColor('editorWarning.background', { dark: null, light: null, hc: null }, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); @@ -278,8 +278,8 @@ export const editorWarningForeground = registerColor('editorWarning.foreground', export const editorWarningBorder = registerColor('editorWarning.border', { dark: null, light: null, hc: Color.fromHex('#FFCC00').transparent(0.8) }, nls.localize('warningBorder', 'Border color of warning boxes in the editor.')); export const editorInfoBackground = registerColor('editorInfo.background', { dark: null, light: null, hc: null }, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorInfoForeground = registerColor('editorInfo.foreground', { dark: '#75BEFF', light: '#75BEFF', hc: null }, nls.localize('editorInfo.foreground', 'Foreground color of info squigglies in the editor.')); -export const editorInfoBorder = registerColor('editorInfo.border', { dark: null, light: null, hc: Color.fromHex('#75BEFF').transparent(0.8) }, nls.localize('infoBorder', 'Border color of info boxes in the editor.')); +export const editorInfoForeground = registerColor('editorInfo.foreground', { dark: '#3794FF', light: '#1a85ff', hc: '#3794FF' }, nls.localize('editorInfo.foreground', 'Foreground color of info squigglies in the editor.')); +export const editorInfoBorder = registerColor('editorInfo.border', { dark: null, light: null, hc: Color.fromHex('#3794FF').transparent(0.8) }, nls.localize('infoBorder', 'Border color of info boxes in the editor.')); export const editorHintForeground = registerColor('editorHint.foreground', { dark: Color.fromHex('#eeeeee').transparent(0.7), light: '#6c6c6c', hc: null }, nls.localize('editorHint.foreground', 'Foreground color of hint squigglies in the editor.')); export const editorHintBorder = registerColor('editorHint.border', { dark: null, light: null, hc: Color.fromHex('#eeeeee').transparent(0.8) }, nls.localize('hintBorder', 'Border color of hint boxes in the editor.')); @@ -371,6 +371,10 @@ export const editorActiveLinkForeground = registerColor('editorLink.activeForegr */ export const editorInlayHintForeground = registerColor('editorInlayHint.foreground', { dark: transparent(badgeForeground, .8), light: transparent(badgeForeground, .8), hc: badgeForeground }, nls.localize('editorInlayHintForeground', 'Foreground color of inline hints')); export const editorInlayHintBackground = registerColor('editorInlayHint.background', { dark: transparent(badgeBackground, .6), light: transparent(badgeBackground, .3), hc: badgeBackground }, nls.localize('editorInlayHintBackground', 'Background color of inline hints')); +export const editorInlayHintTypeForeground = registerColor('editorInlayHint.typeForeground', { dark: editorInlayHintForeground, light: editorInlayHintForeground, hc: editorInlayHintForeground }, nls.localize('editorInlayHintForegroundTypes', 'Foreground color of inline hints for types')); +export const editorInlayHintTypeBackground = registerColor('editorInlayHint.typeBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hc: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundTypes', 'Background color of inline hints for types')); +export const editorInlayHintParameterForeground = registerColor('editorInlayHint.parameterForeground', { dark: editorInlayHintForeground, light: editorInlayHintForeground, hc: editorInlayHintForeground }, nls.localize('editorInlayHintForegroundParameter', 'Foreground color of inline hints for parameters')); +export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hc: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); /** * Editor lighbulb icon colors @@ -496,10 +500,12 @@ export const overviewRulerFindMatchForeground = registerColor('editorOverviewRul export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', { dark: '#A0A0A0CC', light: '#A0A0A0CC', hc: '#A0A0A0CC' }, nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true); export const minimapFindMatch = registerColor('minimap.findMatchHighlight', { light: '#d18616', dark: '#d18616', hc: '#AB5A00' }, nls.localize('minimapFindMatchHighlight', 'Minimap marker color for find matches.'), true); +export const minimapSelectionOccurrenceHighlight = registerColor('minimap.selectionOccurrenceHighlight', { light: '#c9c9c9', dark: '#676767', hc: '#ffffff' }, nls.localize('minimapSelectionOccurrenceHighlight', 'Minimap marker color for repeating editor selections.'), true); export const minimapSelection = registerColor('minimap.selectionHighlight', { light: '#ADD6FF', dark: '#264F78', hc: '#ffffff' }, nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true); export const minimapError = registerColor('minimap.errorHighlight', { 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('minimapError', 'Minimap marker color for errors.')); export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hc: null }, nls.localize('minimapBackground', "Minimap background color.")); +export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hc: Color.fromHex('#000f') }, nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); export const minimapSliderBackground = registerColor('minimapSlider.background', { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hc: transparent(scrollbarSliderBackground, 0.5) }, nls.localize('minimapSliderBackground', "Minimap slider background color.")); export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hc: transparent(scrollbarSliderHoverBackground, 0.5) }, nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index b929b11f19..9627f433df 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -531,6 +531,7 @@ function createDefaultTokenClassificationRegistry(): TokenClassificationRegistry registerTokenType('property', nls.localize('property', "Style for properties."), [['variable.other.property']]); registerTokenType('enumMember', nls.localize('enumMember', "Style for enum members."), [['variable.other.enummember']]); registerTokenType('event', nls.localize('event', "Style for events."), [['variable.other.event']]); + registerTokenType('decorator', nls.localize('decorator', "Style for decorators & annotations."), [['entity.name.decorator'], ['entity.name.function']]); registerTokenType('label', nls.localize('labels', "Style for labels. "), undefined); diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 4e96d6619f..5f698b9960 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -72,7 +72,7 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - const updateMode = getMigratedSettingValue(this.configurationService, 'update.mode', 'update.channel'); + const updateMode = this.getUpdateMode(); const quality = this.getProductQuality(updateMode); if (!quality) { @@ -104,6 +104,10 @@ export abstract class AbstractUpdateService implements IUpdateService { } } + private getUpdateMode(): 'none' | 'manual' | 'start' | 'default' { + return getMigratedSettingValue<'none' | 'manual' | 'start' | 'default'>(this.configurationService, 'update.mode', 'update.channel'); + } + private getProductQuality(updateMode: string): string | undefined { return updateMode === 'none' ? undefined : this.productService.quality; } @@ -177,20 +181,18 @@ export abstract class AbstractUpdateService implements IUpdateService { return Promise.resolve(undefined); } - isLatestVersion(): Promise { + async isLatestVersion(): Promise { if (!this.url) { - return Promise.resolve(undefined); + return undefined; + } else if (this.getUpdateMode() === 'none') { + return false; } - return this.requestService.request({ url: this.url }, CancellationToken.None).then(context => { - // The update server replies with 204 (No Content) when no - // update is available - that's all we want to know. - if (context.res.statusCode === 204) { - return true; - } else { - return false; - } - }); + const context = await this.requestService.request({ url: this.url }, CancellationToken.None); + + // The update server replies with 204 (No Content) when no + // update is available - that's all we want to know. + return context.res.statusCode === 204; } protected getUpdateType(): UpdateType { diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 5887f3fd80..019c06af83 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -24,7 +24,7 @@ import { FileChangesEvent, FileOperationError, FileOperationResult, FileSystemPr import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { Change, getLastSyncResourceUri, IRemoteUserData, IResourcePreview as IBaseResourcePreview, ISyncData, ISyncResourceHandle, ISyncResourcePreview as IBaseSyncResourcePreview, IUserData, IUserDataInitializer, IUserDataManifest, IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, MergeState, PREVIEW_DIR_NAME, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, getLastSyncResourceUri, IRemoteUserData, IResourcePreview as IBaseResourcePreview, ISyncData, ISyncResourceHandle, ISyncResourcePreview as IBaseSyncResourcePreview, IUserData, IUserDataInitializer, IUserDataManifest, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, MergeState, PREVIEW_DIR_NAME, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; type SyncSourceClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -86,7 +86,7 @@ interface ISyncResourcePreview extends IBaseSyncResourcePreview { readonly resourcePreviews: IEditableResourcePreview[]; } -export abstract class AbstractSynchroniser extends Disposable { +export abstract class AbstractSynchroniser extends Disposable implements IUserDataSynchroniser { private syncPreviewPromise: CancelablePromise | null = null; @@ -151,7 +151,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`); const preview = await this.syncPreviewPromise!; this.syncPreviewPromise = null; - const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true); + const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true, this.getUserDataSyncConfiguration()); this.setStatus(status); } @@ -159,7 +159,7 @@ export abstract class AbstractSynchroniser extends Disposable { else { this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`); const lastSyncUserData = await this.getLastSyncUserData(); - const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, true, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true; + const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, true, this.getUserDataSyncConfiguration(), CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true; if (hasRemoteChanged) { this._onDidChangeLocal.fire(); } @@ -183,11 +183,11 @@ export abstract class AbstractSynchroniser extends Disposable { } async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise { - await this._sync(manifest, true, headers); + await this._sync(manifest, true, this.getUserDataSyncConfiguration(), headers); } - async preview(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise { - return this._sync(manifest, false, headers); + async preview(manifest: IUserDataManifest | null, userDataSyncConfiguration: IUserDataSyncConfiguration, headers: IHeaders = {}): Promise { + return this._sync(manifest, false, userDataSyncConfiguration, headers); } async apply(force: boolean, headers: IHeaders = {}): Promise { @@ -203,7 +203,7 @@ export abstract class AbstractSynchroniser extends Disposable { } } - private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise { + private async _sync(manifest: IUserDataManifest | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration, headers: IHeaders): Promise { try { this.syncHeaders = { ...headers }; @@ -232,7 +232,7 @@ export abstract class AbstractSynchroniser extends Disposable { try { const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData); - status = await this.performSync(remoteUserData, lastSyncUserData, apply); + status = await this.performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); if (status === SyncStatus.HasConflicts) { this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`); } else if (status === SyncStatus.Idle) { @@ -268,7 +268,7 @@ export abstract class AbstractSynchroniser extends Disposable { const isRemoteDataFromCurrentMachine = await this.isRemoteDataFromCurrentMachine(remoteUserData); /* use replace sync data */ - const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, isRemoteDataFromCurrentMachine, CancellationToken.None); + const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, isRemoteDataFromCurrentMachine, this.getUserDataSyncConfiguration(), CancellationToken.None); const resourcePreviews: [IResourcePreview, IAcceptResult][] = []; for (const resourcePreviewResult of resourcePreviewResults) { @@ -311,7 +311,7 @@ export abstract class AbstractSynchroniser extends Disposable { return this.getRemoteUserData(lastSyncUserData); } - private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise { + private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { // current version is not compatible with cloud version this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource }); @@ -319,7 +319,7 @@ export abstract class AbstractSynchroniser extends Disposable { } try { - return await this.doSync(remoteUserData, lastSyncUserData, apply); + return await this.doSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); } catch (e) { if (e instanceof UserDataSyncError) { switch (e.code) { @@ -327,7 +327,7 @@ export abstract class AbstractSynchroniser extends Disposable { case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again... this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`); - return this.performSync(remoteUserData, lastSyncUserData, apply); + return this.performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); case UserDataSyncErrorCode.Conflict: case UserDataSyncErrorCode.PreconditionFailed: @@ -341,18 +341,18 @@ export abstract class AbstractSynchroniser extends Disposable { // and one of them successfully updated remote and last sync state. lastSyncUserData = await this.getLastSyncUserData(); - return this.performSync(remoteUserData, lastSyncUserData, apply); + return this.performSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); } } throw e; } } - protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise { + protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { try { // generate or use existing preview if (!this.syncPreviewPromise) { - this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, token)); + this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration, token)); } const preview = await this.syncPreviewPromise; @@ -561,9 +561,9 @@ export abstract class AbstractSynchroniser extends Disposable { } catch (e) { /* ignore */ } } - private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, token: CancellationToken): Promise { + private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration, token: CancellationToken): Promise { const isRemoteDataFromCurrentMachine = await this.isRemoteDataFromCurrentMachine(remoteUserData); - const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserData, isRemoteDataFromCurrentMachine, token); + const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserData, isRemoteDataFromCurrentMachine, userDataSyncConfiguration, token); const resourcePreviews: IEditableResourcePreview[] = []; for (const resourcePreviewResult of resourcePreviewResults) { @@ -710,11 +710,18 @@ export abstract class AbstractSynchroniser extends Disposable { this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); } + private getUserDataSyncConfiguration(): IUserDataSyncConfiguration { + return this.configurationService.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE); + } + protected abstract readonly version: number; - protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise; + protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration, 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; + + abstract hasLocalData(): Promise; + abstract getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>; } export interface IFileResourcePreview extends IResourcePreview { @@ -753,7 +760,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { try { if (oldContent) { // file exists already - await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent); + await this.writeFileContent(newContent, oldContent, force); } else { // file does not exist await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: force }); @@ -768,6 +775,10 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { } } + protected async writeFileContent(newContent: string, oldContent: IFileContent, force: boolean): Promise { + await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent); + } + private onFileChanges(e: FileChangesEvent): void { if (!e.contains(this.file)) { return; diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index bb97e1543f..7838b4aa59 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -14,7 +14,7 @@ import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; @@ -125,7 +125,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const skippedExtensions: ISyncExtension[] = lastSyncUserData?.skippedExtensions || []; const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData?.syncData ? await parseAndMigrateExtensions(lastSyncUserData.syncData, this.extensionManagementService) : null; - const installedExtensions = await this.extensionManagementService.getInstalled(); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, true); const localExtensions = this.getLocalExtensions(installedExtensions); const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); @@ -375,7 +375,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } // User Extension Sync: Install/Update, Enablement & State - const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier); + const extension = (await this.extensionGalleryService.getExtensions([e.identifier], CancellationToken.None))[0]; /* Update extension state only if * extension is installed and version is same as synced version or @@ -416,12 +416,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } } catch (error) { addToSkipped.push(e); - this.logService.error(error); - this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension`, extension.displayName || extension.identifier.id); + if (error instanceof ExtensionManagementError && error.code === ExtensionManagementErrorCode.Incompatible) { + this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, extension.displayName || extension.identifier.id); + } else { + this.logService.error(error); + this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension`, extension.displayName || extension.identifier.id); + } } } else { - this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, e.identifier.id); addToSkipped.push(e); + this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension because the extension is not found.`, e.identifier.id); } })); } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 7dc70490e8..edf01ba8cb 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -6,6 +6,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; +import { getErrorMessage } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { parse } from 'vs/base/common/json'; import { applyEdits } from 'vs/base/common/jsonEdit'; @@ -98,14 +99,14 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs ); } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean): 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(); + const localGlobalState = await this.getLocalGlobalState(); if (remoteGlobalState) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ui state with local ui state...`); @@ -114,7 +115,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } const storageKeys = this.getStorageKeys(lastSyncGlobalState); - const { local, remote } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, storageKeys, this.logService); + const { local, remote } = merge(localGlobalState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, storageKeys, this.logService); const previewResult: IGlobalStateResourceMergeResult = { content: null, local, @@ -125,8 +126,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return [{ localResource: this.localResource, - localContent: formatAndStringify(localGloablState), - localUserData: localGloablState, + localContent: formatAndStringify(localGlobalState), + localUserData: localGlobalState, remoteResource: this.remoteResource, remoteContent: remoteGlobalState ? formatAndStringify(remoteGlobalState) : null, previewResource: this.previewResource, @@ -291,9 +292,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs private async getLocalArgvContent(): Promise { try { + this.logService.debug('GlobalStateSync#getLocalArgvContent', this.environmentService.argvResource); const content = await this.fileService.readFile(this.environmentService.argvResource); + this.logService.debug('GlobalStateSync#getLocalArgvContent - Resolved', this.environmentService.argvResource); return content.value.toString(); - } catch (error) { } + } catch (error) { + this.logService.debug(getErrorMessage(error)); + } return '{}'; } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index ca96f5ca72..e352b8ce92 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -15,11 +15,12 @@ import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +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 { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; -import { Change, IRemoteUserData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; interface ISyncContent { mac?: string; @@ -36,18 +37,23 @@ interface ILastSyncUserData extends IRemoteUserData { platformSpecific?: boolean; } -export function getKeybindingsContentFromSyncContent(syncContent: string, platformSpecific: boolean): string | null { - const parsed = JSON.parse(syncContent); - if (!platformSpecific) { - return isUndefined(parsed.all) ? null : parsed.all; - } - switch (OS) { - case OperatingSystem.Macintosh: - return isUndefined(parsed.mac) ? null : parsed.mac; - case OperatingSystem.Linux: - return isUndefined(parsed.linux) ? null : parsed.linux; - case OperatingSystem.Windows: - return isUndefined(parsed.windows) ? null : parsed.windows; +export function getKeybindingsContentFromSyncContent(syncContent: string, platformSpecific: boolean, logService: ILogService): string | null { + try { + const parsed = JSON.parse(syncContent); + if (!platformSpecific) { + return isUndefined(parsed.all) ? null : parsed.all; + } + switch (OS) { + case OperatingSystem.Macintosh: + return isUndefined(parsed.mac) ? null : parsed.mac; + case OperatingSystem.Linux: + return isUndefined(parsed.linux) ? null : parsed.linux; + case OperatingSystem.Windows: + return isUndefined(parsed.windows) ? null : parsed.windows; + } + } catch (e) { + logService.error(e); + return null; } } @@ -76,8 +82,8 @@ 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, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { - const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { + const remoteContent = remoteUserData.syncData ? getKeybindingsContentFromSyncContent(remoteUserData.syncData.content, userDataSyncConfiguration.keybindingsPerPlatform ?? this.syncKeybindingsPerPlatform(), this.logService) : 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; @@ -271,7 +277,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (syncData) { switch (this.extUri.basename(uri)) { case 'keybindings.json': - return this.getKeybindingsContentFromSyncContent(syncData.content); + return getKeybindingsContentFromSyncContent(syncData.content, this.syncKeybindingsPerPlatform(), this.logService); } } } @@ -288,16 +294,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return null; } - return this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content); - } - - private getKeybindingsContentFromSyncContent(syncContent: string): string | null { - try { - return getKeybindingsContentFromSyncContent(syncContent, this.syncKeybindingsPerPlatform()); - } catch (e) { - this.logService.error(e); - return null; - } + return getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content, this.syncKeybindingsPerPlatform(), this.logService); } private toSyncContent(keybindingsContent: string, syncContent?: string): string { @@ -327,14 +324,6 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } private syncKeybindingsPerPlatform(): boolean { - let userValue = !!this.configurationService.inspect('settingsSync.keybindingsPerPlatform').userValue; - if (userValue !== undefined) { - return userValue; - } - userValue = !!this.configurationService.inspect('sync.keybindingsPerPlatform').userValue; - if (userValue !== undefined) { - return userValue; - } return !!this.configurationService.getValue('settingsSync.keybindingsPerPlatform'); } @@ -380,7 +369,7 @@ export class KeybindingsInitializer extends AbstractInitializer { private getKeybindingsContentFromSyncContent(syncContent: string): string | null { try { - return getKeybindingsContentFromSyncContent(syncContent, true); + return getKeybindingsContentFromSyncContent(syncContent, true, this.logService); } catch (e) { this.logService.error(e); return null; diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index e0c0f536d2..ba5b4116c4 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -11,15 +11,17 @@ import { Edit } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; +import { IUserConfigurationFileService } from 'vs/platform/configuration/common/userConfigurationFileService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { edit } from 'vs/platform/userDataSync/common/content'; import { getIgnoredSettings, isEmpty, merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; -import { Change, CONFIGURATION_SYNC_STORE_KEY, IRemoteUserData, ISyncData, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, CONFIGURATION_SYNC_STORE_KEY, IRemoteUserData, ISyncData, ISyncResourceHandle, IUserDataManifest, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; interface ISettingsResourcePreview extends IFileResourcePreview { previewResult: IMergeResult; @@ -61,11 +63,23 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IUserConfigurationFileService private readonly userConfigurationFileService: IUserConfigurationFileService, ) { super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } - protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { + async getRemoteUserDataSyncConfiguration(manifest: IUserDataManifest | null): Promise { + const lastSyncUserData = await this.getLastSyncUserData(); + const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData); + const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); + const parser = new ConfigurationModelParser(USER_DATA_SYNC_CONFIGURATION_SCOPE); + if (remoteSettingsSyncContent?.settings) { + parser.parse(remoteSettingsSyncContent.settings); + } + return parser.configurationModel.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE) || {}; + } + + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean): Promise { const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); @@ -313,6 +327,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return getIgnoredSettings(defaultIgnoredSettings, this.configurationService, content); } + protected override async writeFileContent(newContent: string, oldContent: IFileContent, force: boolean): Promise { + await this.userConfigurationFileService.write(VSBuffer.fromString(newContent), force ? undefined : { etag: oldContent.etag, mtime: oldContent.mtime }); + } + private validateContent(content: string): void { if (this.hasErrors(content)) { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index aa7ed2c049..9811c6823b 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -49,7 +49,7 @@ 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, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { + protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean): Promise { const local = await this.getSnippetsFileContents(); const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 1f7a3a2f02..45a7d94daa 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -37,6 +37,14 @@ export function getDefaultIgnoredSettings(): string[] { return distinct([CONFIGURATION_SYNC_STORE_KEY, ...ignoreSyncSettings, ...machineSettings, ...disallowedSettings]); } +export const USER_DATA_SYNC_CONFIGURATION_SCOPE = 'settingsSync'; + +export interface IUserDataSyncConfiguration { + keybindingsPerPlatform?: boolean; + ignoredExtensions?: string[]; + ignoredSettings?: string[]; +} + export function registerConfiguration(): IDisposable { const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -382,7 +390,7 @@ export interface IUserDataSynchroniser { replace(uri: URI): Promise; stop(): Promise; - preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise; + preview(manifest: IUserDataManifest | null, userDataSyncConfiguration: IUserDataSyncConfiguration, headers: IHeaders): Promise; accept(resource: URI, content?: string | null): Promise; merge(resource: URI): Promise; discard(resource: URI): Promise; diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 17b8325f3d..dc40505c19 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -14,6 +14,7 @@ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IHeaders } from 'vs/base/parts/request/common/request'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -22,7 +23,7 @@ import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalS import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync'; -import { Change, createSyncHeaders, IManualSyncTask, IResourcePreview, ISyncResourceHandle, ISyncResourcePreview, ISyncTask, IUserDataManifest, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, createSyncHeaders, IManualSyncTask, IResourcePreview, ISyncResourceHandle, ISyncResourcePreview, ISyncTask, IUserDataManifest, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE } from 'vs/platform/userDataSync/common/userDataSync'; type SyncErrorClassification = { code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -77,6 +78,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IStorageService private readonly storageService: IStorageService, @@ -153,7 +155,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ throw userDataSyncError; } - return new ManualSyncTask(executionId, manifest, syncHeaders, this.synchronisers, this.logService); + return new ManualSyncTask(executionId, manifest, syncHeaders, this.synchronisers, this.configurationService, this.logService); } private recoveredSettings: boolean = false; @@ -445,6 +447,7 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { readonly manifest: IUserDataManifest | null, private readonly syncHeaders: IHeaders, private readonly synchronisers: IUserDataSynchroniser[], + private readonly configurationService: IConfigurationService, private readonly logService: IUserDataSyncLogService, ) { super(); @@ -707,11 +710,12 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> { const result: [SyncResource, ISyncResourcePreview][] = []; + const remoteUserDataSyncConfiguration: IUserDataSyncConfiguration = await this.getUserDataSyncConfiguration(); for (const synchroniser of this.synchronisers) { if (token.isCancellationRequested) { return []; } - const preview = await synchroniser.preview(this.manifest, this.syncHeaders); + const preview = await synchroniser.preview(this.manifest, remoteUserDataSyncConfiguration, this.syncHeaders); if (preview) { result.push(this.toSyncResourcePreview(synchroniser.resource, preview)); } @@ -719,6 +723,12 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { return result; } + private async getUserDataSyncConfiguration(): Promise { + const local = this.configurationService.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE); + const remote = await (this.synchronisers.find(synchronizer => synchronizer instanceof SettingsSynchroniser)).getRemoteUserDataSyncConfiguration(this.manifest); + return { ...local, ...remote }; + } + private toSyncResourcePreview(syncResource: SyncResource, preview: ISyncResourcePreview): [SyncResource, ISyncResourcePreview] { return [ syncResource, diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 2352e87a2b..e02f9c07e0 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -175,7 +175,6 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync const headers: IHeaders = { 'X-Client-Name': `${productService.applicationName}${isWeb ? '-web' : ''}`, 'X-Client-Version': productService.version, - 'X-Machine-Id': uuid }; if (productService.commit) { headers['X-Client-Commit'] = productService.commit; diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index f779e48784..fd1ebadb7f 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -8,6 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { getKeybindingsContentFromSyncContent, KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; import { IUserDataSyncService, IUserDataSyncStoreService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -70,8 +71,8 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), '[]'); - assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), '[]'); + assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), '[]'); + assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), '[]'); assert.strictEqual((await fileService.readFile(keybindingsResource)).value.toString(), ''); }); @@ -95,8 +96,8 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); - assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); + assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), content); assert.strictEqual((await fileService.readFile(keybindingsResource)).value.toString(), content); }); @@ -110,8 +111,8 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), expectedContent); - assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), expectedContent); + assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), expectedContent); + assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), expectedContent); assert.strictEqual((await fileService.readFile(keybindingsResource)).value.toString(), expectedContent); }); @@ -135,8 +136,8 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); - assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); + assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), content); assert.strictEqual((await fileService.readFile(keybindingsResource)).value.toString(), content); }); @@ -159,8 +160,8 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); - assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); + assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), content); + assert.strictEqual(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), content); assert.strictEqual((await fileService.readFile(keybindingsResource)).value.toString(), expectedLocalContent); }); @@ -183,7 +184,7 @@ suite('KeybindingsSync', () => { const remoteUserData = await testObject.getRemoteUserData(null); assert.deepStrictEqual(lastSyncUserData!.ref, remoteUserData.ref); assert.deepStrictEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), '[]'); + assert.strictEqual(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true, client.instantiationService.get(ILogService)), '[]'); }); test('test apply remote when keybindings file does not exist', async () => { @@ -193,7 +194,7 @@ suite('KeybindingsSync', () => { await fileService.del(keybindingsResource); } - const preview = (await testObject.preview(await client.manifest()))!; + const preview = (await testObject.preview(await client.manifest(), {}))!; server.reset(); const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource); diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 59106156e8..831b806c26 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -536,7 +536,7 @@ suite('SettingsSync - Manual', () => { }`; await updateSettings(settingsContent, client); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index 5851b01f62..834cc96e7f 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -677,7 +677,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -703,7 +703,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -730,7 +730,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -757,7 +757,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -786,7 +786,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -813,7 +813,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -846,7 +846,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -881,7 +881,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -911,7 +911,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, @@ -942,7 +942,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - let preview = await testObject.preview(await testClient.manifest()); + let preview = await testObject.preview(await testClient.manifest(), {}); assert.strictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 443c67b2d0..392dddb11b 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -14,7 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; -import { Change, IRemoteUserData, IResourcePreview as IBaseResourcePreview, IUserDataManifest, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { Change, IRemoteUserData, IResourcePreview as IBaseResourcePreview, IUserDataManifest, IUserDataSyncConfiguration, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; interface ITestResourcePreview extends IResourcePreview { @@ -41,7 +41,7 @@ class TestSynchroniser extends AbstractSynchroniser { return super.getLatestRemoteUserData(manifest, lastSyncUserData); } - protected override async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise { + protected override async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise { this.cancelled = false; this.onDoSyncCall.fire(); await this.syncBarrier.wait(); @@ -50,10 +50,10 @@ class TestSynchroniser extends AbstractSynchroniser { return SyncStatus.Idle; } - return super.doSync(remoteUserData, lastSyncUserData, apply); + return super.doSync(remoteUserData, lastSyncUserData, apply, userDataSyncConfiguration); } - protected override async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, token: CancellationToken): Promise { + protected override async generateSyncPreview(remoteUserData: IRemoteUserData): Promise { if (this.syncResult.hasError) { throw new Error('failed'); } @@ -161,6 +161,8 @@ class TestSynchroniser extends AbstractSynchroniser { this.onDidTriggerLocalChangeCall.fire(); } + hasLocalData(): Promise { throw new Error('not implemented'); } + getAssociatedResources(): Promise<{ resource: URI, comparableResource: URI }[]> { throw new Error('not implemented'); } } suite('TestSynchronizer - Auto Sync', () => { @@ -506,7 +508,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - const preview = await testObject.preview(await client.manifest()); + const preview = await testObject.preview(await client.manifest(), {}); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, [testObject.localResource]); @@ -518,7 +520,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); @@ -532,7 +534,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); @@ -546,7 +548,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].localResource); @@ -563,7 +565,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.manifest()); const manifest = await client.manifest(); - let preview = await testObject.preview(manifest); + let preview = await testObject.preview(manifest, {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); @@ -584,7 +586,7 @@ suite('TestSynchronizer - Manual Sync', () => { const manifest = await client.manifest(); const expectedContent = manifest!.latest![testObject.resource]; - let preview = await testObject.preview(manifest); + let preview = await testObject.preview(manifest, {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); @@ -603,7 +605,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.manifest()); const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].localResource); preview = await testObject.apply(false); @@ -621,7 +623,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); @@ -637,7 +639,7 @@ suite('TestSynchronizer - Manual Sync', () => { const manifest = await client.manifest(); const expectedContent = manifest!.latest![testObject.resource]; - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); @@ -654,7 +656,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -669,7 +671,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); @@ -685,7 +687,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -700,7 +702,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); @@ -716,7 +718,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); @@ -732,7 +734,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -750,7 +752,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.manifest()); const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].localResource); @@ -770,7 +772,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.manifest()); const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -792,7 +794,7 @@ suite('TestSynchronizer - Manual Sync', () => { const manifest = await client.manifest(); const expectedContent = manifest!.latest![testObject.resource]; - let preview = await testObject.preview(manifest); + let preview = await testObject.preview(manifest, {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -812,7 +814,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - const preview = await testObject.preview(await client.manifest()); + const preview = await testObject.preview(await client.manifest(), {}); assert.deepStrictEqual(testObject.status, SyncStatus.Syncing); assertPreviews(preview!.resourcePreviews, [testObject.localResource]); @@ -824,7 +826,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); assert.deepStrictEqual(testObject.status, SyncStatus.HasConflicts); @@ -838,7 +840,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - const preview = await testObject.preview(await client.manifest()); + const preview = await testObject.preview(await client.manifest(), {}); await testObject.merge(preview!.resourcePreviews[0].previewResource); await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -853,7 +855,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); await testObject.merge(preview!.resourcePreviews[0].previewResource); const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); @@ -872,7 +874,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; const manifest = await client.manifest(); const expectedContent = manifest!.latest![testObject.resource]; - let preview = await testObject.preview(manifest); + let preview = await testObject.preview(manifest, {}); await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); @@ -891,7 +893,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content); @@ -909,7 +911,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; const manifest = await client.manifest(); const expectedContent = manifest!.latest![testObject.resource]; - let preview = await testObject.preview(manifest); + let preview = await testObject.preview(manifest, {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.apply(false); @@ -927,7 +929,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -942,7 +944,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); @@ -958,7 +960,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -973,7 +975,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); @@ -989,7 +991,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.accept(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); @@ -1005,7 +1007,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource); @@ -1021,7 +1023,7 @@ suite('TestSynchronizer - Manual Sync', () => { testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); @@ -1039,7 +1041,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.manifest()); const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].localResource); @@ -1059,7 +1061,7 @@ suite('TestSynchronizer - Manual Sync', () => { await testObject.sync(await client.manifest()); const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(); - let preview = await testObject.preview(await client.manifest()); + let preview = await testObject.preview(await client.manifest(), {}); preview = await testObject.merge(preview!.resourcePreviews[0].previewResource); preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource); preview = await testObject.discard(preview!.resourcePreviews[0].previewResource); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 3ff3f74ffd..e50fb2b390 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -16,6 +16,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { IUserConfigurationFileService, UserConfigurationFileService } from 'vs/platform/configuration/common/userConfigurationFileService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { DidUninstallExtensionEvent, IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -88,6 +89,7 @@ export class UserDataSyncClient extends Disposable { const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); + this.instantiationService.stub(IUserConfigurationFileService, this.instantiationService.createInstance(UserConfigurationFileService)); this.instantiationService.stub(IRequestService, this.testServer); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index 56fe425fcd..a948f80fcd 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -12,6 +12,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; import { ConfigurationSyncStore } from 'vs/base/common/product'; import { URI } from 'vs/base/common/uri'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; @@ -88,7 +89,6 @@ suite('UserDataSyncStoreService', () => { assert.strictEqual(target.requestsWithAllHeaders.length, 1); assert.strictEqual(target.requestsWithAllHeaders[0].headers!['X-Client-Name'], `${productService.applicationName}${isWeb ? '-web' : ''}`); assert.strictEqual(target.requestsWithAllHeaders[0].headers!['X-Client-Version'], productService.version); - assert.notStrictEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Id'], undefined); assert.notStrictEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined); assert.strictEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); }); @@ -396,19 +396,22 @@ suite('UserDataSyncStoreService', () => { }); test('test donotMakeRequestsUntil is reset after retry time is finished', async () => { - const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 0.25))); - await client.setUp(); - const testObject = client.instantiationService.get(IUserDataSyncStoreService); + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 0.25))); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); - await testObject.manifest(null); - try { await testObject.manifest(null); - } catch (e) { } + try { + await testObject.manifest(null); + assert.fail('should fail'); + } catch (e) { } - const promise = Event.toPromise(testObject.onDidChangeDonotMakeRequestsUntil); - await timeout(300); - await promise; - assert.ok(!testObject.donotMakeRequestsUntil); + const promise = Event.toPromise(testObject.onDidChangeDonotMakeRequestsUntil); + await timeout(300); + await promise; + assert.ok(!testObject.donotMakeRequestsUntil); + }); }); test('test donotMakeRequestsUntil is retrieved', async () => { @@ -426,18 +429,21 @@ suite('UserDataSyncStoreService', () => { }); test('test donotMakeRequestsUntil is checked and reset after retreived', async () => { - const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 0.25))); - await client.setUp(); - const testObject = client.instantiationService.get(IUserDataSyncStoreService); + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 0.25))); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); - await testObject.manifest(null); - try { await testObject.manifest(null); - } catch (e) { } + try { + await testObject.manifest(null); + assert.fail('should fail'); + } catch (e) { } - await timeout(300); - const target = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreService)); - assert.ok(!target.donotMakeRequestsUntil); + await timeout(300); + const target = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreService)); + assert.ok(!target.donotMakeRequestsUntil); + }); }); test('test read resource request handles 304', async () => { @@ -479,16 +485,16 @@ suite('UserDataSyncRequestsSession', () => { }); test('requests are handled after session is expired', async () => { - const testObject = new RequestsSession(1, 500, requestService, new NullLogService()); + const testObject = new RequestsSession(1, 100, requestService, new NullLogService()); await testObject.request('url', {}, CancellationToken.None); - await timeout(600); + await timeout(125); await testObject.request('url', {}, CancellationToken.None); }); test('too many requests are thrown after session is expired', async () => { - const testObject = new RequestsSession(1, 500, requestService, new NullLogService()); + const testObject = new RequestsSession(1, 100, requestService, new NullLogService()); await testObject.request('url', {}, CancellationToken.None); - await timeout(600); + await timeout(125); await testObject.request('url', {}, CancellationToken.None); try { diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 65a6d1d4b2..74dfffbeb1 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -128,13 +128,17 @@ export interface IWindowSettings { readonly clickThroughInactive: boolean; } +interface IWindowBorderColors { + readonly 'window.activeBorder'?: string; + readonly 'window.inactiveBorder'?: string; +} + export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { if (isWeb) { return 'custom'; } const configuration = configurationService.getValue('window'); - if (configuration) { const useNativeTabs = isMacintosh && configuration.nativeTabs === true; if (useNativeTabs) { @@ -146,6 +150,11 @@ export function getTitleBarStyle(configurationService: IConfigurationService): ' return 'native'; // simple fullscreen does not work well with custom title style (https://github.com/microsoft/vscode/issues/63291) } + const colorCustomizations = configurationService.getValue('workbench.colorCustomizations'); + if (colorCustomizations?.['window.activeBorder'] || colorCustomizations?.['window.inactiveBorder']) { + return 'custom'; // window border colors do not work with native title style + } + const style = configuration.titleBarStyle; if (style === 'native' || style === 'custom') { return style; @@ -229,9 +238,6 @@ export interface IColorScheme { export interface IWindowConfiguration { remoteAuthority?: string; - colorScheme: IColorScheme; - autoDetectHighContrast?: boolean; - filesToOpenOrCreate?: IPath[]; filesToDiff?: IPath[]; } @@ -288,6 +294,11 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native fullscreen?: boolean; maximized?: boolean; accessibilitySupport?: boolean; + colorScheme: IColorScheme; + autoDetectHighContrast?: boolean; + + legacyWatcher?: string; // TODO@bpasero remove me once watcher is settled + experimentalSandboxedFileService?: boolean; // TODO@bpasero remove me once sandbox is settled perfMarks: PerformanceMark[]; diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index eda4deab48..65f2cd044d 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -6,6 +6,7 @@ import { app, BrowserWindow, BrowserWindowConstructorOptions, Display, Event, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl, WebFrameMain } from 'electron'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -32,8 +33,8 @@ import { IStorageMainService } from 'vs/platform/storage/electron-main/storageMa import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { getMenuBarVisibility, getTitleBarStyle, INativeWindowConfiguration, IWindowSettings, MenuBarVisibility, WindowMinimumSize, zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; -import { defaultWindowState, ICodeWindow, ILoadEvent, IWindowState, LoadReason, WindowError, WindowMode } from 'vs/platform/windows/electron-main/windows'; +import { getMenuBarVisibility, getTitleBarStyle, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, WindowMinimumSize, zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; +import { defaultWindowState, ICodeWindow, ILoadEvent, IWindowsMainService, IWindowState, LoadReason, OpenContext, WindowError, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; @@ -158,7 +159,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { @IDialogMainService private readonly dialogMainService: IDialogMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IProductService private readonly productService: IProductService, - @IProtocolMainService private readonly protocolMainService: IProtocolMainService + @IProtocolMainService private readonly protocolMainService: IProtocolMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService ) { super(); @@ -302,7 +304,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.createTouchBar(); // Request handling - this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.productService.version, this.environmentMainService, this.fileService, { + this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.productService.version, this.productService, this.environmentMainService, this.configurationService, this.fileService, { get: key => storageMainService.globalStorage.get(key), store: (key, value) => storageMainService.globalStorage.set(key, value) }); @@ -434,7 +436,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@mjbvz: handle webview origin + const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); // But allow them if the are made from inside an webview const isSafeFrame = (requestFrame: WebFrameMain | undefined): boolean => { @@ -453,7 +455,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { 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); + const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme); if (!isSafeResourceUrl) { return callback({ cancel: !isRequestFromSafeContext(details) }); } @@ -617,15 +619,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { cancelId: 1 }, this._win); - if (!this._win) { - return; // Return early if the window has been going down already - } - - if (result.response === 0) { - this._win.webContents.forcefullyCrashRenderer(); // Calling reload() immediately after calling this method will force the reload to occur in a new process - this.reload(); - } else if (result.response === 2) { - this.destroyWindow(); + // Handle choice + if (result.response !== 1 /* keep waiting */) { + const reopen = result.response === 0; + this.destroyWindow(reopen); } } @@ -638,6 +635,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { message = localize('appCrashedDetails', "The window has crashed (reason: '{0}', code: '{1}')", details.reason, details.exitCode ?? ''); } + // Show Dialog const result = await this.dialogMainService.showMessageBox({ title: this.productService.nameLong, type: 'warning', @@ -651,23 +649,50 @@ export class CodeWindow extends Disposable implements ICodeWindow { defaultId: 0 }, this._win); - if (!this._win) { - return; // Return early if the window has been going down already - } - - if (result.response === 0) { - this.reload(); - } else if (result.response === 1) { - this.destroyWindow(); - } + // Handle choice + const reopen = result.response === 0; + this.destroyWindow(reopen); } break; } } - 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 + private destroyWindow(reopen: boolean): void { + + // 'close' event will not be fired on destroy(), so signal crash via explicit event + this._onDidDestroy.fire(); + + // make sure to destroy the window as it has crashed + this._win?.destroy(); + + // ask the windows service to open a new fresh window if specified + if (reopen && this.config) { + + // We have to reconstruct a openable from the current workspace + let workspace: IWorkspaceToOpen | IFolderToOpen | undefined = undefined; + let forceEmpty = undefined; + if (isSingleFolderWorkspaceIdentifier(this.openedWorkspace)) { + workspace = { folderUri: this.openedWorkspace.uri }; + } else if (isWorkspaceIdentifier(this.openedWorkspace)) { + workspace = { workspaceUri: this.openedWorkspace.configPath }; + } else { + forceEmpty = true; + } + + // Delegate to windows service + const [window] = this.windowsMainService.open({ + context: OpenContext.API, + userEnv: this.config.userEnv, + cli: { + ...this.environmentMainService.args, + _: [] // we pass in the workspace to open explicitly via `urisToOpen` + }, + urisToOpen: workspace ? [workspace] : undefined, + forceEmpty, + forceNewWindow: true + }); + window.focus(); + } } private onDidDeleteUntitledWorkspace(workspace: IWorkspaceIdentifier): void { @@ -844,6 +869,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (this.isExtensionDevelopmentHost && cli) { configuration.verbose = cli.verbose; configuration.debugId = cli.debugId; + configuration.extensionEnvironment = cli.extensionEnvironment; configuration['inspect-extensions'] = cli['inspect-extensions']; configuration['inspect-brk-extensions'] = cli['inspect-brk-extensions']; configuration['extensions-dir'] = cli['extensions-dir']; @@ -1290,11 +1316,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { send(channel: string, ...args: any[]): void { if (this._win) { if (this._win.isDestroyed() || this._win.webContents.isDestroyed()) { - this.logService.warn(`Sending IPC message to channel ${channel} for window that is destroyed`); + this.logService.warn(`Sending IPC message to channel '${channel}' for window that is destroyed`); return; } - this._win.webContents.send(channel, ...args); + try { + this._win.webContents.send(channel, ...args); + } catch (error) { + this.logService.warn(`Error sending IPC message to channel '${channel}' of window ${this._id}: ${toErrorMessage(error)}`); + } } } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index d579f46456..ba8f450223 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -204,6 +204,7 @@ export interface IWindowsMainService { open(openConfig: IOpenConfiguration): ICodeWindow[]; openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; + openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void; openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[]; sendToFocused(channel: string, ...args: any[]): void; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 562384e5f7..cf2de0c29b 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -17,9 +17,10 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename, join, normalize, posix } from 'vs/base/common/path'; import { getMarks, mark } from 'vs/base/common/performance'; -import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; import { cwd } from 'vs/base/common/process'; import { extUriBiasedIgnorePathCase, normalizePath, originalFSPath, removeTrailingPathSeparator } from 'vs/base/common/resources'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -231,6 +232,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow, remoteAuthority }); } + openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void { + + // Bring window to front + window.focus(); + + // Handle --wait + this.handleWaitMarkerFile(openConfig, [window]); + } + open(openConfig: IOpenConfiguration): ICodeWindow[] { this.logService.trace('windowsManager#open'); @@ -275,10 +285,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - // When run with --diff, take the files to open as files to diff - // if there are exactly two files provided. - if (openConfig.diffMode && filesToOpen?.filesToOpenOrCreate.length === 2) { - filesToOpen.filesToDiff = filesToOpen.filesToOpenOrCreate; + // When run with --diff, take the first 2 files to open as files to diff + if (openConfig.diffMode && filesToOpen && filesToOpen.filesToOpenOrCreate.length >= 2) { + filesToOpen.filesToDiff = filesToOpen.filesToOpenOrCreate.slice(0, 2); filesToOpen.filesToOpenOrCreate = []; } @@ -371,6 +380,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this.workspacesHistoryMainService.addRecentlyOpened(recents); } + // Handle --wait + this.handleWaitMarkerFile(openConfig, usedWindows); + + return usedWindows; + } + + private handleWaitMarkerFile(openConfig: IOpenConfiguration, usedWindows: ICodeWindow[]): void { + // If we got started with --wait from the CLI, we need to signal to the outside when the window // used for the edit operation is closed or loaded to a different folder so that the waiting // process can continue. We do this by deleting the waitMarkerFilePath. @@ -386,8 +403,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } })(); } - - return usedWindows; } private doOpen( @@ -434,7 +449,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0]; // only look at the windows with correct authority - const windows = this.getWindows().filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); + const windows = this.getWindows().filter(window => filesToOpen && isEqualAuthority(window.remoteAuthority, filesToOpen.remoteAuthority)); // figure out a good window to open the files in if any // with a fallback to the last active window. @@ -493,7 +508,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), workspaceToOpen.workspace.configPath))); if (windowsOnWorkspace.length > 0) { const windowOnWorkspace = windowsOnWorkspace[0]; - const filesToOpenInWindow = (filesToOpen?.remoteAuthority === windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; + const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; // Do open files addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow), !!filesToOpenInWindow); @@ -508,7 +523,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } const remoteAuthority = workspaceToOpen.remoteAuthority; - const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; + const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined; // Do open folder addUsedWindow(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); @@ -525,7 +540,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), folderToOpen.workspace.uri))); if (windowsOnFolderPath.length > 0) { const windowOnFolderPath = windowsOnFolderPath[0]; - const filesToOpenInWindow = filesToOpen?.remoteAuthority === windowOnFolderPath.remoteAuthority ? filesToOpen : undefined; + const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, windowOnFolderPath.remoteAuthority) ? filesToOpen : undefined; // Do open files addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow), !!filesToOpenInWindow); @@ -540,7 +555,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } const remoteAuthority = folderToOpen.remoteAuthority; - const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; + const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined; // Do open folder addUsedWindow(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); @@ -554,7 +569,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (allEmptyToRestore.length > 0) { allEmptyToRestore.forEach(emptyWindowBackupInfo => { const remoteAuthority = emptyWindowBackupInfo.remoteAuthority; - const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; + const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined; addUsedWindow(this.openInBrowserWindow({ userEnv: openConfig.userEnv, @@ -696,7 +711,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)) as ISingleFolderWorkspacePathToOpen[]; if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; - if (foldersToOpen.every(folderToOpen => folderToOpen.remoteAuthority === remoteAuthority)) { // only if all folder have the same authority + if (foldersToOpen.every(folderToOpen => isEqualAuthority(folderToOpen.remoteAuthority, remoteAuthority))) { // only if all folder have the same authority const workspace = this.workspacesManagementMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); // Add workspace and remove folders thereby @@ -959,6 +974,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic try { const pathStat = statSync(path); + + // File if (pathStat.isFile()) { // Workspace (unless disabled via flag) @@ -976,7 +993,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - // File return { fileUri: URI.file(path), selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined, @@ -984,11 +1000,23 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }; } - // Folder (we check for isDirectory() because e.g. paths like /dev/null - // are neither file nor folder but some external tools might pass them - // over to us) + // Folder else if (pathStat.isDirectory()) { - return { workspace: getSingleFolderWorkspaceIdentifier(URI.file(path), pathStat), exists: true }; + return { + workspace: getSingleFolderWorkspaceIdentifier(URI.file(path), pathStat), + exists: true + }; + } + + // Special device: in POSIX environments, we may get /dev/null passed + // in (for example git uses it to signal one side of a diff does not + // exist). In that special case, treat it like a file to support this + // scenario () + else if (!isWindows && path === '/dev/null') { + return { + fileUri: URI.file(path), + exists: true + }; } } catch (error) { const fileUri = URI.file(path); @@ -998,7 +1026,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // assume this is a file that does not yet exist if (options.ignoreFileNotFound) { - return { fileUri, exists: false }; + return { + fileUri, + exists: false + }; } } @@ -1256,6 +1287,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic os: { release: release(), hostname: hostname() }, zoomLevel: typeof windowConfig?.zoomLevel === 'number' ? windowConfig.zoomLevel : undefined, + legacyWatcher: this.configurationService.getValue('files.legacyWatcher'), + experimentalSandboxedFileService: this.configurationService.getValue('files.experimentalSandboxedFileService'), autoDetectHighContrast: windowConfig?.autoDetectHighContrast ?? true, accessibilitySupport: app.accessibilitySupportEnabled, colorScheme: { @@ -1326,6 +1359,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic configuration.verbose = currentWindowConfig.verbose; configuration['inspect-brk-extensions'] = currentWindowConfig['inspect-brk-extensions']; configuration.debugId = currentWindowConfig.debugId; + configuration.extensionEnvironment = currentWindowConfig.extensionEnvironment; configuration['inspect-extensions'] = currentWindowConfig['inspect-extensions']; configuration['extensions-dir'] = currentWindowConfig['extensions-dir']; } @@ -1393,7 +1427,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } private getLastActiveWindowForAuthority(remoteAuthority: string | undefined): ICodeWindow | undefined { - return this.doGetLastActiveWindow(this.getWindows().filter(window => window.remoteAuthority === remoteAuthority)); + return this.doGetLastActiveWindow(this.getWindows().filter(window => isEqualAuthority(window.remoteAuthority, remoteAuthority))); } private doGetLastActiveWindow(windows: ICodeWindow[]): ICodeWindow | undefined { @@ -1443,3 +1477,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return this.getWindowById(browserWindow.id); } } + +function isEqualAuthority(a1: string | undefined, a2: string | undefined) { + return a1 === a2 || (a1 !== undefined && a2 !== undefined && equalsIgnoreCase(a1, a2)); +} diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 1a188ac400..516bcc53d8 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -104,6 +104,13 @@ export interface IWorkspace { */ readonly folders: IWorkspaceFolder[]; + /** + * Transient workspaces are meant to go away after being used + * once, e.g. a window reload of a transient workspace will + * open an empty window. + */ + readonly transient?: boolean; + /** * the location of the workspace configuration */ @@ -162,6 +169,7 @@ export class Workspace implements IWorkspace { constructor( private _id: string, folders: WorkspaceFolder[], + private _transient: boolean, private _configuration: URI | null, private _ignorePathCasing: (key: URI) => boolean, ) { @@ -171,6 +179,7 @@ export class Workspace implements IWorkspace { update(workspace: Workspace) { this._id = workspace.id; this._configuration = workspace.configuration; + this._transient = workspace.transient; this._ignorePathCasing = workspace._ignorePathCasing; this.folders = workspace.folders; } @@ -188,6 +197,10 @@ export class Workspace implements IWorkspace { return this._id; } + get transient(): boolean { + return this._transient; + } + get configuration(): URI | null { return this._configuration; } @@ -216,7 +229,7 @@ export class Workspace implements IWorkspace { } toJSON(): IWorkspace { - return { id: this.id, folders: this.folders, configuration: this.configuration }; + return { id: this.id, folders: this.folders, transient: this.transient, configuration: this.configuration }; } } diff --git a/src/vs/platform/workspace/test/common/testWorkspace.ts b/src/vs/platform/workspace/test/common/testWorkspace.ts index fd16ad36a1..a354ec3e52 100644 --- a/src/vs/platform/workspace/test/common/testWorkspace.ts +++ b/src/vs/platform/workspace/test/common/testWorkspace.ts @@ -14,7 +14,7 @@ export class Workspace extends BaseWorkspace { configuration: URI | null = null, ignorePathCasing: (key: URI) => boolean = () => !isLinux ) { - super(id, folders, configuration, ignorePathCasing); + super(id, folders, false, configuration, ignorePathCasing); } } diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index 765f20f44d..b370ca0cab 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -28,7 +28,7 @@ suite('Workspace', () => { test('getFolder returns the folder with given uri', () => { const expected = new WorkspaceFolder({ uri: testFolderUri, name: '', index: 2 }); - let testObject = new Workspace('', [new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 0 }), expected, new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })], null, () => !isLinux); + let testObject = new Workspace('', [new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 0 }), expected, new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })], false, null, () => !isLinux); const actual = testObject.getFolder(expected.uri); @@ -37,7 +37,7 @@ suite('Workspace', () => { test('getFolder returns the folder if the uri is sub', () => { const expected = new WorkspaceFolder({ uri: testFolderUri, name: '', index: 0 }); - let testObject = new Workspace('', [expected, new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 1 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })], null, () => !isLinux); + let testObject = new Workspace('', [expected, new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 1 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })], false, null, () => !isLinux); const actual = testObject.getFolder(URI.file(join(fileFolder, 'test/a'))); @@ -46,7 +46,7 @@ suite('Workspace', () => { test('getFolder returns the closest folder if the uri is sub', () => { const expected = new WorkspaceFolder({ uri: testFolderUri, name: '', index: 2 }); - let testObject = new Workspace('', [new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 }), expected], null, () => !isLinux); + let testObject = new Workspace('', [new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 }), expected], false, null, () => !isLinux); const actual = testObject.getFolder(URI.file(join(fileFolder, 'test/a'))); @@ -55,7 +55,7 @@ suite('Workspace', () => { test('getFolder returns the folder even if the uri has query path', () => { const expected = new WorkspaceFolder({ uri: testFolderUri, name: '', index: 2 }); - let testObject = new Workspace('', [new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 }), expected], null, () => !isLinux); + let testObject = new Workspace('', [new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 }), expected], false, null, () => !isLinux); const actual = testObject.getFolder(URI.file(join(fileFolder, 'test/a')).with({ query: 'somequery' })); @@ -63,7 +63,7 @@ suite('Workspace', () => { }); test('getFolder returns null if the uri is not sub', () => { - let testObject = new Workspace('', [new WorkspaceFolder({ uri: testFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 })], null, () => !isLinux); + let testObject = new Workspace('', [new WorkspaceFolder({ uri: testFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 })], false, null, () => !isLinux); const actual = testObject.getFolder(URI.file(join(fileFolder, 'main/a'))); diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 3bfe63ab56..1d3b3c9871 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -547,6 +547,7 @@ export function toStoreData(recents: IRecentlyOpened): RecentlyOpenedStorageData for (const recent of recents.files) { serialized.entries.push({ fileUri: recent.fileUri.toString(), label: recent.label, remoteAuthority: recent.remoteAuthority }); } + return serialized; } diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index d2047eb907..611a5d77b5 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -41,11 +41,13 @@ export interface IWorkspacesHistoryMainService { export class WorkspacesHistoryMainService extends Disposable implements IWorkspacesHistoryMainService { - private static readonly MAX_TOTAL_RECENT_ENTRIES = 100; + private static readonly MAX_TOTAL_RECENT_ENTRIES = 500; private static readonly MAX_MACOS_DOCK_RECENT_WORKSPACES = 7; // prefer higher number of workspaces... private static readonly MAX_MACOS_DOCK_RECENT_ENTRIES_TOTAL = 10; // ...over number of files + private static readonly MAX_WINDOWS_JUMP_LIST_ENTRIES = 7; + // Exclude some very common files from the dock/taskbar private static readonly COMMON_FILES_FILTER = [ 'COMMIT_EDITMSG', @@ -98,21 +100,21 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Workspace if (isRecentWorkspace(recent)) { - if (!this.workspacesManagementMainService.isUntitledWorkspace(recent.workspace) && indexOfWorkspace(workspaces, recent.workspace) === -1) { + if (!this.workspacesManagementMainService.isUntitledWorkspace(recent.workspace) && this.indexOfWorkspace(workspaces, recent.workspace) === -1) { workspaces.push(recent); } } // Folder else if (isRecentFolder(recent)) { - if (indexOfFolder(workspaces, recent.folderUri) === -1) { + if (this.indexOfFolder(workspaces, recent.folderUri) === -1) { workspaces.push(recent); } } // File else { - const alreadyExistsInHistory = indexOfFile(files, recent.fileUri) >= 0; + const alreadyExistsInHistory = this.indexOfFile(files, recent.fileUri) >= 0; const shouldBeFiltered = recent.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(recent.fileUri)) >= 0; if (!alreadyExistsInHistory && !shouldBeFiltered) { @@ -147,7 +149,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa removeRecentlyOpened(recentToRemove: URI[]): void { const keep = (recent: IRecent) => { - const uri = location(recent); + const uri = this.location(recent); for (const resourceToRemove of recentToRemove) { if (extUriBiasedIgnorePathCase.isEqual(resourceToRemove, uri)) { return false; @@ -187,7 +189,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa const workspaceEntries: string[] = []; let entries = 0; for (let i = 0; i < mru.workspaces.length && entries < WorkspacesHistoryMainService.MAX_MACOS_DOCK_RECENT_WORKSPACES; i++) { - const loc = location(mru.workspaces[i]); + const loc = this.location(mru.workspaces[i]); if (loc.scheme === Schemas.file) { const workspacePath = originalFSPath(loc); if (await Promises.exists(workspacePath)) { @@ -200,7 +202,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Collect max-N recent files that are known to exist const fileEntries: string[] = []; for (let i = 0; i < mru.files.length && entries < WorkspacesHistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES_TOTAL; i++) { - const loc = location(mru.files[i]); + const loc = this.location(mru.files[i]); if (loc.scheme === Schemas.file) { const filePath = originalFSPath(loc); if ( @@ -256,7 +258,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa if (currentFiles) { for (let currentFile of currentFiles) { const fileUri = currentFile.fileUri; - if (fileUri && indexOfFile(files, fileUri) === -1) { + if (fileUri && this.indexOfFile(files, fileUri) === -1) { files.push({ fileUri }); } } @@ -272,7 +274,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Get from storage let recents = this.getRecentlyOpenedFromStorage(); for (let recent of recents.workspaces) { - let index = isRecentFolder(recent) ? indexOfFolder(workspaces, recent.folderUri) : indexOfWorkspace(workspaces, recent.workspace); + let index = isRecentFolder(recent) ? this.indexOfFolder(workspaces, recent.folderUri) : this.indexOfWorkspace(workspaces, recent.workspace); if (index >= 0) { workspaces[index].label = workspaces[index].label || recent.label; } else { @@ -281,7 +283,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } for (let recent of recents.files) { - let index = indexOfFile(files, recent.fileUri); + let index = this.indexOfFile(files, recent.fileUri); if (index >= 0) { files[index].label = files[index].label || recent.label; } else { @@ -346,7 +348,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Add entries let hasWorkspaces = false; - const items: JumpListItem[] = coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { + const items: JumpListItem[] = coalesce(this.getRecentlyOpened().workspaces.slice(0, WorkspacesHistoryMainService.MAX_WINDOWS_JUMP_LIST_ENTRIES).map(recent => { const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; const { title, description } = this.getWindowsJumpListLabel(workspace, recent.label); @@ -399,7 +401,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Single Folder if (URI.isUri(workspace)) { - return { title: basename(workspace), description: renderJumpListPathDescription(workspace) }; + return { title: basename(workspace), description: this.renderJumpListPathDescription(workspace) }; } // Workspace: Untitled @@ -413,34 +415,34 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } - return { title: localize('workspaceName', "{0} (Workspace)", filename), description: renderJumpListPathDescription(workspace.configPath) }; - } -} - -function renderJumpListPathDescription(uri: URI) { - return uri.scheme === 'file' ? normalizeDriveLetter(uri.fsPath) : uri.toString(); -} - -function location(recent: IRecent): URI { - if (isRecentFolder(recent)) { - return recent.folderUri; + return { title: localize('workspaceName', "{0} (Workspace)", filename), description: this.renderJumpListPathDescription(workspace.configPath) }; } - if (isRecentFile(recent)) { - return recent.fileUri; + private renderJumpListPathDescription(uri: URI) { + return uri.scheme === 'file' ? normalizeDriveLetter(uri.fsPath) : uri.toString(); } - return recent.workspace.configPath; -} + private location(recent: IRecent): URI { + if (isRecentFolder(recent)) { + return recent.folderUri; + } -function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): number { - return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id); -} + if (isRecentFile(recent)) { + return recent.fileUri; + } -function indexOfFolder(arr: IRecent[], candidate: URI): number { - return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate)); -} + return recent.workspace.configPath; + } -function indexOfFile(arr: IRecentFile[], candidate: URI): number { - return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate)); + private indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): number { + return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id); + } + + private indexOfFolder(arr: IRecent[], candidate: URI): number { + return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate)); + } + + private indexOfFile(arr: IRecentFile[], candidate: URI): number { + return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate)); + } } diff --git a/src/vs/server/cli.js b/src/vs/server/cli.js new file mode 100644 index 0000000000..40ab15157e --- /dev/null +++ b/src/vs/server/cli.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 + +const path = require('path'); + +// Keep bootstrap-amd.js from redefining 'fs'. +delete process.env['ELECTRON_RUN_AS_NODE']; + +// Set default remote native node modules path, if unset +process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] = process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] || path.join(__dirname, '..', '..', '..', 'remote', 'node_modules'); + +require('../../bootstrap-node').injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); +require('../../bootstrap-amd').load('vs/server/remoteCli'); diff --git a/src/vs/server/extensionHostConnection.ts b/src/vs/server/extensionHostConnection.ts new file mode 100644 index 0000000000..4dc7bcc721 --- /dev/null +++ b/src/vs/server/extensionHostConnection.ts @@ -0,0 +1,272 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as net from 'net'; +import { getNLSConfiguration } from 'vs/server/remoteLanguagePacks'; +import { uriTransformerPath } from 'vs/server/remoteUriTransformer'; +import { FileAccess } from 'vs/base/common/network'; +import { join, delimiter } from 'vs/base/common/path'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IRemoteConsoleLog } from 'vs/base/common/console'; +import { Emitter, Event } from 'vs/base/common/event'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; +import { removeDangerousEnvVariables } from 'vs/base/node/processes'; + +export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, language: string, isDebug: boolean, environmentService: IServerEnvironmentService, logService: ILogService): Promise { + const nlsConfig = await getNLSConfiguration(language, environmentService.userDataPath); + + let userShellEnv: typeof process.env | undefined = undefined; + try { + userShellEnv = await resolveShellEnv(logService, environmentService.args, process.env); + } catch (error) { + logService.error('ExtensionHostConnection#buildUserEnvironment resolving shell environment failed', error); + userShellEnv = {}; + } + + const binFolder = environmentService.isBuilt ? join(environmentService.appRoot, 'bin') : join(environmentService.appRoot, 'resources', 'server', 'bin-dev'); + const processEnv = process.env; + let PATH = startParamsEnv['PATH'] || (userShellEnv ? userShellEnv['PATH'] : undefined) || processEnv['PATH']; + if (PATH) { + PATH = binFolder + delimiter + PATH; + } else { + PATH = binFolder; + } + + const env: IProcessEnvironment = { + ...processEnv, + ...userShellEnv, + ...{ + VSCODE_LOG_NATIVE: String(isDebug), + VSCODE_AMD_ENTRYPOINT: 'vs/server/remoteExtensionHostProcess', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true', + VSCODE_EXTHOST_WILL_SEND_SOCKET: 'true', + VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true', + VSCODE_LOG_STACK: 'false', + VSCODE_NLS_CONFIG: JSON.stringify(nlsConfig, undefined, 0) + }, + ...startParamsEnv + }; + if (!environmentService.args['without-browser-env-var']) { + env.BROWSER = join(binFolder, 'helpers', isWindows ? 'browser.cmd' : 'browser.sh'); + } + + setCaseInsensitive(env, 'PATH', PATH); + removeNulls(env); + + return env; +} + +class ConnectionData { + constructor( + public readonly socket: net.Socket, + public readonly socketDrain: Promise, + public readonly initialDataChunk: VSBuffer, + public readonly skipWebSocketFrames: boolean, + public readonly permessageDeflate: boolean, + public readonly inflateBytes: VSBuffer, + ) { } + + public toIExtHostSocketMessage(): IExtHostSocketMessage { + return { + type: 'VSCODE_EXTHOST_IPC_SOCKET', + initialDataChunk: (this.initialDataChunk.buffer).toString('base64'), + skipWebSocketFrames: this.skipWebSocketFrames, + permessageDeflate: this.permessageDeflate, + inflateBytes: (this.inflateBytes.buffer).toString('base64'), + }; + } +} + +export class ExtensionHostConnection { + + private _onClose = new Emitter(); + readonly onClose: Event = this._onClose.event; + + private _disposed: boolean; + private _remoteAddress: string; + private _extensionHostProcess: cp.ChildProcess | null; + private _connectionData: ConnectionData | null; + + constructor( + private readonly _environmentService: IServerEnvironmentService, + private readonly _logService: ILogService, + private readonly _reconnectionToken: string, + remoteAddress: string, + socket: NodeSocket | WebSocketNodeSocket, + initialDataChunk: VSBuffer + ) { + this._disposed = false; + this._remoteAddress = remoteAddress; + this._extensionHostProcess = null; + this._connectionData = ExtensionHostConnection._toConnectionData(socket, initialDataChunk); + this._connectionData.socket.pause(); + + this._log(`New connection established.`); + } + + private get _logPrefix(): string { + return `[${this._remoteAddress}][${this._reconnectionToken.substr(0, 8)}][ExtensionHostConnection] `; + } + + private _log(_str: string): void { + this._logService.info(`${this._logPrefix}${_str}`); + } + + private _logError(_str: string): void { + this._logService.error(`${this._logPrefix}${_str}`); + } + + private static _toConnectionData(socket: NodeSocket | WebSocketNodeSocket, initialDataChunk: VSBuffer): ConnectionData { + if (socket instanceof NodeSocket) { + return new ConnectionData(socket.socket, socket.drain(), initialDataChunk, true, false, VSBuffer.alloc(0)); + } else { + return new ConnectionData(socket.socket.socket, socket.drain(), initialDataChunk, false, socket.permessageDeflate, socket.recordedInflateBytes); + } + } + + private async _sendSocketToExtensionHost(extensionHostProcess: cp.ChildProcess, connectionData: ConnectionData): Promise { + // Make sure all outstanding writes have been drained before sending the socket + await connectionData.socketDrain; + const msg = connectionData.toIExtHostSocketMessage(); + extensionHostProcess.send(msg, connectionData.socket); + } + + public shortenReconnectionGraceTimeIfNecessary(): void { + if (!this._extensionHostProcess) { + return; + } + const msg: IExtHostReduceGraceTimeMessage = { + type: 'VSCODE_EXTHOST_IPC_REDUCE_GRACE_TIME' + }; + this._extensionHostProcess.send(msg); + } + + public acceptReconnection(remoteAddress: string, _socket: NodeSocket | WebSocketNodeSocket, initialDataChunk: VSBuffer): void { + this._remoteAddress = remoteAddress; + this._log(`The client has reconnected.`); + const connectionData = ExtensionHostConnection._toConnectionData(_socket, initialDataChunk); + connectionData.socket.pause(); + + if (!this._extensionHostProcess) { + // The extension host didn't even start up yet + this._connectionData = connectionData; + return; + } + + this._sendSocketToExtensionHost(this._extensionHostProcess, connectionData); + } + + private _cleanResources(): void { + if (this._disposed) { + // already called + return; + } + this._disposed = true; + if (this._connectionData) { + this._connectionData.socket.end(); + this._connectionData = null; + } + if (this._extensionHostProcess) { + this._extensionHostProcess.kill(); + this._extensionHostProcess = null; + } + this._onClose.fire(undefined); + } + + public async start(startParams: IRemoteExtensionHostStartParams): Promise { + try { + let execArgv: string[] = []; + if (startParams.port && !(process).pkg) { + execArgv = [`--inspect${startParams.break ? '-brk' : ''}=0.0.0.0:${startParams.port}`]; + } + + const env = await buildUserEnvironment(startParams.env, startParams.language, !!startParams.debugId, this._environmentService, this._logService); + removeDangerousEnvVariables(env); + + const opts = { + env, + execArgv, + silent: true + }; + + // Run Extension Host as fork of current process + const args = ['--type=extensionHost', `--uriTransformerPath=${uriTransformerPath}`]; + const useHostProxy = this._environmentService.args['use-host-proxy']; + if (useHostProxy !== undefined) { + args.push(`--useHostProxy=${useHostProxy}`); + } + this._extensionHostProcess = cp.fork(FileAccess.asFileUri('bootstrap-fork', require).fsPath, args, opts); + const pid = this._extensionHostProcess.pid; + this._log(`<${pid}> Launched Extension Host Process.`); + + // Catch all output coming from the extension host process + this._extensionHostProcess.stdout!.setEncoding('utf8'); + this._extensionHostProcess.stderr!.setEncoding('utf8'); + const onStdout = Event.fromNodeEventEmitter(this._extensionHostProcess.stdout!, 'data'); + const onStderr = Event.fromNodeEventEmitter(this._extensionHostProcess.stderr!, 'data'); + onStdout((e) => this._log(`<${pid}> ${e}`)); + onStderr((e) => this._log(`<${pid}> ${e}`)); + + + // Support logging from extension host + this._extensionHostProcess.on('message', msg => { + if (msg && (msg).type === '__$console') { + logRemoteEntry(this._logService, (msg), `${this._logPrefix}<${pid}>`); + } + }); + + // Lifecycle + this._extensionHostProcess.on('error', (err) => { + this._logError(`<${pid}> Extension Host Process had an error`); + this._logService.error(err); + this._cleanResources(); + }); + + this._extensionHostProcess.on('exit', (code: number, signal: string) => { + this._log(`<${pid}> Extension Host Process exited with code: ${code}, signal: ${signal}.`); + this._cleanResources(); + }); + + const messageListener = (msg: IExtHostReadyMessage) => { + if (msg.type === 'VSCODE_EXTHOST_IPC_READY') { + this._extensionHostProcess!.removeListener('message', messageListener); + this._sendSocketToExtensionHost(this._extensionHostProcess!, this._connectionData!); + this._connectionData = null; + } + }; + this._extensionHostProcess.on('message', messageListener); + + } catch (error) { + console.error('ExtensionHostConnection errored'); + if (error) { + console.error(error); + } + } + } +} + +function setCaseInsensitive(env: { [key: string]: unknown }, key: string, value: string): void { + const pathKeys = Object.keys(env).filter(k => k.toLowerCase() === key.toLowerCase()); + const pathKey = pathKeys.length > 0 ? pathKeys[0] : key; + env[pathKey] = value; +} + +function removeNulls(env: { [key: string]: unknown | null }): void { + // Don't delete while iterating the object itself + for (let key of Object.keys(env)) { + if (env[key] === null) { + delete env[key]; + } + } +} diff --git a/src/vs/server/main.js b/src/vs/server/main.js new file mode 100644 index 0000000000..f8a8a3dfb7 --- /dev/null +++ b/src/vs/server/main.js @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check + +const perf = require('../base/common/performance'); +const performance = require('perf_hooks').performance; +const product = require('../../../product.json'); + +perf.mark('code/server/start'); +// @ts-ignore +global.vscodeServerStartTime = performance.now(); + +function start() { + if (process.argv[2] === '--exec') { + process.argv.splice(1, 2); + require(process.argv[1]); + return; + } + + const minimist = require('minimist'); + + // Do a quick parse to determine if a server or the cli needs to be started + const parsedArgs = minimist(process.argv.slice(2), { + boolean: ['start-server', 'list-extensions', 'print-ip-address'], + string: ['install-extension', 'install-builtin-extension', 'uninstall-extension', 'locate-extension', 'socket-path', 'host', 'port'] + }); + + const shouldSpawnCli = ( + !parsedArgs['start-server'] && + (!!parsedArgs['list-extensions'] || !!parsedArgs['install-extension'] || !!parsedArgs['install-builtin-extension'] || !!parsedArgs['uninstall-extension'] || !!parsedArgs['locate-extension']) + ); + + if (shouldSpawnCli) { + loadCode().then((mod) => { + mod.spawnCli(); + }); + return; + } + + /** + * @typedef { import('./remoteExtensionHostAgentServer').IServerAPI } IServerAPI + */ + /** @type {IServerAPI | null} */ + let _remoteExtensionHostAgentServer = null; + /** @type {Promise | null} */ + let _remoteExtensionHostAgentServerPromise = null; + /** @returns {Promise} */ + const getRemoteExtensionHostAgentServer = () => { + if (!_remoteExtensionHostAgentServerPromise) { + _remoteExtensionHostAgentServerPromise = loadCode().then((mod) => mod.createServer(address)); + } + return _remoteExtensionHostAgentServerPromise; + }; + + const http = require('http'); + const os = require('os'); + + let firstRequest = true; + let firstWebSocket = true; + + /** @type {string | import('net').AddressInfo | null} */ + let address = null; + const server = http.createServer(async (req, res) => { + if (firstRequest) { + firstRequest = false; + perf.mark('code/server/firstRequest'); + } + const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer(); + return remoteExtensionHostAgentServer.handleRequest(req, res); + }); + server.on('upgrade', async (req, socket) => { + if (firstWebSocket) { + firstWebSocket = false; + perf.mark('code/server/firstWebSocket'); + } + const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer(); + // @ts-ignore + return remoteExtensionHostAgentServer.handleUpgrade(req, socket); + }); + server.on('error', async (err) => { + const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer(); + return remoteExtensionHostAgentServer.handleServerError(err); + }); + const nodeListenOptions = ( + parsedArgs['socket-path'] + ? { path: parsedArgs['socket-path'] } + : { host: parsedArgs['host'], port: parsePort(parsedArgs['port']) } + ); + server.listen(nodeListenOptions, async () => { + const serverGreeting = product.serverGreeting.join('\n'); + let output = serverGreeting ? `\n\n${serverGreeting}\n\n` : ``; + + if (typeof nodeListenOptions.port === 'number' && parsedArgs['print-ip-address']) { + const ifaces = os.networkInterfaces(); + Object.keys(ifaces).forEach(function (ifname) { + ifaces[ifname].forEach(function (iface) { + if (!iface.internal && iface.family === 'IPv4') { + output += `IP Address: ${iface.address}\n`; + } + }); + }); + } + + address = server.address(); + if (address === null) { + throw new Error('Unexpected server address'); + } + + // Do not change this line. VS Code looks for this in the output. + output += `Extension host agent listening on ${typeof address === 'string' ? address : address.port}\n`; + console.log(output); + + perf.mark('code/server/started'); + // @ts-ignore + global.vscodeServerListenTime = performance.now(); + + await getRemoteExtensionHostAgentServer(); + }); + + process.on('exit', () => { + server.close(); + if (_remoteExtensionHostAgentServer) { + _remoteExtensionHostAgentServer.dispose(); + } + }); +} + +/** + * @param {string | undefined} strPort + * @returns {number} + */ +function parsePort(strPort) { + try { + if (strPort) { + return parseInt(strPort); + } + } catch (e) { + console.log('Port is not a number, using 8000 instead.'); + } + return 8000; +} + +/** @returns { Promise } */ +function loadCode() { + return new Promise((resolve, reject) => { + const path = require('path'); + + // Set default remote native node modules path, if unset + process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] = process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] || path.join(__dirname, '..', '..', '..', 'remote', 'node_modules'); + require('../../bootstrap-node').injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); + require('../../bootstrap-amd').load('vs/server/remoteExtensionHostAgent', resolve, reject); + }); +} + +start(); diff --git a/src/vs/server/remoteAgentEnvironmentImpl.ts b/src/vs/server/remoteAgentEnvironmentImpl.ts new file mode 100644 index 0000000000..eae200ece6 --- /dev/null +++ b/src/vs/server/remoteAgentEnvironmentImpl.ts @@ -0,0 +1,508 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as platform from 'vs/base/common/platform'; +import * as performance from 'vs/base/common/performance'; +import { URI } from 'vs/base/common/uri'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { IRemoteAgentEnvironmentDTO, IGetEnvironmentDataArguments, IScanExtensionsArguments, IScanSingleExtensionArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; +import * as nls from 'vs/nls'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import { ExtensionScanner, ExtensionScannerInput, IExtensionResolver, IExtensionReference } from 'vs/workbench/services/extensions/node/extensionPoints'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; +import { ILogService } from 'vs/platform/log/common/log'; +import { getNLSConfiguration, InternalNLSConfiguration } from 'vs/server/remoteLanguagePacks'; +import { ContextKeyExpr, ContextKeyDefinedExpr, ContextKeyNotExpr, ContextKeyEqualsExpr, ContextKeyNotEqualsExpr, ContextKeyRegexExpr, IContextKeyExprMapper, ContextKeyExpression, ContextKeyInExpr, ContextKeyGreaterExpr, ContextKeyGreaterEqualsExpr, ContextKeySmallerExpr, ContextKeySmallerEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; +import { listProcesses } from 'vs/base/node/ps'; +import { getMachineInfo, collectWorkspaceStats } from 'vs/platform/diagnostics/node/diagnosticsService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { basename, isAbsolute, join, normalize } from 'vs/base/common/path'; +import { ProcessItem } from 'vs/base/common/processes'; +import { ILog, Translations } from 'vs/workbench/services/extensions/common/extensionPoints'; +import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IBuiltInExtension } from 'vs/base/common/product'; +import { IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { cwd } from 'vs/base/common/process'; +import { IRemoteTelemetryService } from 'vs/server/remoteTelemetryService'; +import { Promises } from 'vs/base/node/pfs'; +import { IProductService } from 'vs/platform/product/common/productService'; + +let _SystemExtensionsRoot: string | null = null; +function getSystemExtensionsRoot(): string { + if (!_SystemExtensionsRoot) { + _SystemExtensionsRoot = normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); + } + return _SystemExtensionsRoot; +} +let _ExtraDevSystemExtensionsRoot: string | null = null; +function getExtraDevSystemExtensionsRoot(): string { + if (!_ExtraDevSystemExtensionsRoot) { + _ExtraDevSystemExtensionsRoot = normalize(join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')); + } + return _ExtraDevSystemExtensionsRoot; +} + +export class RemoteAgentEnvironmentChannel implements IServerChannel { + + private static _namePool = 1; + private readonly _logger: ILog; + + private readonly whenExtensionsReady: Promise; + + constructor( + private readonly _connectionToken: string, + private readonly environmentService: IServerEnvironmentService, + extensionManagementCLIService: IExtensionManagementCLIService, + private readonly logService: ILogService, + private readonly telemetryService: IRemoteTelemetryService, + private readonly telemetryAppender: ITelemetryAppender | null, + private readonly productService: IProductService + ) { + this._logger = new class implements ILog { + public error(source: string, message: string): void { + logService.error(source, message); + } + public warn(source: string, message: string): void { + logService.warn(source, message); + } + public info(source: string, message: string): void { + logService.info(source, message); + } + }; + + if (environmentService.args['install-builtin-extension']) { + this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], environmentService.args['install-builtin-extension'], !!environmentService.args['do-not-sync'], !!environmentService.args['force']) + .then(null, error => { + logService.error(error); + }); + } else { + this.whenExtensionsReady = Promise.resolve(); + } + + const extensionsToInstall = environmentService.args['install-extension']; + if (extensionsToInstall) { + const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); + this.whenExtensionsReady + .then(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], !!environmentService.args['do-not-sync'], !!environmentService.args['force'])) + .then(null, error => { + logService.error(error); + }); + } + } + + async call(_: any, command: string, arg?: any): Promise { + switch (command) { + case 'disableTelemetry': { + this.telemetryService.permanentlyDisableTelemetry(); + return; + } + + case 'getEnvironmentData': { + const args = arg; + const uriTransformer = createRemoteURITransformer(args.remoteAuthority); + + let environmentData = await this._getEnvironmentData(); + environmentData = transformOutgoingURIs(environmentData, uriTransformer); + + return environmentData; + } + + case 'whenExtensionsReady': { + await this.whenExtensionsReady; + return; + } + + case 'scanExtensions': { + await this.whenExtensionsReady; + const args = arg; + const language = args.language; + this.logService.trace(`Scanning extensions using UI language: ${language}`); + const uriTransformer = createRemoteURITransformer(args.remoteAuthority); + + const extensionDevelopmentLocations = args.extensionDevelopmentPath && args.extensionDevelopmentPath.map(url => URI.revive(uriTransformer.transformIncoming(url))); + const extensionDevelopmentPath = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined; + + let extensions = await this._scanExtensions(language, extensionDevelopmentPath); + extensions = transformOutgoingURIs(extensions, uriTransformer); + + this.logService.trace('Scanned Extensions', extensions); + RemoteAgentEnvironmentChannel._massageWhenConditions(extensions); + + return extensions; + } + + case 'scanSingleExtension': { + await this.whenExtensionsReady; + const args = arg; + const language = args.language; + const isBuiltin = args.isBuiltin; + const uriTransformer = createRemoteURITransformer(args.remoteAuthority); + const extensionLocation = URI.revive(uriTransformer.transformIncoming(args.extensionLocation)); + const extensionPath = extensionLocation.scheme === Schemas.file ? extensionLocation.fsPath : null; + + if (!extensionPath) { + return null; + } + + const translations = await this._getTranslations(language); + let extension = await this._scanSingleExtension(extensionPath, isBuiltin, language, translations); + + if (!extension) { + return null; + } + + extension = transformOutgoingURIs(extension, uriTransformer); + + RemoteAgentEnvironmentChannel._massageWhenConditions([extension]); + + return extension; + } + + case 'getDiagnosticInfo': { + const options = arg; + const diagnosticInfo: IDiagnosticInfo = { + machineInfo: getMachineInfo() + }; + + const processesPromise: Promise = options.includeProcesses ? listProcesses(process.pid) : Promise.resolve(); + + let workspaceMetadataPromises: Promise[] = []; + const workspaceMetadata: { [key: string]: any } = {}; + if (options.folders) { + // only incoming paths are transformed, so remote authority is unneeded. + const uriTransformer = createRemoteURITransformer(''); + const folderPaths = options.folders + .map(folder => URI.revive(uriTransformer.transformIncoming(folder))) + .filter(uri => uri.scheme === 'file'); + + workspaceMetadataPromises = folderPaths.map(folder => { + return collectWorkspaceStats(folder.fsPath, ['node_modules', '.git']) + .then(stats => { + workspaceMetadata[basename(folder.fsPath)] = stats; + }); + }); + } + + return Promise.all([processesPromise, ...workspaceMetadataPromises]).then(([processes, _]) => { + diagnosticInfo.processes = processes || undefined; + diagnosticInfo.workspaceMetadata = options.folders ? workspaceMetadata : undefined; + return diagnosticInfo; + }); + } + + case 'logTelemetry': { + const { eventName, data } = arg; + // Logging is done directly to the appender instead of through the telemetry service + // as the data sent from the client has already had common properties added to it and + // has already been sent to the telemetry output channel + if (this.telemetryAppender) { + return this.telemetryAppender.log(eventName, data); + } + + return Promise.resolve(); + } + + case 'flushTelemetry': { + if (this.telemetryAppender) { + return this.telemetryAppender.flush(); + } + + return Promise.resolve(); + } + } + + throw new Error(`IPC Command ${command} not found`); + } + + listen(_: any, event: string, arg: any): Event { + throw new Error('Not supported'); + } + + private static _massageWhenConditions(extensions: IExtensionDescription[]): void { + // Massage "when" conditions which mention `resourceScheme` + + interface WhenUser { when?: string; } + + interface LocWhenUser { [loc: string]: WhenUser[]; } + + const _mapResourceSchemeValue = (value: string, isRegex: boolean): string => { + // console.log(`_mapResourceSchemeValue: ${value}, ${isRegex}`); + return value.replace(/file/g, 'vscode-remote'); + }; + + const _mapResourceRegExpValue = (value: RegExp): RegExp => { + let flags = ''; + flags += value.global ? 'g' : ''; + flags += value.ignoreCase ? 'i' : ''; + flags += value.multiline ? 'm' : ''; + return new RegExp(_mapResourceSchemeValue(value.source, true), flags); + }; + + const _exprKeyMapper = new class implements IContextKeyExprMapper { + mapDefined(key: string): ContextKeyExpression { + return ContextKeyDefinedExpr.create(key); + } + mapNot(key: string): ContextKeyExpression { + return ContextKeyNotExpr.create(key); + } + mapEquals(key: string, value: any): ContextKeyExpression { + if (key === 'resourceScheme' && typeof value === 'string') { + return ContextKeyEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); + } else { + return ContextKeyEqualsExpr.create(key, value); + } + } + mapNotEquals(key: string, value: any): ContextKeyExpression { + if (key === 'resourceScheme' && typeof value === 'string') { + return ContextKeyNotEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); + } else { + return ContextKeyNotEqualsExpr.create(key, value); + } + } + mapGreater(key: string, value: any): ContextKeyExpression { + return ContextKeyGreaterExpr.create(key, value); + } + mapGreaterEquals(key: string, value: any): ContextKeyExpression { + return ContextKeyGreaterEqualsExpr.create(key, value); + } + mapSmaller(key: string, value: any): ContextKeyExpression { + return ContextKeySmallerExpr.create(key, value); + } + mapSmallerEquals(key: string, value: any): ContextKeyExpression { + return ContextKeySmallerEqualsExpr.create(key, value); + } + mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr { + if (key === 'resourceScheme' && regexp) { + return ContextKeyRegexExpr.create(key, _mapResourceRegExpValue(regexp)); + } else { + return ContextKeyRegexExpr.create(key, regexp); + } + } + mapIn(key: string, valueKey: string): ContextKeyInExpr { + return ContextKeyInExpr.create(key, valueKey); + } + }; + + const _massageWhenUser = (element: WhenUser) => { + if (!element || !element.when || !/resourceScheme/.test(element.when)) { + return; + } + + const expr = ContextKeyExpr.deserialize(element.when); + if (!expr) { + return; + } + + const massaged = expr.map(_exprKeyMapper); + element.when = massaged.serialize(); + }; + + const _massageWhenUserArr = (elements: WhenUser[] | WhenUser) => { + if (Array.isArray(elements)) { + for (let element of elements) { + _massageWhenUser(element); + } + } else { + _massageWhenUser(elements); + } + }; + + const _massageLocWhenUser = (target: LocWhenUser) => { + for (let loc in target) { + _massageWhenUserArr(target[loc]); + } + }; + + extensions.forEach((extension) => { + if (extension.contributes) { + if (extension.contributes.menus) { + _massageLocWhenUser(extension.contributes.menus); + } + if (extension.contributes.keybindings) { + _massageWhenUserArr(extension.contributes.keybindings); + } + if (extension.contributes.views) { + _massageLocWhenUser(extension.contributes.views); + } + } + }); + } + + private async _getEnvironmentData(): Promise { + return { + pid: process.pid, + connectionToken: this._connectionToken, + appRoot: URI.file(this.environmentService.appRoot), + settingsPath: this.environmentService.machineSettingsResource, + logsPath: URI.file(this.environmentService.logsPath), + extensionsPath: URI.file(this.environmentService.extensionsPath!), + extensionHostLogsPath: URI.file(join(this.environmentService.logsPath, `exthost${RemoteAgentEnvironmentChannel._namePool++}`)), + globalStorageHome: this.environmentService.globalStorageHome, + workspaceStorageHome: this.environmentService.workspaceStorageHome, + userHome: this.environmentService.userHome, + os: platform.OS, + arch: process.arch, + marks: performance.getMarks(), + useHostProxy: (this.environmentService.args['use-host-proxy'] !== undefined) + }; + } + + private async _getTranslations(language: string): Promise { + const config = await getNLSConfiguration(language, this.environmentService.userDataPath); + if (InternalNLSConfiguration.is(config)) { + try { + const content = await Promises.readFile(config._translationsConfigFile, 'utf8'); + return JSON.parse(content); + } catch (err) { + return Object.create(null); + } + } else { + return Object.create(null); + } + } + + private async _scanExtensions(language: string, extensionDevelopmentPath?: string[]): Promise { + // Ensure that the language packs are available + const translations = await this._getTranslations(language); + + const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([ + this._scanBuiltinExtensions(language, translations), + this._scanInstalledExtensions(language, translations), + this._scanDevelopedExtensions(language, translations, extensionDevelopmentPath) + ]); + + let result = new Map(); + + builtinExtensions.forEach((builtinExtension) => { + if (!builtinExtension) { + return; + } + result.set(ExtensionIdentifier.toKey(builtinExtension.identifier), builtinExtension); + }); + + installedExtensions.forEach((installedExtension) => { + if (!installedExtension) { + return; + } + if (result.has(ExtensionIdentifier.toKey(installedExtension.identifier))) { + console.warn(nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result.get(ExtensionIdentifier.toKey(installedExtension.identifier))!.extensionLocation.fsPath, installedExtension.extensionLocation.fsPath)); + } + result.set(ExtensionIdentifier.toKey(installedExtension.identifier), installedExtension); + }); + + developedExtensions.forEach((developedExtension) => { + if (!developedExtension) { + return; + } + result.set(ExtensionIdentifier.toKey(developedExtension.identifier), developedExtension); + }); + + const r: IExtensionDescription[] = []; + result.forEach((v) => r.push(v)); + return r; + } + + private _scanDevelopedExtensions(language: string, translations: Translations, extensionDevelopmentPaths?: string[]): Promise { + + if (extensionDevelopmentPaths) { + + const extDescsP = extensionDevelopmentPaths.map(extDevPath => { + return ExtensionScanner.scanOneOrMultipleExtensions( + new ExtensionScannerInput( + this.productService.version, + this.productService.date, + this.productService.commit, + language, + true, // dev mode + extDevPath, + false, // isBuiltin + true, // isUnderDevelopment + translations // translations + ), this._logger + ); + }); + + return Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { + let extDesc: IExtensionDescription[] = []; + for (let eds of extDescArrays) { + extDesc = extDesc.concat(eds); + } + return extDesc; + }); + } + return Promise.resolve([]); + } + + private _scanBuiltinExtensions(language: string, translations: Translations): Promise { + const version = this.productService.version; + const commit = this.productService.commit; + const date = this.productService.date; + const devMode = !!process.env['VSCODE_DEV']; + + const input = new ExtensionScannerInput(version, date, commit, language, devMode, getSystemExtensionsRoot(), true, false, translations); + const builtinExtensions = ExtensionScanner.scanExtensions(input, this._logger); + let finalBuiltinExtensions: Promise = builtinExtensions; + + if (devMode) { + + class ExtraBuiltInExtensionResolver implements IExtensionResolver { + constructor(private builtInExtensions: IBuiltInExtension[]) { } + resolveExtensions(): Promise { + return Promise.resolve(this.builtInExtensions.map((ext) => { + return { name: ext.name, path: join(getExtraDevSystemExtensionsRoot(), ext.name) }; + })); + } + } + + const builtInExtensions = Promise.resolve(this.productService.builtInExtensions || []); + + const input = new ExtensionScannerInput(version, date, commit, language, devMode, getExtraDevSystemExtensionsRoot(), true, false, {}); + const extraBuiltinExtensions = builtInExtensions + .then((builtInExtensions) => new ExtraBuiltInExtensionResolver(builtInExtensions)) + .then(resolver => ExtensionScanner.scanExtensions(input, this._logger, resolver)); + + finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions); + } + + return finalBuiltinExtensions; + } + + private _scanInstalledExtensions(language: string, translations: Translations): Promise { + const devMode = !!process.env['VSCODE_DEV']; + const input = new ExtensionScannerInput( + this.productService.version, + this.productService.date, + this.productService.commit, + language, + devMode, + this.environmentService.extensionsPath!, + false, // isBuiltin + false, // isUnderDevelopment + translations + ); + + return ExtensionScanner.scanExtensions(input, this._logger); + } + + private _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string, translations: Translations): Promise { + const devMode = !!process.env['VSCODE_DEV']; + const input = new ExtensionScannerInput( + this.productService.version, + this.productService.date, + this.productService.commit, + language, + devMode, + extensionPath, + isBuiltin, + false, // isUnderDevelopment + translations + ); + return ExtensionScanner.scanSingleExtension(input, this._logger); + } +} diff --git a/src/vs/server/remoteCli.ts b/src/vs/server/remoteCli.ts new file mode 100644 index 0000000000..66688b3bbb --- /dev/null +++ b/src/vs/server/remoteCli.ts @@ -0,0 +1,412 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as _fs from 'fs'; +import * as _url from 'url'; +import * as _cp from 'child_process'; +import * as _http from 'http'; +import * as _os from 'os'; +import { cwd } from 'vs/base/common/process'; +import { dirname, extname, resolve, join } from 'vs/base/common/path'; +import { parseArgs, buildHelpMessage, buildVersionMessage, OPTIONS, OptionDescriptions } from 'vs/platform/environment/node/argv'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; +import { PipeCommand } from 'vs/workbench/api/node/extHostCLIServer'; +import { hasStdinWithoutTty, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; + +/* + * Implements a standalone CLI app that opens VS Code from a remote terminal. + * - In integrated terminals for remote windows this connects to the remote server though a pipe. + * The pipe is passed in env VSCODE_IPC_HOOK_CLI. + * - In external terminals for WSL this calls VS Code on the Windows side. + * The VS Code desktop executable path is passed in env VSCODE_CLIENT_COMMAND. + */ + + +interface ProductDescription { + productName: string; + version: string; + commit: string; + executableName: string; +} + +interface RemoteParsedArgs extends NativeParsedArgs { 'gitCredential'?: string; 'openExternal'?: boolean; } + + +const isSupportedForCmd = (optionId: keyof RemoteParsedArgs) => { + switch (optionId) { + case 'user-data-dir': + case 'extensions-dir': + case 'export-default-configuration': + case 'install-source': + case 'driver': + case 'extensions-download-dir': + case 'builtin-extensions-dir': + case 'telemetry': + return false; + default: + return true; + } +}; + +const isSupportedForPipe = (optionId: keyof RemoteParsedArgs) => { + switch (optionId) { + case 'version': + case 'help': + case 'folder-uri': + case 'file-uri': + case 'add': + case 'diff': + case 'wait': + case 'goto': + case 'reuse-window': + case 'new-window': + case 'status': + case 'install-extension': + case 'uninstall-extension': + case 'list-extensions': + case 'force': + case 'show-versions': + case 'category': + return true; + default: + return false; + } +}; + +const cliPipe = process.env['VSCODE_IPC_HOOK_CLI'] as string; +const cliCommand = process.env['VSCODE_CLIENT_COMMAND'] as string; +const cliCommandCwd = process.env['VSCODE_CLIENT_COMMAND_CWD'] as string; +const remoteAuthority = process.env['VSCODE_CLI_AUTHORITY'] as string; +const cliStdInFilePath = process.env['VSCODE_STDIN_FILE_PATH'] as string; + + +export function main(desc: ProductDescription, args: string[]): void { + if (!cliPipe && !cliCommand) { + console.log('Command is only available in WSL or inside a Visual Studio Code terminal.'); + return; + } + + // take the local options and remove the ones that don't apply + const options: OptionDescriptions = { ...OPTIONS }; + const isSupported = cliCommand ? isSupportedForCmd : isSupportedForPipe; + for (const optionId in OPTIONS) { + const optId = optionId; + if (!isSupported(optId)) { + delete options[optId]; + } + } + + if (cliPipe) { + options['openExternal'] = { type: 'boolean' }; + } + + const errorReporter = { + onMultipleValues: (id: string, usedValue: string) => { + console.error(`Option ${id} can only be defined once. Using value ${usedValue}.`); + }, + + onUnknownOption: (id: string) => { + console.error(`Ignoring option ${id}: not supported for ${desc.executableName}.`); + } + }; + + const parsedArgs = parseArgs(args, options, errorReporter); + const mapFileUri = remoteAuthority ? mapFileToRemoteUri : (uri: string) => uri; + + if (parsedArgs.help) { + console.log(buildHelpMessage(desc.productName, desc.executableName, desc.version, options, true)); + return; + } + if (parsedArgs.version) { + console.log(buildVersionMessage(desc.version, desc.commit)); + return; + } + if (cliPipe) { + if (parsedArgs['openExternal']) { + openInBrowser(parsedArgs['_']); + return; + } + } + + + let folderURIs = (parsedArgs['folder-uri'] || []).map(mapFileUri); + parsedArgs['folder-uri'] = folderURIs; + + let fileURIs = (parsedArgs['file-uri'] || []).map(mapFileUri); + parsedArgs['file-uri'] = fileURIs; + + let inputPaths = parsedArgs['_']; + let hasReadStdinArg = false; + for (let input of inputPaths) { + if (input === '-') { + hasReadStdinArg = true; + } else { + translatePath(input, mapFileUri, folderURIs, fileURIs); + } + } + + parsedArgs['_'] = []; + + if (hasReadStdinArg && fileURIs.length === 0 && folderURIs.length === 0 && hasStdinWithoutTty()) { + try { + let stdinFilePath = cliStdInFilePath; + if (!stdinFilePath) { + stdinFilePath = getStdinFilePath(); + readFromStdin(stdinFilePath, !!parsedArgs.verbose); // throws error if file can not be written + } + + // Make sure to open tmp file + translatePath(stdinFilePath, mapFileUri, folderURIs, fileURIs); + + // Enable --wait to get all data and ignore adding this to history + parsedArgs.wait = true; + parsedArgs['skip-add-to-recently-opened'] = true; + + console.log(`Reading from stdin via: ${stdinFilePath}`); + } catch (e) { + console.log(`Failed to create file to read via stdin: ${e.toString()}`); + } + + } + + if (parsedArgs.extensionDevelopmentPath) { + parsedArgs.extensionDevelopmentPath = parsedArgs.extensionDevelopmentPath.map(p => mapFileUri(pathToURI(p).href)); + } + + if (parsedArgs.extensionTestsPath) { + parsedArgs.extensionTestsPath = mapFileUri(pathToURI(parsedArgs['extensionTestsPath']).href); + } + + const crashReporterDirectory = parsedArgs['crash-reporter-directory']; + if (crashReporterDirectory !== undefined && !crashReporterDirectory.match(/^([a-zA-Z]:[\\\/])/)) { + console.log(`The crash reporter directory '${crashReporterDirectory}' must be an absolute Windows path (e.g. c:/crashes)`); + return; + } + + if (remoteAuthority) { + parsedArgs['remote'] = remoteAuthority; + } + + if (cliCommand) { + if (parsedArgs['install-extension'] !== undefined || parsedArgs['uninstall-extension'] !== undefined || parsedArgs['list-extensions']) { + const cmdLine: string[] = []; + parsedArgs['install-extension']?.forEach(id => cmdLine.push('--install-extension', id)); + parsedArgs['uninstall-extension']?.forEach(id => cmdLine.push('--uninstall-extension', id)); + ['list-extensions', 'force', 'show-versions', 'category'].forEach(opt => { + const value = parsedArgs[opt]; + if (value !== undefined) { + cmdLine.push(`--${opt}=${value}`); + } + }); + const cp = _cp.fork(join(__dirname, 'main.js'), cmdLine, { stdio: 'inherit' }); + cp.on('error', err => console.log(err)); + return; + } + + + let newCommandline: string[] = []; + for (let key in parsedArgs) { + let val = parsedArgs[key as keyof typeof parsedArgs]; + if (typeof val === 'boolean') { + if (val) { + newCommandline.push('--' + key); + } + } else if (Array.isArray(val)) { + for (let entry of val) { + newCommandline.push(`--${key}=${entry.toString()}`); + } + } else if (val) { + newCommandline.push(`--${key}=${val.toString()}`); + } + } + + const ext = extname(cliCommand); + if (ext === '.bat' || ext === '.cmd') { + const processCwd = cliCommandCwd || cwd(); + if (parsedArgs['verbose']) { + console.log(`Invoking: cmd.exe /C ${cliCommand} ${newCommandline.join(' ')} in ${processCwd}`); + } + _cp.spawn('cmd.exe', ['/C', cliCommand, ...newCommandline], { + stdio: 'inherit', + cwd: processCwd + }); + } else { + const cliCwd = dirname(cliCommand); + const env = { ...process.env, ELECTRON_RUN_AS_NODE: '1' }; + newCommandline.unshift('resources/app/out/cli.js'); + if (parsedArgs['verbose']) { + console.log(`Invoking: ${cliCommand} ${newCommandline.join(' ')} in ${cliCwd}`); + } + _cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: ['inherit'] }); + } + } else { + if (args.length === 0) { + console.log(buildHelpMessage(desc.productName, desc.executableName, desc.version, options, true)); + return; + } + if (parsedArgs.status) { + sendToPipe({ + type: 'status' + }).then((res: string) => { + console.log(res); + }); + return; + } + + if (parsedArgs['install-extension'] !== undefined || parsedArgs['uninstall-extension'] !== undefined || parsedArgs['list-extensions']) { + sendToPipe({ + type: 'extensionManagement', + list: parsedArgs['list-extensions'] ? { showVersions: parsedArgs['show-versions'], category: parsedArgs['category'] } : undefined, + install: asExtensionIdOrVSIX(parsedArgs['install-extension']), + uninstall: asExtensionIdOrVSIX(parsedArgs['uninstall-extension']), + force: parsedArgs['force'] + }).then((res: string) => { + console.log(res); + }); + return; + } + + if (!fileURIs.length && !folderURIs.length) { + console.log('At least one file or folder must be provided.'); + return; + } + + let waitMarkerFilePath: string | undefined = undefined; + if (parsedArgs['wait']) { + if (!fileURIs.length) { + console.log('At least one file must be provided to wait for.'); + return; + } + waitMarkerFilePath = createWaitMarkerFile(parsedArgs.verbose); + } + + sendToPipe({ + type: 'open', + fileURIs, + folderURIs, + diffMode: parsedArgs.diff, + addMode: parsedArgs.add, + gotoLineMode: parsedArgs.goto, + forceReuseWindow: parsedArgs['reuse-window'], + forceNewWindow: parsedArgs['new-window'], + waitMarkerFilePath + }); + + if (waitMarkerFilePath) { + waitForFileDeleted(waitMarkerFilePath); + } + } +} + +async function waitForFileDeleted(path: string) { + while (_fs.existsSync(path)) { + await new Promise(res => setTimeout(res, 1000)); + } +} + +function openInBrowser(args: string[]) { + let uris: string[] = []; + for (let location of args) { + try { + if (/^(http|https|file):\/\//.test(location)) { + uris.push(_url.parse(location).href); + } else { + uris.push(pathToURI(location).href); + } + } catch (e) { + console.log(`Invalid url: ${location}`); + } + } + if (uris.length) { + sendToPipe({ + type: 'openExternal', + uris + }); + } +} + +function sendToPipe(args: PipeCommand): Promise { + return new Promise(resolve => { + const message = JSON.stringify(args); + if (!cliPipe) { + console.log('Message ' + message); + resolve(''); + return; + } + + const opts: _http.RequestOptions = { + socketPath: cliPipe, + path: '/', + method: 'POST' + }; + + const req = _http.request(opts, res => { + const chunks: string[] = []; + res.setEncoding('utf8'); + res.on('data', chunk => { + chunks.push(chunk); + }); + res.on('error', () => fatal('Error in response')); + res.on('end', () => { + resolve(chunks.join('')); + }); + }); + + req.on('error', () => fatal('Error in request')); + req.write(message); + req.end(); + }); +} + +function asExtensionIdOrVSIX(inputs: string[] | undefined) { + return inputs?.map(input => /\.vsix$/i.test(input) ? pathToURI(input).href : input); +} + +function fatal(err: any): void { + console.error('Unable to connect to VS Code server.'); + console.error(err); + process.exit(1); +} + +const preferredCwd = process.env.PWD || cwd(); // prefer process.env.PWD as it does not follow symlinks + +function pathToURI(input: string): _url.URL { + input = input.trim(); + input = resolve(preferredCwd, input); + + return _url.pathToFileURL(input); +} + +function translatePath(input: string, mapFileUri: (input: string) => string, folderURIS: string[], fileURIS: string[]) { + let url = pathToURI(input); + let mappedUri = mapFileUri(url.href); + try { + let stat = _fs.lstatSync(_fs.realpathSync(input)); + + if (stat.isFile()) { + fileURIS.push(mappedUri); + } else if (stat.isDirectory()) { + folderURIS.push(mappedUri); + } else if (input === '/dev/null') { + // handle /dev/null passed to us by external tools such as `git difftool` + fileURIS.push(mappedUri); + } + } catch (e) { + if (e.code === 'ENOENT') { + fileURIS.push(mappedUri); + } else { + console.log(`Problem accessing file ${input}. Ignoring file`, e); + } + } +} + +function mapFileToRemoteUri(uri: string): string { + return uri.replace(/^file:\/\//, 'vscode-remote://' + remoteAuthority); +} + +let [, , productName, version, commit, executableName, ...remainingArgs] = process.argv; +main({ productName, version, commit, executableName }, remainingArgs); + diff --git a/src/vs/server/remoteExtensionHostAgent.ts b/src/vs/server/remoteExtensionHostAgent.ts new file mode 100644 index 0000000000..0ec5b5e623 --- /dev/null +++ b/src/vs/server/remoteExtensionHostAgent.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as fs from 'fs'; +import * as net from 'net'; +import { FileAccess } from 'vs/base/common/network'; +import { run as runCli } from 'vs/server/remoteExtensionHostAgentCli'; +import { createServer as doCreateServer, IServerAPI } from 'vs/server/remoteExtensionHostAgentServer'; +import { parseArgs, ErrorReporter } from 'vs/platform/environment/node/argv'; +import { join, dirname } from 'vs/base/common/path'; +import { performance } from 'perf_hooks'; +import { serverOptions } from 'vs/server/serverEnvironmentService'; +import * as perf from 'vs/base/common/performance'; + +perf.mark('code/server/codeLoaded'); +(global).vscodeServerCodeLoadedTime = performance.now(); + +const errorReporter: ErrorReporter = { + onMultipleValues: (id: string, usedValue: string) => { + console.error(`Option ${id} can only be defined once. Using value ${usedValue}.`); + }, + + onUnknownOption: (id: string) => { + console.error(`Ignoring option ${id}: not supported for server.`); + } +}; + +const args = parseArgs(process.argv.slice(2), serverOptions, errorReporter); + +const REMOTE_DATA_FOLDER = process.env['VSCODE_AGENT_FOLDER'] || join(os.homedir(), '.vscode-remote'); +const USER_DATA_PATH = join(REMOTE_DATA_FOLDER, 'data'); +const APP_SETTINGS_HOME = join(USER_DATA_PATH, 'User'); +const GLOBAL_STORAGE_HOME = join(APP_SETTINGS_HOME, 'globalStorage'); +const MACHINE_SETTINGS_HOME = join(USER_DATA_PATH, 'Machine'); +args['user-data-dir'] = USER_DATA_PATH; +const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath); +const BUILTIN_EXTENSIONS_FOLDER_PATH = join(APP_ROOT, 'extensions'); +args['builtin-extensions-dir'] = BUILTIN_EXTENSIONS_FOLDER_PATH; +args['extensions-dir'] = args['extensions-dir'] || join(REMOTE_DATA_FOLDER, 'extensions'); + +[REMOTE_DATA_FOLDER, args['extensions-dir'], USER_DATA_PATH, APP_SETTINGS_HOME, MACHINE_SETTINGS_HOME, GLOBAL_STORAGE_HOME].forEach(f => { + try { + if (!fs.existsSync(f)) { + fs.mkdirSync(f); + } + } catch (err) { console.error(err); } +}); + +/** + * invoked by vs/server/main.js + */ +export function spawnCli() { + runCli(args, REMOTE_DATA_FOLDER); +} + +/** + * invoked by vs/server/main.js + */ +export function createServer(address: string | net.AddressInfo | null): Promise { + return doCreateServer(address, args, REMOTE_DATA_FOLDER); +} diff --git a/src/vs/server/remoteExtensionHostAgentCli.ts b/src/vs/server/remoteExtensionHostAgentCli.ts new file mode 100644 index 0000000000..d117d55a46 --- /dev/null +++ b/src/vs/server/remoteExtensionHostAgentCli.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { getLogLevel, ILogService, LogService } from 'vs/platform/log/common/log'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { RequestService } from 'vs/platform/request/node/requestService'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import product from 'vs/platform/product/common/product'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { Schemas } from 'vs/base/common/network'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; +import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IServerEnvironmentService, ServerEnvironmentService, ServerParsedArgs } from 'vs/server/serverEnvironmentService'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { getErrorMessage } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { isAbsolute, join } from 'vs/base/common/path'; +import { cwd } from 'vs/base/common/process'; +import { DownloadService } from 'vs/platform/download/common/downloadService'; +import { IDownloadService } from 'vs/platform/download/common/download'; + +class CliMain extends Disposable { + + constructor(private readonly args: ServerParsedArgs, private readonly remoteDataFolder: string) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + // Dispose on exit + process.once('exit', () => this.dispose()); + } + + async run(): Promise { + const instantiationService = await this.initServices(); + await instantiationService.invokeFunction(async accessor => { + const logService = accessor.get(ILogService); + const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService); + try { + await this.doRun(extensionManagementCLIService); + } catch (error) { + logService.error(error); + console.error(getErrorMessage(error)); + throw error; + } + }); + } + + private async initServices(): Promise { + const services = new ServiceCollection(); + + const productService = { _serviceBrand: undefined, ...product }; + services.set(IProductService, productService); + + const environmentService = new ServerEnvironmentService(this.args, productService); + services.set(IServerEnvironmentService, environmentService); + const logService: ILogService = new LogService(new SpdLogLogger(RemoteExtensionLogFileName, join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, getLogLevel(environmentService))); + services.set(ILogService, logService); + logService.trace(`Remote configuration data at ${this.remoteDataFolder}`); + logService.trace('process arguments:', this.args); + + + // Files + const fileService = this._register(new FileService(logService)); + services.set(IFileService, fileService); + fileService.registerProvider(Schemas.file, this._register(new DiskFileSystemProvider(logService))); + + // Configuration + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + await configurationService.initialize(); + services.set(IConfigurationService, configurationService); + + services.set(IRequestService, new SyncDescriptor(RequestService)); + services.set(IDownloadService, new SyncDescriptor(DownloadService)); + services.set(ITelemetryService, NullTelemetryService); + services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); + services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + + return new InstantiationService(services); + } + + private async doRun(extensionManagementCLIService: IExtensionManagementCLIService): Promise { + + // List Extensions + if (this.args['list-extensions']) { + return extensionManagementCLIService.listExtensions(!!this.args['show-versions'], this.args['category']); + } + + // Install Extension + else if (this.args['install-extension'] || this.args['install-builtin-extension']) { + return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.args['install-extension'] || []), this.args['install-builtin-extension'] || [], !!this.args['do-not-sync'], !!this.args['force']); + } + + // Uninstall Extension + else if (this.args['uninstall-extension']) { + return extensionManagementCLIService.uninstallExtensions(this.asExtensionIdOrVSIX(this.args['uninstall-extension']), !!this.args['force']); + } + + // Locate Extension + else if (this.args['locate-extension']) { + return extensionManagementCLIService.locateExtension(this.args['locate-extension']); + } + } + + private asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] { + return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); + } +} + +function eventuallyExit(code: number): void { + setTimeout(() => process.exit(code), 0); +} + +export async function run(args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise { + const cliMain = new CliMain(args, REMOTE_DATA_FOLDER); + try { + await cliMain.run(); + eventuallyExit(0); + } catch (err) { + eventuallyExit(1); + } finally { + cliMain.dispose(); + } +} diff --git a/src/vs/server/remoteExtensionHostAgentServer.ts b/src/vs/server/remoteExtensionHostAgentServer.ts new file mode 100644 index 0000000000..41ce6e3b22 --- /dev/null +++ b/src/vs/server/remoteExtensionHostAgentServer.ts @@ -0,0 +1,1043 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as net from 'net'; +import * as url from 'url'; +import { release, hostname } from 'os'; +import * as perf from 'vs/base/common/performance'; +import { performance } from 'perf_hooks'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { Promises } from 'vs/base/node/pfs'; +import { findFreePort } from 'vs/base/node/ports'; +import * as platform from 'vs/base/common/platform'; +import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { ConnectionType, ConnectionTypeRequest, ErrorMessage, HandshakeMessage, IRemoteExtensionHostStartParams, ITunnelConnectionStartParams, SignRequest } from 'vs/platform/remote/common/remoteAgentConnection'; +import { ExtensionHostConnection } from 'vs/server/extensionHostConnection'; +import { ManagementConnection } from 'vs/server/remoteExtensionManagement'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { ILogService, LogLevel, AbstractLogger, DEFAULT_LOG_LEVEL, MultiplexLogService, getLogLevel, LogService } from 'vs/platform/log/common/log'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import product from 'vs/platform/product/common/product'; +import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { RequestService } from 'vs/platform/request/node/requestService'; +import { ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { DownloadServiceChannelClient } from 'vs/platform/download/common/downloadIpc'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; +import { ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; +import { getMachineId } from 'vs/base/node/id'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IPCServer, ClientConnectionEvent, IMessagePassingProtocol, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; +import { Emitter, Event } from 'vs/base/common/event'; +import { RemoteAgentEnvironmentChannel } from 'vs/server/remoteAgentEnvironmentImpl'; +import { RemoteAgentFileSystemProviderChannel } from 'vs/server/remoteFileSystemProviderIpc'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; +import { RequestChannel } from 'vs/platform/request/common/requestIpc'; +import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import ErrorTelemetry from 'vs/platform/telemetry/node/errorTelemetry'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; +import { LogLevelChannel } from 'vs/platform/log/common/logIpc'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { WebClientServer, serveError, serveFile } from 'vs/server/webClientServer'; +import { URI } from 'vs/base/common/uri'; +import { isEqualOrParent } from 'vs/base/common/extpath'; +import { IServerEnvironmentService, ServerEnvironmentService, ServerParsedArgs } from 'vs/server/serverEnvironmentService'; +import { basename, dirname, join } from 'vs/base/common/path'; +import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { RemoteTerminalChannel } from 'vs/server/remoteTerminalChannel'; +import { LoaderStats } from 'vs/base/common/amd'; +import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; +import { IPtyService, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; +import { IRemoteTelemetryService, RemoteNullTelemetryService, RemoteTelemetryService } from 'vs/server/remoteTelemetryService'; + +const SHUTDOWN_TIMEOUT = 5 * 60 * 1000; + +const eventPrefix = 'monacoworkbench'; + +class SocketServer extends IPCServer { + + private _onDidConnectEmitter: Emitter; + + constructor() { + const emitter = new Emitter(); + super(emitter.event); + this._onDidConnectEmitter = emitter; + } + + public acceptConnection(protocol: IMessagePassingProtocol, onDidClientDisconnect: Event): void { + this._onDidConnectEmitter.fire({ protocol, onDidClientDisconnect }); + } +} + +function twodigits(n: number): string { + if (n < 10) { + return `0${n}`; + } + return String(n); +} + +function now(): string { + const date = new Date(); + return `${twodigits(date.getHours())}:${twodigits(date.getMinutes())}:${twodigits(date.getSeconds())}`; +} + +class ServerLogService extends AbstractLogger implements ILogService { + _serviceBrand: undefined; + private useColors: boolean; + + constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) { + super(); + this.setLevel(logLevel); + this.useColors = Boolean(process.stdout.isTTY); + } + + trace(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Trace) { + if (this.useColors) { + console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); + } else { + console.log(`[${now()}]`, message, ...args); + } + } + } + + debug(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Debug) { + if (this.useColors) { + console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); + } else { + console.log(`[${now()}]`, message, ...args); + } + } + } + + info(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Info) { + if (this.useColors) { + console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); + } else { + console.log(`[${now()}]`, message, ...args); + } + } + } + + warn(message: string | Error, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Warning) { + if (this.useColors) { + console.warn(`\x1b[93m[${now()}]\x1b[0m`, message, ...args); + } else { + console.warn(`[${now()}]`, message, ...args); + } + } + } + + error(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Error) { + if (this.useColors) { + console.error(`\x1b[91m[${now()}]\x1b[0m`, message, ...args); + } else { + console.error(`[${now()}]`, message, ...args); + } + } + } + + critical(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Critical) { + if (this.useColors) { + console.error(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); + } else { + console.error(`[${now()}]`, message, ...args); + } + } + } + + override dispose(): void { + // noop + } + + flush(): void { + // noop + } +} + +export type ServerListenOptions = { host?: string; port?: number; socketPath?: string }; + +declare module vsda { + // the signer is a native module that for historical reasons uses a lower case class name + // eslint-disable-next-line @typescript-eslint/naming-convention + export class signer { + sign(arg: string): string; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + export class validator { + createNewMessage(arg: string): string; + validate(arg: string): 'ok' | 'error'; + } +} + +export class RemoteExtensionHostAgentServer extends Disposable { + + private readonly _logService: ILogService; + private readonly _socketServer: SocketServer; + private readonly _uriTransformerCache: { [remoteAuthority: string]: IURITransformer; }; + private readonly _extHostConnections: { [reconnectionToken: string]: ExtensionHostConnection; }; + private readonly _managementConnections: { [reconnectionToken: string]: ManagementConnection; }; + private readonly _allReconnectionTokens: Set; + private readonly _webClientServer: WebClientServer | null; + + private shutdownTimer: NodeJS.Timer | undefined; + + constructor( + private readonly _environmentService: IServerEnvironmentService, + private readonly _productService: IProductService, + private readonly _connectionToken: string, + private readonly _connectionTokenIsMandatory: boolean, + hasWebClient: boolean, + REMOTE_DATA_FOLDER: string + ) { + super(); + + const logService = getOrCreateSpdLogService(this._environmentService); + logService.trace(`Remote configuration data at ${REMOTE_DATA_FOLDER}`); + logService.trace('process arguments:', this._environmentService.args); + const serverGreeting = _productService.serverGreeting.join('\n'); + if (serverGreeting) { + logService.info(`\n\n${serverGreeting}\n\n`); + } + + this._logService = new MultiplexLogService([new ServerLogService(getLogLevel(this._environmentService)), logService]); + this._socketServer = new SocketServer(); + this._uriTransformerCache = Object.create(null); + this._extHostConnections = Object.create(null); + this._managementConnections = Object.create(null); + this._allReconnectionTokens = new Set(); + + if (hasWebClient) { + this._webClientServer = new WebClientServer(this._connectionToken, this._environmentService, this._logService, this._productService); + } else { + this._webClientServer = null; + } + this._logService.info(`Extension host agent started.`); + } + + public async initialize(): Promise<{ telemetryService: ITelemetryService; }> { + const services = await this._createServices(); + setTimeout(() => this._cleanupOlderLogs(this._environmentService.logsPath).then(null, err => this._logService.error(err)), 10000); + return services; + } + + private async _createServices(): Promise<{ telemetryService: ITelemetryService; }> { + const services = new ServiceCollection(); + + // ExtensionHost Debug broadcast service + this._socketServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel()); + + // TODO: @Sandy @Joao need dynamic context based router + const router = new StaticRouter(ctx => ctx.clientId === 'renderer'); + this._socketServer.registerChannel('logger', new LogLevelChannel(this._logService)); + + services.set(IEnvironmentService, this._environmentService); + services.set(INativeEnvironmentService, this._environmentService); + + services.set(ILogService, this._logService); + services.set(IProductService, this._productService); + + // Files + const fileService = this._register(new FileService(this._logService)); + services.set(IFileService, fileService); + fileService.registerProvider(Schemas.file, this._register(new DiskFileSystemProvider(this._logService))); + + const configurationService = new ConfigurationService(this._environmentService.machineSettingsResource, fileService); + services.set(IConfigurationService, configurationService); + services.set(IRequestService, new SyncDescriptor(RequestService)); + + let appInsightsAppender: ITelemetryAppender = NullAppender; + if (!this._environmentService.args['disable-telemetry'] && this._productService.enableTelemetry) { + if (this._productService.aiConfig && this._productService.aiConfig.asimovKey) { + appInsightsAppender = new AppInsightsAppender(eventPrefix, null, this._productService.aiConfig.asimovKey); + this._register(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data + } + + const machineId = await getMachineId(); + const config: ITelemetryServiceConfig = { + appenders: [appInsightsAppender], + commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, this._productService.commit, this._productService.version + '-remote', machineId, this._productService.msftInternalDomains, this._environmentService.installSourcePath, 'remoteAgent'), + piiPaths: [this._environmentService.appRoot] + }; + + services.set(IRemoteTelemetryService, new SyncDescriptor(RemoteTelemetryService, [config])); + } else { + services.set(IRemoteTelemetryService, RemoteNullTelemetryService); + } + + services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); + + const downloadChannel = this._socketServer.getChannel('download', router); + services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel, () => this._getUriTransformer('renderer') /* TODO: @Sandy @Joao need dynamic context based router */)); + + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + + const instantiationService = new InstantiationService(services); + services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService)); + + const extensionManagementCLIService = instantiationService.createInstance(ExtensionManagementCLIService); + services.set(IExtensionManagementCLIService, extensionManagementCLIService); + + const ptyService = instantiationService.createInstance( + PtyHostService, + { + GraceTime: ProtocolConstants.ReconnectionGraceTime, + ShortGraceTime: ProtocolConstants.ReconnectionShortGraceTime, + scrollback: configurationService.getValue(TerminalSettingId.PersistentSessionScrollback) ?? 100 + } + ); + services.set(IPtyService, ptyService); + + return instantiationService.invokeFunction(accessor => { + const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(this._connectionToken, this._environmentService, extensionManagementCLIService, this._logService, accessor.get(IRemoteTelemetryService), appInsightsAppender, this._productService); + this._socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); + + this._socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(this._environmentService, this._logService, ptyService, this._productService)); + + const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(this._logService, this._environmentService); + this._socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel); + + this._socketServer.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); + + const extensionManagementService = accessor.get(IExtensionManagementService); + const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => this._getUriTransformer(ctx.remoteAuthority)); + this._socketServer.registerChannel('extensions', channel); + + // clean up deprecated extensions + (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); + + this._register(new ErrorTelemetry(accessor.get(ITelemetryService))); + + return { + telemetryService: accessor.get(ITelemetryService) + }; + }); + } + + private _getUriTransformer(remoteAuthority: string): IURITransformer { + if (!this._uriTransformerCache[remoteAuthority]) { + this._uriTransformerCache[remoteAuthority] = createRemoteURITransformer(remoteAuthority); + } + return this._uriTransformerCache[remoteAuthority]; + } + + public async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) { + // Only serve GET requests + if (req.method !== 'GET') { + return serveError(req, res, 405, `Unsupported method ${req.method}`); + } + + if (!req.url) { + return serveError(req, res, 400, `Bad request.`); + } + + const parsedUrl = url.parse(req.url, true); + const pathname = parsedUrl.pathname; + + if (!pathname) { + return serveError(req, res, 400, `Bad request.`); + } + + // Version + if (pathname === '/version') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + return res.end(this._productService.commit || ''); + } + + // Delay shutdown + if (pathname === '/delay-shutdown') { + this._delayShutdown(); + res.writeHead(200); + return res.end('OK'); + } + + if (pathname === '/vscode-remote-resource') { + // Handle HTTP requests for resources rendered in the rich client (images, fonts, etc.) + // These resources could be files shipped with extensions or even workspace files. + if (parsedUrl.query['tkn'] !== this._connectionToken) { + return serveError(req, res, 403, `Forbidden.`); + } + + const desiredPath = parsedUrl.query['path']; + if (typeof desiredPath !== 'string') { + return serveError(req, res, 400, `Bad request.`); + } + + let filePath: string; + try { + filePath = URI.from({ scheme: Schemas.file, path: desiredPath }).fsPath; + } catch (err) { + return serveError(req, res, 400, `Bad request.`); + } + + const responseHeaders: Record = Object.create(null); + if (this._environmentService.isBuilt) { + if (isEqualOrParent(filePath, this._environmentService.builtinExtensionsPath, !platform.isLinux) + || isEqualOrParent(filePath, this._environmentService.extensionsPath, !platform.isLinux) + ) { + responseHeaders['Cache-Control'] = 'public, max-age=31536000'; + } + } + return serveFile(this._logService, req, res, filePath, responseHeaders); + } + + // workbench web UI + if (this._webClientServer) { + this._webClientServer.handle(req, res, parsedUrl); + return; + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + return res.end('Not found'); + } + + public handleUpgrade(req: http.IncomingMessage, socket: net.Socket) { + let reconnectionToken = generateUuid(); + let isReconnection = false; + let skipWebSocketFrames = false; + + if (req.url) { + const query = url.parse(req.url, true).query; + if (typeof query.reconnectionToken === 'string') { + reconnectionToken = query.reconnectionToken; + } + if (query.reconnection === 'true') { + isReconnection = true; + } + if (query.skipWebSocketFrames === 'true') { + skipWebSocketFrames = true; + } + } + + if (req.headers['upgrade'] !== 'websocket') { + socket.end('HTTP/1.1 400 Bad Request'); + return; + } + + // https://tools.ietf.org/html/rfc6455#section-4 + const requestNonce = req.headers['sec-websocket-key']; + const hash = crypto.createHash('sha1'); + hash.update(requestNonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'); + const responseNonce = hash.digest('base64'); + + const responseHeaders = [ + `HTTP/1.1 101 Switching Protocols`, + `Upgrade: websocket`, + `Connection: Upgrade`, + `Sec-WebSocket-Accept: ${responseNonce}` + ]; + + // See https://tools.ietf.org/html/rfc7692#page-12 + let permessageDeflate = false; + if (!skipWebSocketFrames && !this._environmentService.args['disable-websocket-compression'] && req.headers['sec-websocket-extensions']) { + const websocketExtensionOptions = Array.isArray(req.headers['sec-websocket-extensions']) ? req.headers['sec-websocket-extensions'] : [req.headers['sec-websocket-extensions']]; + for (const websocketExtensionOption of websocketExtensionOptions) { + if (/\b((server_max_window_bits)|(server_no_context_takeover)|(client_no_context_takeover))\b/.test(websocketExtensionOption)) { + // sorry, the server does not support zlib parameter tweaks + continue; + } + if (/\b(permessage-deflate)\b/.test(websocketExtensionOption)) { + permessageDeflate = true; + responseHeaders.push(`Sec-WebSocket-Extensions: permessage-deflate`); + break; + } + if (/\b(x-webkit-deflate-frame)\b/.test(websocketExtensionOption)) { + permessageDeflate = true; + responseHeaders.push(`Sec-WebSocket-Extensions: x-webkit-deflate-frame`); + break; + } + } + } + + socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); + + // Never timeout this socket due to inactivity! + socket.setTimeout(0); + // Finally! + + if (skipWebSocketFrames) { + this._handleWebSocketConnection(new NodeSocket(socket), isReconnection, reconnectionToken); + } else { + this._handleWebSocketConnection(new WebSocketNodeSocket(new NodeSocket(socket), permessageDeflate, null, true), isReconnection, reconnectionToken); + } + } + + public handleServerError(err: Error): void { + this._logService.error(`Error occurred in server`); + this._logService.error(err); + } + + // Eventually cleanup + /** + * Cleans up older logs, while keeping the 10 most recent ones. + */ + private async _cleanupOlderLogs(logsPath: string): Promise { + const currentLog = basename(logsPath); + const logsRoot = dirname(logsPath); + const children = await Promises.readdir(logsRoot); + const allSessions = children.filter(name => /^\d{8}T\d{6}$/.test(name)); + const oldSessions = allSessions.sort().filter((d) => d !== currentLog); + const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9)); + + await Promise.all(toDelete.map(name => Promises.rm(join(logsRoot, name)))); + } + + private _getRemoteAddress(socket: NodeSocket | WebSocketNodeSocket): string { + let _socket: net.Socket; + if (socket instanceof NodeSocket) { + _socket = socket.socket; + } else { + _socket = socket.socket.socket; + } + return _socket.remoteAddress || ``; + } + + private async _rejectWebSocketConnection(logPrefix: string, protocol: PersistentProtocol, reason: string): Promise { + const socket = protocol.getSocket(); + this._logService.error(`${logPrefix} ${reason}.`); + const errMessage: ErrorMessage = { + type: 'error', + reason: reason + }; + protocol.sendControl(VSBuffer.fromString(JSON.stringify(errMessage))); + protocol.dispose(); + await socket.drain(); + socket.dispose(); + } + + /** + * NOTE: Avoid using await in this method! + * The problem is that await introduces a process.nextTick due to the implicit Promise.then + * This can lead to some bytes being interpreted and a control message being emitted before the next listener has a chance to be registered. + */ + private _handleWebSocketConnection(socket: NodeSocket | WebSocketNodeSocket, isReconnection: boolean, reconnectionToken: string): void { + const remoteAddress = this._getRemoteAddress(socket); + const logPrefix = `[${remoteAddress}][${reconnectionToken.substr(0, 8)}]`; + const protocol = new PersistentProtocol(socket); + + let validator: vsda.validator; + let signer: vsda.signer; + try { + const vsdaMod = require.__$__nodeRequire('vsda'); + validator = new vsdaMod.validator(); + signer = new vsdaMod.signer(); + } catch (e) { + } + + const enum State { + WaitingForAuth, + WaitingForConnectionType, + Done, + Error + } + let state = State.WaitingForAuth; + + const rejectWebSocketConnection = (msg: string) => { + state = State.Error; + listener.dispose(); + this._rejectWebSocketConnection(logPrefix, protocol, msg); + }; + + const listener = protocol.onControlMessage((raw) => { + if (state === State.WaitingForAuth) { + let msg1: HandshakeMessage; + try { + msg1 = JSON.parse(raw.toString()); + } catch (err) { + return rejectWebSocketConnection(`Malformed first message`); + } + if (msg1.type !== 'auth') { + return rejectWebSocketConnection(`Invalid first message`); + } + + if (this._connectionTokenIsMandatory && msg1.auth !== this._connectionToken) { + return rejectWebSocketConnection(`Unauthorized client refused: auth mismatch`); + } + + // Send `sign` request + let signedData = generateUuid(); + if (signer) { + try { + signedData = signer.sign(msg1.data); + } catch (e) { + } + } + let someText = generateUuid(); + if (validator) { + try { + someText = validator.createNewMessage(someText); + } catch (e) { + } + } + const signRequest: SignRequest = { + type: 'sign', + data: someText, + signedData: signedData + }; + protocol.sendControl(VSBuffer.fromString(JSON.stringify(signRequest))); + + state = State.WaitingForConnectionType; + + } else if (state === State.WaitingForConnectionType) { + + let msg2: HandshakeMessage; + try { + msg2 = JSON.parse(raw.toString()); + } catch (err) { + return rejectWebSocketConnection(`Malformed second message`); + } + if (msg2.type !== 'connectionType') { + return rejectWebSocketConnection(`Invalid second message`); + } + if (typeof msg2.signedData !== 'string') { + return rejectWebSocketConnection(`Invalid second message field type`); + } + + const rendererCommit = msg2.commit; + const myCommit = this._productService.commit; + if (rendererCommit && myCommit) { + // Running in the built version where commits are defined + if (rendererCommit !== myCommit) { + return rejectWebSocketConnection(`Client refused: version mismatch`); + } + } + + let valid = false; + if (!validator) { + valid = true; + } else if (msg2.signedData === this._connectionToken) { + // web client + valid = true; + } else { + try { + valid = validator.validate(msg2.signedData) === 'ok'; + } catch (e) { + } + } + + if (!valid) { + if (this._environmentService.isBuilt) { + return rejectWebSocketConnection(`Unauthorized client refused`); + } else { + this._logService.error(`${logPrefix} Unauthorized client handshake failed but we proceed because of dev mode.`); + } + } + + // We have received a new connection. + // This indicates that the server owner has connectivity. + // Therefore we will shorten the reconnection grace period for disconnected connections! + for (let key in this._managementConnections) { + const managementConnection = this._managementConnections[key]; + managementConnection.shortenReconnectionGraceTimeIfNecessary(); + } + for (let key in this._extHostConnections) { + const extHostConnection = this._extHostConnections[key]; + extHostConnection.shortenReconnectionGraceTimeIfNecessary(); + } + + state = State.Done; + listener.dispose(); + this._handleConnectionType(remoteAddress, logPrefix, protocol, socket, isReconnection, reconnectionToken, msg2); + } + }); + } + + private async _handleConnectionType(remoteAddress: string, _logPrefix: string, protocol: PersistentProtocol, socket: NodeSocket | WebSocketNodeSocket, isReconnection: boolean, reconnectionToken: string, msg: ConnectionTypeRequest): Promise { + const logPrefix = ( + msg.desiredConnectionType === ConnectionType.Management + ? `${_logPrefix}[ManagementConnection]` + : msg.desiredConnectionType === ConnectionType.ExtensionHost + ? `${_logPrefix}[ExtensionHostConnection]` + : _logPrefix + ); + + if (msg.desiredConnectionType === ConnectionType.Management) { + // This should become a management connection + + if (isReconnection) { + // This is a reconnection + if (!this._managementConnections[reconnectionToken]) { + if (!this._allReconnectionTokens.has(reconnectionToken)) { + // This is an unknown reconnection token + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (never seen)`); + } else { + // This is a connection that was seen in the past, but is no longer valid + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (seen before)`); + } + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'ok' }))); + const dataChunk = protocol.readEntireBuffer(); + protocol.dispose(); + this._managementConnections[reconnectionToken].acceptReconnection(remoteAddress, socket, dataChunk); + + } else { + // This is a fresh connection + if (this._managementConnections[reconnectionToken]) { + // Cannot have two concurrent connections using the same reconnection token + return this._rejectWebSocketConnection(logPrefix, protocol, `Duplicate reconnection token`); + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'ok' }))); + const con = new ManagementConnection(this._logService, reconnectionToken, remoteAddress, protocol); + this._socketServer.acceptConnection(con.protocol, con.onClose); + this._managementConnections[reconnectionToken] = con; + this._allReconnectionTokens.add(reconnectionToken); + con.onClose(() => { + delete this._managementConnections[reconnectionToken]; + }); + + } + + } else if (msg.desiredConnectionType === ConnectionType.ExtensionHost) { + + // This should become an extension host connection + const startParams0 = msg.args || { language: 'en' }; + const startParams = await this._updateWithFreeDebugPort(startParams0); + + if (startParams.port) { + this._logService.trace(`${logPrefix} - startParams debug port ${startParams.port}`); + } + this._logService.trace(`${logPrefix} - startParams language: ${startParams.language}`); + this._logService.trace(`${logPrefix} - startParams env: ${JSON.stringify(startParams.env)}`); + + if (isReconnection) { + // This is a reconnection + if (!this._extHostConnections[reconnectionToken]) { + if (!this._allReconnectionTokens.has(reconnectionToken)) { + // This is an unknown reconnection token + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (never seen)`); + } else { + // This is a connection that was seen in the past, but is no longer valid + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (seen before)`); + } + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {}))); + const dataChunk = protocol.readEntireBuffer(); + protocol.dispose(); + this._extHostConnections[reconnectionToken].acceptReconnection(remoteAddress, socket, dataChunk); + + } else { + // This is a fresh connection + if (this._extHostConnections[reconnectionToken]) { + // Cannot have two concurrent connections using the same reconnection token + return this._rejectWebSocketConnection(logPrefix, protocol, `Duplicate reconnection token`); + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {}))); + const dataChunk = protocol.readEntireBuffer(); + protocol.dispose(); + const con = new ExtensionHostConnection(this._environmentService, this._logService, reconnectionToken, remoteAddress, socket, dataChunk); + this._extHostConnections[reconnectionToken] = con; + this._allReconnectionTokens.add(reconnectionToken); + con.onClose(() => { + delete this._extHostConnections[reconnectionToken]; + this._onDidCloseExtHostConnection(); + }); + con.start(startParams); + } + + } else if (msg.desiredConnectionType === ConnectionType.Tunnel) { + + const tunnelStartParams = msg.args; + this._createTunnel(protocol, tunnelStartParams); + + } else { + + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown initial data received`); + + } + } + + private async _createTunnel(protocol: PersistentProtocol, tunnelStartParams: ITunnelConnectionStartParams): Promise { + const remoteSocket = (protocol.getSocket()).socket; + const dataChunk = protocol.readEntireBuffer(); + protocol.dispose(); + + remoteSocket.pause(); + const localSocket = await this._connectTunnelSocket(tunnelStartParams.host, tunnelStartParams.port); + + if (dataChunk.byteLength > 0) { + localSocket.write(dataChunk.buffer); + } + + localSocket.on('end', () => remoteSocket.end()); + localSocket.on('close', () => remoteSocket.end()); + localSocket.on('error', () => remoteSocket.destroy()); + remoteSocket.on('end', () => localSocket.end()); + remoteSocket.on('close', () => localSocket.end()); + remoteSocket.on('error', () => localSocket.destroy()); + + localSocket.pipe(remoteSocket); + remoteSocket.pipe(localSocket); + } + + private _connectTunnelSocket(host: string, port: number): Promise { + return new Promise((c, e) => { + const socket = net.createConnection( + { + host: host, + port: port + }, () => { + socket.removeListener('error', e); + socket.pause(); + c(socket); + } + ); + + socket.once('error', e); + }); + } + + private _updateWithFreeDebugPort(startParams: IRemoteExtensionHostStartParams): Thenable { + if (typeof startParams.port === 'number') { + return findFreePort(startParams.port, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */).then(freePort => { + startParams.port = freePort; + return startParams; + }); + } + // No port clear debug configuration. + startParams.debugId = undefined; + startParams.port = undefined; + startParams.break = undefined; + return Promise.resolve(startParams); + } + + private async _onDidCloseExtHostConnection(): Promise { + if (!this._environmentService.args['enable-remote-auto-shutdown']) { + return; + } + + this._cancelShutdown(); + + const hasActiveExtHosts = !!Object.keys(this._extHostConnections).length; + if (!hasActiveExtHosts) { + console.log('Last EH closed, waiting before shutting down'); + this._logService.info('Last EH closed, waiting before shutting down'); + this._waitThenShutdown(); + } + } + + private _waitThenShutdown(): void { + if (!this._environmentService.args['enable-remote-auto-shutdown']) { + return; + } + + if (this._environmentService.args['remote-auto-shutdown-without-delay']) { + this._shutdown(); + } else { + this.shutdownTimer = setTimeout(() => { + this.shutdownTimer = undefined; + + this._shutdown(); + }, SHUTDOWN_TIMEOUT); + } + } + + private _shutdown(): void { + const hasActiveExtHosts = !!Object.keys(this._extHostConnections).length; + if (hasActiveExtHosts) { + console.log('New EH opened, aborting shutdown'); + this._logService.info('New EH opened, aborting shutdown'); + return; + } else { + console.log('Last EH closed, shutting down'); + this._logService.info('Last EH closed, shutting down'); + this.dispose(); + process.exit(0); + } + } + + /** + * If the server is in a shutdown timeout, cancel it and start over + */ + private _delayShutdown(): void { + if (this.shutdownTimer) { + console.log('Got delay-shutdown request while in shutdown timeout, delaying'); + this._logService.info('Got delay-shutdown request while in shutdown timeout, delaying'); + this._cancelShutdown(); + this._waitThenShutdown(); + } + } + + private _cancelShutdown(): void { + if (this.shutdownTimer) { + console.log('Cancelling previous shutdown timeout'); + this._logService.info('Cancelling previous shutdown timeout'); + clearTimeout(this.shutdownTimer); + this.shutdownTimer = undefined; + } + } +} + +function parseConnectionToken(args: ServerParsedArgs): { connectionToken: string; connectionTokenIsMandatory: boolean; } { + if (args['connection-secret']) { + if (args['connectionToken']) { + console.warn(`Please do not use the argument connectionToken at the same time as connection-secret.`); + process.exit(1); + } + let rawConnectionToken = fs.readFileSync(args['connection-secret']).toString(); + rawConnectionToken = rawConnectionToken.replace(/\r?\n$/, ''); + if (!/^[0-9A-Za-z\-]+$/.test(rawConnectionToken)) { + console.warn(`The secret defined in ${args['connection-secret']} does not adhere to the characters 0-9, a-z, A-Z or -.`); + process.exit(1); + } + return { connectionToken: rawConnectionToken, connectionTokenIsMandatory: true }; + } else { + return { connectionToken: args['connectionToken'] || generateUuid(), connectionTokenIsMandatory: false }; + } +} + +export interface IServerAPI { + /** + * Do not remove!!. Called from vs/server/main.js + */ + handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise; + /** + * Do not remove!!. Called from vs/server/main.js + */ + handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void; + /** + * Do not remove!!. Called from vs/server/main.js + */ + handleServerError(err: Error): void; + /** + * Do not remove!!. Called from vs/server/main.js + */ + dispose(): void; +} + +export async function createServer(address: string | net.AddressInfo | null, args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise { + const productService = { _serviceBrand: undefined, ...product }; + const environmentService = new ServerEnvironmentService(args, productService); + + // + // On Windows, exit early with warning message to users about potential security issue + // if there is node_modules folder under home drive or Users folder. + // + if (process.platform === 'win32' && process.env.HOMEDRIVE && process.env.HOMEPATH) { + const homeDirModulesPath = join(process.env.HOMEDRIVE, 'node_modules'); + const userDir = dirname(join(process.env.HOMEDRIVE, process.env.HOMEPATH)); + const userDirModulesPath = join(userDir, 'node_modules'); + if (fs.existsSync(homeDirModulesPath) || fs.existsSync(userDirModulesPath)) { + const message = ` + +* +* !!!! Server terminated due to presence of CVE-2020-1416 !!!! +* +* Please remove the following directories and re-try +* ${homeDirModulesPath} +* ${userDirModulesPath} +* +* For more information on the vulnerability https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1416 +* + +`; + const logService = getOrCreateSpdLogService(environmentService); + logService.warn(message); + console.warn(message); + process.exit(0); + } + } + + const { connectionToken, connectionTokenIsMandatory } = parseConnectionToken(args); + const hasWebClient = fs.existsSync(FileAccess.asFileUri('vs/code/browser/workbench/workbench.html', require).fsPath); + + if (hasWebClient && address && typeof address !== 'string') { + // ships the web ui! + console.log(`Web UI available at http://localhost${address.port === 80 ? '' : `:${address.port}`}/?tkn=${connectionToken}`); + } + + const remoteExtensionHostAgentServer = new RemoteExtensionHostAgentServer(environmentService, productService, connectionToken, connectionTokenIsMandatory, hasWebClient, REMOTE_DATA_FOLDER); + const services = await remoteExtensionHostAgentServer.initialize(); + const { telemetryService } = services; + + perf.mark('code/server/ready'); + const currentTime = performance.now(); + const vscodeServerStartTime: number = (global).vscodeServerStartTime; + const vscodeServerListenTime: number = (global).vscodeServerListenTime; + const vscodeServerCodeLoadedTime: number = (global).vscodeServerCodeLoadedTime; + + type ServerStartClassification = { + startTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + startedTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + codeLoadedTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + readyTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + }; + type ServerStartEvent = { + startTime: number; + startedTime: number; + codeLoadedTime: number; + readyTime: number; + }; + telemetryService.publicLog2('serverStart', { + startTime: vscodeServerStartTime, + startedTime: vscodeServerListenTime, + codeLoadedTime: vscodeServerCodeLoadedTime, + readyTime: currentTime + }); + + if (args['print-startup-performance']) { + const stats = LoaderStats.get(); + let output = ''; + output += '\n\n### Load AMD-module\n'; + output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.amdLoad); + output += '\n\n### Load commonjs-module\n'; + output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.nodeRequire); + output += '\n\n### Invoke AMD-module factory\n'; + output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.amdInvoke); + output += '\n\n### Invoke commonjs-module\n'; + output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.nodeEval); + output += `Start-up time: ${vscodeServerListenTime - vscodeServerStartTime}\n`; + output += `Code loading time: ${vscodeServerCodeLoadedTime - vscodeServerStartTime}\n`; + output += `Initialized time: ${currentTime - vscodeServerStartTime}\n`; + output += `\n`; + console.log(output); + } + return remoteExtensionHostAgentServer; +} + +const getOrCreateSpdLogService: (environmentService: IServerEnvironmentService) => ILogService = (function () { + let _logService: ILogService | null; + return function getLogService(environmentService: IServerEnvironmentService): ILogService { + if (!_logService) { + _logService = new LogService(new SpdLogLogger(RemoteExtensionLogFileName, join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, getLogLevel(environmentService))); + } + return _logService; + }; +})(); diff --git a/src/vs/server/remoteExtensionHostProcess.ts b/src/vs/server/remoteExtensionHostProcess.ts new file mode 100644 index 0000000000..b43ea6602c --- /dev/null +++ b/src/vs/server/remoteExtensionHostProcess.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { startExtensionHostProcess } from 'vs/workbench/services/extensions/node/extensionHostProcessSetup'; + +startExtensionHostProcess().catch((err) => console.log(err)); diff --git a/src/vs/server/remoteExtensionManagement.ts b/src/vs/server/remoteExtensionManagement.ts new file mode 100644 index 0000000000..606d128d88 --- /dev/null +++ b/src/vs/server/remoteExtensionManagement.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 { PersistentProtocol, ProtocolConstants, ISocket } from 'vs/base/parts/ipc/common/ipc.net'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Emitter, Event } from 'vs/base/common/event'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; + +export interface IExtensionsManagementProcessInitData { + args: NativeParsedArgs; +} + +export function printTime(ms: number): string { + let h = 0; + let m = 0; + let s = 0; + if (ms >= 1000) { + s = Math.floor(ms / 1000); + ms -= s * 1000; + } + if (s >= 60) { + m = Math.floor(s / 60); + s -= m * 60; + } + if (m >= 60) { + h = Math.floor(m / 60); + m -= h * 60; + } + const _h = h ? `${h}h` : ``; + const _m = m ? `${m}m` : ``; + const _s = s ? `${s}s` : ``; + const _ms = ms ? `${ms}ms` : ``; + return `${_h}${_m}${_s}${_ms}`; +} + +export class ManagementConnection { + + private _onClose = new Emitter(); + public readonly onClose: Event = this._onClose.event; + + private readonly _reconnectionGraceTime: number; + private readonly _reconnectionShortGraceTime: number; + private _remoteAddress: string; + + public readonly protocol: PersistentProtocol; + private _disposed: boolean; + private _disconnectRunner1: ProcessTimeRunOnceScheduler; + private _disconnectRunner2: ProcessTimeRunOnceScheduler; + + constructor( + private readonly _logService: ILogService, + private readonly _reconnectionToken: string, + remoteAddress: string, + protocol: PersistentProtocol + ) { + this._reconnectionGraceTime = ProtocolConstants.ReconnectionGraceTime; + this._reconnectionShortGraceTime = ProtocolConstants.ReconnectionShortGraceTime; + this._remoteAddress = remoteAddress; + + this.protocol = protocol; + this._disposed = false; + this._disconnectRunner1 = new ProcessTimeRunOnceScheduler(() => { + this._log(`The reconnection grace time of ${printTime(this._reconnectionGraceTime)} has expired, so the connection will be disposed.`); + this._cleanResources(); + }, this._reconnectionGraceTime); + this._disconnectRunner2 = new ProcessTimeRunOnceScheduler(() => { + this._log(`The reconnection short grace time of ${printTime(this._reconnectionShortGraceTime)} has expired, so the connection will be disposed.`); + this._cleanResources(); + }, this._reconnectionShortGraceTime); + + this.protocol.onDidDispose(() => { + this._log(`The client has disconnected gracefully, so the connection will be disposed.`); + this._cleanResources(); + }); + this.protocol.onSocketClose(() => { + this._log(`The client has disconnected, will wait for reconnection ${printTime(this._reconnectionGraceTime)} before disposing...`); + // The socket has closed, let's give the renderer a certain amount of time to reconnect + this._disconnectRunner1.schedule(); + }); + + this._log(`New connection established.`); + } + + private _log(_str: string): void { + this._logService.info(`[${this._remoteAddress}][${this._reconnectionToken.substr(0, 8)}][ManagementConnection] ${_str}`); + } + + public shortenReconnectionGraceTimeIfNecessary(): void { + if (this._disconnectRunner2.isScheduled()) { + // we are disconnected and already running the short reconnection timer + return; + } + if (this._disconnectRunner1.isScheduled()) { + this._log(`Another client has connected, will shorten the wait for reconnection ${printTime(this._reconnectionShortGraceTime)} before disposing...`); + // we are disconnected and running the long reconnection timer + this._disconnectRunner2.schedule(); + } + } + + private _cleanResources(): void { + if (this._disposed) { + // already called + return; + } + this._disposed = true; + this._disconnectRunner1.dispose(); + this._disconnectRunner2.dispose(); + const socket = this.protocol.getSocket(); + this.protocol.sendDisconnect(); + this.protocol.dispose(); + socket.end(); + this._onClose.fire(undefined); + } + + public acceptReconnection(remoteAddress: string, socket: ISocket, initialDataChunk: VSBuffer): void { + this._remoteAddress = remoteAddress; + this._log(`The client has reconnected.`); + this._disconnectRunner1.cancel(); + this._disconnectRunner2.cancel(); + this.protocol.beginAcceptReconnection(socket, initialDataChunk); + this.protocol.endAcceptReconnection(); + } +} diff --git a/src/vs/server/remoteFileSystemProviderIpc.ts b/src/vs/server/remoteFileSystemProviderIpc.ts new file mode 100644 index 0000000000..dd648307e6 --- /dev/null +++ b/src/vs/server/remoteFileSystemProviderIpc.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { IFileChange, IWatchOptions } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { DiskFileSystemProvider, IWatcherOptions } from 'vs/platform/files/node/diskFileSystemProvider'; +import { posix, delimiter } from 'vs/base/common/path'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import { AbstractDiskFileSystemProviderChannel, ISessionFileWatcher } from 'vs/platform/files/node/diskFileSystemProviderIpc'; + +export class RemoteAgentFileSystemProviderChannel extends AbstractDiskFileSystemProviderChannel { + + private readonly uriTransformerCache = new Map(); + + constructor( + logService: ILogService, + private readonly environmentService: IServerEnvironmentService + ) { + super(new DiskFileSystemProvider(logService), logService); + + this._register(this.provider); + } + + protected override getUriTransformer(ctx: RemoteAgentConnectionContext): IURITransformer { + let transformer = this.uriTransformerCache.get(ctx.remoteAuthority); + if (!transformer) { + transformer = createRemoteURITransformer(ctx.remoteAuthority); + this.uriTransformerCache.set(ctx.remoteAuthority, transformer); + } + + return transformer; + } + + protected override transformIncoming(uriTransformer: IURITransformer, _resource: UriComponents, supportVSCodeResource = false): URI { + if (supportVSCodeResource && _resource.path === '/vscode-resource' && _resource.query) { + const requestResourcePath = JSON.parse(_resource.query).requestResourcePath; + + return URI.from({ scheme: 'file', path: requestResourcePath }); + } + + return URI.revive(uriTransformer.transformIncoming(_resource)); + } + + //#region File Watching + + protected createSessionFileWatcher(uriTransformer: IURITransformer, emitter: Emitter): ISessionFileWatcher { + return new SessionFileWatcher(uriTransformer, emitter, this.logService, this.environmentService); + } + + //#endregion +} + +class SessionFileWatcher extends Disposable implements ISessionFileWatcher { + + private readonly watcherRequests = new Map(); + private readonly fileWatcher = this._register(new DiskFileSystemProvider(this.logService, { watcher: this.getWatcherOptions() })); + + constructor( + private readonly uriTransformer: IURITransformer, + sessionEmitter: Emitter, + private readonly logService: ILogService, + private readonly environmentService: IServerEnvironmentService + ) { + super(); + + this.registerListeners(sessionEmitter); + } + + private registerListeners(sessionEmitter: Emitter): void { + const localChangeEmitter = this._register(new Emitter()); + + this._register(localChangeEmitter.event((events) => { + sessionEmitter.fire( + events.map(e => ({ + resource: this.uriTransformer.transformOutgoingURI(e.resource), + type: e.type + })) + ); + })); + + this._register(this.fileWatcher.onDidChangeFile(events => localChangeEmitter.fire(events))); + this._register(this.fileWatcher.onDidErrorOccur(error => sessionEmitter.fire(error))); + } + + private getWatcherOptions(): IWatcherOptions | undefined { + const fileWatcherPolling = this.environmentService.args['fileWatcherPolling']; + if (fileWatcherPolling) { + const segments = fileWatcherPolling.split(delimiter); + const pollingInterval = Number(segments[0]); + if (pollingInterval > 0) { + const usePolling = segments.length > 1 ? segments.slice(1) : true; + return { usePolling, pollingInterval }; + } + } + + return undefined; + } + + watch(req: number, resource: URI, opts: IWatchOptions): IDisposable { + if (this.environmentService.extensionsPath) { + // when opening the $HOME folder, we end up watching the extension folder + // so simply exclude watching the extensions folder + opts.excludes = [...(opts.excludes || []), posix.join(this.environmentService.extensionsPath, '**')]; + } + + this.watcherRequests.set(req, this.fileWatcher.watch(resource, opts)); + + return toDisposable(() => { + dispose(this.watcherRequests.get(req)); + this.watcherRequests.delete(req); + }); + } + + override dispose(): void { + super.dispose(); + + this.watcherRequests.forEach(disposable => dispose(disposable)); + this.watcherRequests.clear(); + } +} diff --git a/src/vs/server/remoteLanguagePacks.ts b/src/vs/server/remoteLanguagePacks.ts new file mode 100644 index 0000000000..80e0e51482 --- /dev/null +++ b/src/vs/server/remoteLanguagePacks.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { FileAccess } from 'vs/base/common/network'; +import * as path from 'vs/base/common/path'; + +import * as lp from 'vs/base/node/languagePacks'; +import product from 'vs/platform/product/common/product'; + +const metaData = path.join(FileAccess.asFileUri('', require).fsPath, 'nls.metadata.json'); +const _cache: Map> = new Map(); + +function exists(file: string) { + return new Promise(c => fs.exists(file, c)); +} + +export function getNLSConfiguration(language: string, userDataPath: string): Promise { + return exists(metaData).then((fileExists) => { + if (!fileExists || !product.commit) { + // console.log(`==> MetaData or commit unknown. Using default language.`); + return Promise.resolve({ locale: 'en', availableLanguages: {} }); + } + let key = `${language}||${userDataPath}`; + let result = _cache.get(key); + if (!result) { + result = lp.getNLSConfiguration(product.commit, userDataPath, metaData, language).then(value => { + if (InternalNLSConfiguration.is(value)) { + value._languagePackSupport = true; + } + return value; + }); + _cache.set(key, result); + } + return result; + }); +} + +export namespace InternalNLSConfiguration { + export function is(value: lp.NLSConfiguration): value is lp.InternalNLSConfiguration { + let candidate: lp.InternalNLSConfiguration = value as lp.InternalNLSConfiguration; + return candidate && typeof candidate._languagePackId === 'string'; + } +} diff --git a/src/vs/server/remoteTelemetryService.ts b/src/vs/server/remoteTelemetryService.ts new file mode 100644 index 0000000000..57ad6afd90 --- /dev/null +++ b/src/vs/server/remoteTelemetryService.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; +import { NullTelemetryServiceShape } from 'vs/platform/telemetry/common/telemetryUtils'; + +export interface IRemoteTelemetryService extends ITelemetryService { + permanentlyDisableTelemetry(): void +} + +export class RemoteTelemetryService extends TelemetryService implements IRemoteTelemetryService { + private _isDisabled = false; + constructor( + config: ITelemetryServiceConfig, + @IConfigurationService _configurationService: IConfigurationService + ) { + super(config, _configurationService); + } + + override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { + if (this._isDisabled) { + return Promise.resolve(undefined); + } + return super.publicLog(eventName, data, anonymizeFilePaths); + } + + override publicLog2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean): Promise { + if (this._isDisabled) { + return Promise.resolve(undefined); + } + return super.publicLog2(eventName, data, anonymizeFilePaths); + } + + override publicLogError(errorEventName: string, data?: ITelemetryData): Promise { + if (this._isDisabled) { + return Promise.resolve(undefined); + } + return super.publicLogError(errorEventName, data); + } + + override publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck): Promise { + if (this._isDisabled) { + return Promise.resolve(undefined); + } + return super.publicLogError2(eventName, data); + } + + permanentlyDisableTelemetry(): void { + this._isDisabled = true; + this.dispose(); + } +} + +export const RemoteNullTelemetryService = new class extends NullTelemetryServiceShape implements IRemoteTelemetryService { + permanentlyDisableTelemetry(): void { return; } // No-op, telemetry is already disabled +}; + +export const IRemoteTelemetryService = refineServiceDecorator(ITelemetryService); diff --git a/src/vs/server/remoteTerminalChannel.ts b/src/vs/server/remoteTerminalChannel.ts new file mode 100644 index 0000000000..65f602dc50 --- /dev/null +++ b/src/vs/server/remoteTerminalChannel.ts @@ -0,0 +1,328 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import { Emitter, Event } from 'vs/base/common/event'; +import { cloneAndChange } from 'vs/base/common/objects'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as path from 'vs/base/common/path'; +import * as platform from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { createRandomIPCHandle } from 'vs/base/parts/ipc/node/ipc.net'; +import { ILogService } from 'vs/platform/log/common/log'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IPtyService, IShellLaunchConfig, ITerminalProfile, ITerminalsLayoutInfo } from 'vs/platform/terminal/common/terminal'; +import { IGetTerminalLayoutInfoArgs, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { CLIServerBase, ICommandsExecuter } from 'vs/workbench/api/node/extHostCLIServer'; +import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { ICreateTerminalProcessArguments, ICreateTerminalProcessResult, IWorkspaceFolderData } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { buildUserEnvironment } from 'vs/server/extensionHostConnection'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import { IProductService } from 'vs/platform/product/common/productService'; + +class CustomVariableResolver extends AbstractVariableResolverService { + constructor( + env: platform.IProcessEnvironment, + workspaceFolders: IWorkspaceFolder[], + activeFileResource: URI | undefined, + resolvedVariables: { [name: string]: string; } + ) { + super({ + getFolderUri: (folderName: string): URI | undefined => { + const found = workspaceFolders.filter(f => f.name === folderName); + if (found && found.length > 0) { + return found[0].uri; + } + return undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspaceFolders.length; + }, + getConfigurationValue: (folderUri: URI, section: string): string | undefined => { + return resolvedVariables[`config:${section}`]; + }, + getExecPath: (): string | undefined => { + return env['VSCODE_EXEC_PATH']; + }, + getAppRoot: (): string | undefined => { + return env['VSCODE_CWD']; + }, + getFilePath: (): string | undefined => { + if (activeFileResource) { + return path.normalize(activeFileResource.fsPath); + } + return undefined; + }, + getSelectedText: (): string | undefined => { + return resolvedVariables['selectedText']; + }, + getLineNumber: (): string | undefined => { + return resolvedVariables['lineNumber']; + } + }, undefined, Promise.resolve(env)); + } +} + +export class RemoteTerminalChannel extends Disposable implements IServerChannel { + + private _lastReqId = 0; + private readonly _pendingCommands = new Map void; + reject: (err: any) => void; + uriTransformer: IURITransformer; + }>(); + + private readonly _onExecuteCommand = this._register(new Emitter<{ reqId: number, commandId: string, commandArgs: any[] }>()); + readonly onExecuteCommand = this._onExecuteCommand.event; + + constructor( + private readonly _environmentService: IServerEnvironmentService, + private readonly _logService: ILogService, + private readonly _ptyService: IPtyService, + private readonly _productService: IProductService + ) { + super(); + } + + async call(ctx: RemoteAgentConnectionContext, command: string, args?: any): Promise { + switch (command) { + case '$restartPtyHost': return this._ptyService.restartPtyHost?.apply(this._ptyService, args); + + case '$createProcess': { + const uriTransformer = createRemoteURITransformer(ctx.remoteAuthority); + return this._createProcess(uriTransformer, args); + } + case '$attachToProcess': return this._ptyService.attachToProcess.apply(this._ptyService, args); + case '$detachFromProcess': return this._ptyService.detachFromProcess.apply(this._ptyService, args); + + case '$listProcesses': return this._ptyService.listProcesses.apply(this._ptyService, args); + case '$orphanQuestionReply': return this._ptyService.orphanQuestionReply.apply(this._ptyService, args); + case '$acceptPtyHostResolvedVariables': return this._ptyService.acceptPtyHostResolvedVariables?.apply(this._ptyService, args); + + case '$start': return this._ptyService.start.apply(this._ptyService, args); + case '$input': return this._ptyService.input.apply(this._ptyService, args); + case '$acknowledgeDataEvent': return this._ptyService.acknowledgeDataEvent.apply(this._ptyService, args); + case '$shutdown': return this._ptyService.shutdown.apply(this._ptyService, args); + case '$resize': return this._ptyService.resize.apply(this._ptyService, args); + case '$getInitialCwd': return this._ptyService.getInitialCwd.apply(this._ptyService, args); + case '$getCwd': return this._ptyService.getCwd.apply(this._ptyService, args); + + case '$processBinary': return this._ptyService.processBinary.apply(this._ptyService, args); + + case '$sendCommandResult': return this._sendCommandResult(args[0], args[1], args[2]); + case '$getDefaultSystemShell': return this._getDefaultSystemShell.apply(this, args); + case '$getProfiles': return this._getProfiles.apply(this, args); + case '$getEnvironment': return this._getEnvironment(); + case '$getWslPath': return this._getWslPath(args[0]); + case '$getTerminalLayoutInfo': return this._getTerminalLayoutInfo(args); + case '$setTerminalLayoutInfo': return this._setTerminalLayoutInfo(args); + case '$serializeTerminalState': return this._ptyService.serializeTerminalState.apply(this._ptyService, args); + case '$reviveTerminalProcesses': return this._ptyService.reviveTerminalProcesses.apply(this._ptyService, args); + case '$reduceConnectionGraceTime': return this._reduceConnectionGraceTime(); + case '$updateIcon': return this._ptyService.updateIcon.apply(this._ptyService, args); + case '$updateTitle': return this._ptyService.updateTitle.apply(this._ptyService, args); + case '$updateProperty': return this._ptyService.updateProperty.apply(this._ptyService, args); + case '$refreshProperty': return this._ptyService.refreshProperty.apply(this._ptyService, args); + case '$requestDetachInstance': return this._ptyService.requestDetachInstance(args[0], args[1]); + case '$acceptDetachedInstance': return this._ptyService.acceptDetachInstanceReply(args[0], args[1]); + } + + throw new Error(`IPC Command ${command} not found`); + } + + listen(_: any, event: string, arg: any): Event { + switch (event) { + case '$onPtyHostExitEvent': return this._ptyService.onPtyHostExit || Event.None; + case '$onPtyHostStartEvent': return this._ptyService.onPtyHostStart || Event.None; + case '$onPtyHostUnresponsiveEvent': return this._ptyService.onPtyHostUnresponsive || Event.None; + case '$onPtyHostResponsiveEvent': return this._ptyService.onPtyHostResponsive || Event.None; + case '$onPtyHostRequestResolveVariablesEvent': return this._ptyService.onPtyHostRequestResolveVariables || Event.None; + case '$onProcessDataEvent': return this._ptyService.onProcessData; + case '$onProcessReadyEvent': return this._ptyService.onProcessReady; + case '$onProcessExitEvent': return this._ptyService.onProcessExit; + case '$onProcessReplayEvent': return this._ptyService.onProcessReplay; + case '$onProcessOrphanQuestion': return this._ptyService.onProcessOrphanQuestion; + case '$onExecuteCommand': return this.onExecuteCommand; + case '$onDidRequestDetach': return this._ptyService.onDidRequestDetach || Event.None; + case '$onDidChangeProperty': return this._ptyService.onDidChangeProperty; + default: + break; + } + + throw new Error('Not supported'); + } + + private async _createProcess(uriTransformer: IURITransformer, args: ICreateTerminalProcessArguments): Promise { + const shellLaunchConfig: IShellLaunchConfig = { + name: args.shellLaunchConfig.name, + executable: args.shellLaunchConfig.executable, + args: args.shellLaunchConfig.args, + cwd: ( + typeof args.shellLaunchConfig.cwd === 'string' || typeof args.shellLaunchConfig.cwd === 'undefined' + ? args.shellLaunchConfig.cwd + : URI.revive(uriTransformer.transformIncoming(args.shellLaunchConfig.cwd)) + ), + env: args.shellLaunchConfig.env, + useShellEnvironment: args.shellLaunchConfig.useShellEnvironment + }; + + + let baseEnv: platform.IProcessEnvironment; + if (args.shellLaunchConfig.useShellEnvironment) { + this._logService.trace('*'); + baseEnv = await buildUserEnvironment(args.resolverEnv, platform.language, false, this._environmentService, this._logService); + } else { + baseEnv = this._getEnvironment(); + } + this._logService.trace('baseEnv', baseEnv); + + const reviveWorkspaceFolder = (workspaceData: IWorkspaceFolderData): IWorkspaceFolder => { + return { + uri: URI.revive(uriTransformer.transformIncoming(workspaceData.uri)), + name: workspaceData.name, + index: workspaceData.index, + toResource: () => { + throw new Error('Not implemented'); + } + }; + }; + const workspaceFolders = args.workspaceFolders.map(reviveWorkspaceFolder); + const activeWorkspaceFolder = args.activeWorkspaceFolder ? reviveWorkspaceFolder(args.activeWorkspaceFolder) : undefined; + const activeFileResource = args.activeFileResource ? URI.revive(uriTransformer.transformIncoming(args.activeFileResource)) : undefined; + const customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables); + const variableResolver = terminalEnvironment.createVariableResolver(activeWorkspaceFolder, process.env, customVariableResolver); + + // Get the initial cwd + const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), variableResolver, activeWorkspaceFolder?.uri, args.configuration['terminal.integrated.cwd'], this._logService); + shellLaunchConfig.cwd = initialCwd; + + const envPlatformKey = platform.isWindows ? 'terminal.integrated.env.windows' : (platform.isMacintosh ? 'terminal.integrated.env.osx' : 'terminal.integrated.env.linux'); + const envFromConfig = args.configuration[envPlatformKey]; + const env = terminalEnvironment.createTerminalEnvironment( + shellLaunchConfig, + envFromConfig, + variableResolver, + this._productService.version, + args.configuration['terminal.integrated.detectLocale'], + baseEnv + ); + + // Apply extension environment variable collections to the environment + if (!shellLaunchConfig.strictEnv) { + const entries: [string, IEnvironmentVariableCollection][] = []; + for (const [k, v] of args.envVariableCollections) { + entries.push([k, { map: deserializeEnvironmentVariableCollection(v) }]); + } + const envVariableCollections = new Map(entries); + const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections); + mergedCollection.applyToProcessEnvironment(env); + } + + // Fork the process and listen for messages + this._logService.debug(`Terminal process launching on remote agent`, { shellLaunchConfig, initialCwd, cols: args.cols, rows: args.rows, env }); + + // Setup the CLI server to support forwarding commands run from the CLI + const ipcHandlePath = createRandomIPCHandle(); + env.VSCODE_IPC_HOOK_CLI = ipcHandlePath; + const commandsExecuter: ICommandsExecuter = { + executeCommand: (id: string, ...args: any[]): Promise => this._executeCommand(id, args, uriTransformer) + }; + const cliServer = new CLIServerBase(commandsExecuter, this._logService, ipcHandlePath); + + const id = await this._ptyService.createProcess(shellLaunchConfig, initialCwd, args.cols, args.rows, args.unicodeVersion, env, baseEnv, false, args.shouldPersistTerminal, args.workspaceId, args.workspaceName); + this._ptyService.onProcessExit(e => e.id === id && cliServer.dispose()); + + return { + persistentTerminalId: id, + resolvedShellLaunchConfig: shellLaunchConfig + }; + } + + private _executeCommand(commandId: string, commandArgs: any[], uriTransformer: IURITransformer): Promise { + let resolve!: (data: any) => void; + let reject!: (err: any) => void; + const result = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + const reqId = ++this._lastReqId; + this._pendingCommands.set(reqId, { resolve, reject, uriTransformer }); + + const serializedCommandArgs = cloneAndChange(commandArgs, (obj) => { + if (obj && obj.$mid === 1) { + // this is UriComponents + return uriTransformer.transformOutgoing(obj); + } + if (obj && obj instanceof URI) { + return uriTransformer.transformOutgoingURI(obj); + } + return undefined; + }); + this._onExecuteCommand.fire({ + reqId, + commandId, + commandArgs: serializedCommandArgs + }); + + return result; + } + + private _sendCommandResult(reqId: number, isError: boolean, serializedPayload: any): void { + const data = this._pendingCommands.get(reqId); + if (!data) { + return; + } + this._pendingCommands.delete(reqId); + const payload = cloneAndChange(serializedPayload, (obj) => { + if (obj && obj.$mid === 1) { + // this is UriComponents + return data.uriTransformer.transformIncoming(obj); + } + return undefined; + }); + if (isError) { + data.reject(payload); + } else { + data.resolve(payload); + } + } + + private _getDefaultSystemShell(osOverride?: platform.OperatingSystem): Promise { + return this._ptyService.getDefaultSystemShell(osOverride); + } + + private async _getProfiles(workspaceId: string, profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise { + return this._ptyService.getProfiles?.(workspaceId, profiles, defaultProfile, includeDetectedProfiles) || []; + } + + private _getEnvironment(): platform.IProcessEnvironment { + return { ...process.env }; + } + + private _getWslPath(original: string): Promise { + return this._ptyService.getWslPath(original); + } + + private _setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void { + this._ptyService.setTerminalLayoutInfo(args); + } + + private async _getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise { + return this._ptyService.getTerminalLayoutInfo(args); + } + + private _reduceConnectionGraceTime(): Promise { + return this._ptyService.reduceConnectionGraceTime(); + } +} diff --git a/src/vs/server/remoteUriTransformer.ts b/src/vs/server/remoteUriTransformer.ts new file mode 100644 index 0000000000..0077752d61 --- /dev/null +++ b/src/vs/server/remoteUriTransformer.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URITransformer, IURITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; +import { FileAccess } from 'vs/base/common/network'; + +export const uriTransformerPath = FileAccess.asFileUri('vs/server/uriTransformer.js', require).fsPath; + +export function createRemoteURITransformer(remoteAuthority: string): IURITransformer { + const rawURITransformerFactory = require.__$__nodeRequire(uriTransformerPath); + const rawURITransformer = rawURITransformerFactory(remoteAuthority); + return new URITransformer(rawURITransformer); +} diff --git a/src/vs/server/serverEnvironmentService.ts b/src/vs/server/serverEnvironmentService.ts new file mode 100644 index 0000000000..c969e185eb --- /dev/null +++ b/src/vs/server/serverEnvironmentService.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { OPTIONS, OptionDescriptions } from 'vs/platform/environment/node/argv'; +import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; + +export const serverOptions: OptionDescriptions = { + 'port': { type: 'string' }, + 'connectionToken': { type: 'string' }, + 'connection-secret': { type: 'string', description: nls.localize('connection-secret', "Path to file that contains the connection token. This will require that all incoming connections know the secret.") }, + 'host': { type: 'string' }, + 'socket-path': { type: 'string' }, + 'driver': { type: 'string' }, + 'start-server': { type: 'boolean' }, + 'print-startup-performance': { type: 'boolean' }, + 'print-ip-address': { type: 'boolean' }, + 'disable-websocket-compression': { type: 'boolean' }, + + 'fileWatcherPolling': { type: 'string' }, + + 'enable-remote-auto-shutdown': { type: 'boolean' }, + 'remote-auto-shutdown-without-delay': { type: 'boolean' }, + + 'without-browser-env-var': { type: 'boolean' }, + + 'disable-telemetry': OPTIONS['disable-telemetry'], + + 'extensions-dir': OPTIONS['extensions-dir'], + 'extensions-download-dir': OPTIONS['extensions-download-dir'], + 'install-extension': OPTIONS['install-extension'], + 'install-builtin-extension': OPTIONS['install-builtin-extension'], + 'uninstall-extension': OPTIONS['uninstall-extension'], + 'locate-extension': OPTIONS['locate-extension'], + 'list-extensions': OPTIONS['list-extensions'], + 'force': OPTIONS['force'], + 'show-versions': OPTIONS['show-versions'], + 'category': OPTIONS['category'], + 'do-not-sync': OPTIONS['do-not-sync'], + + 'force-disable-user-env': OPTIONS['force-disable-user-env'], + + 'folder': { type: 'string' }, + 'workspace': { type: 'string' }, + 'web-user-data-dir': { type: 'string' }, + 'use-host-proxy': { type: 'string' }, + 'enable-sync': { type: 'boolean' }, + 'github-auth': { type: 'string' }, + 'log': { type: 'string' }, + 'logsPath': { type: 'string' }, + + _: OPTIONS['_'] +}; + +export interface ServerParsedArgs { + port?: string; + connectionToken?: string; + /** + * A path to a filename which will be read on startup. + * Consider placing this file in a folder readable only by the same user (a `chmod 0700` directory). + * + * The contents of the file will be used as the connectionToken. Use only `[0-9A-Z\-]` as contents in the file. + * The file can optionally end in a `\n` which will be ignored. + * + * This secret must be communicated to any vscode instance via the resolver or embedder API. + */ + 'connection-secret'?: string; + host?: string; + 'socket-path'?: string; + driver?: string; + 'print-startup-performance'?: boolean; + 'print-ip-address'?: boolean; + 'disable-websocket-compression'?: boolean; + 'disable-telemetry'?: boolean; + fileWatcherPolling?: string; + 'start-server'?: boolean; + + 'enable-remote-auto-shutdown'?: boolean; + 'remote-auto-shutdown-without-delay'?: boolean; + + 'extensions-dir'?: string; + 'extensions-download-dir'?: string; + 'install-extension'?: string[]; + 'install-builtin-extension'?: string[]; + 'uninstall-extension'?: string[]; + 'list-extensions'?: boolean; + 'locate-extension'?: string[]; + 'show-versions'?: boolean; + 'category'?: string; + + 'force-disable-user-env'?: boolean; + 'use-host-proxy'?: string; + + 'without-browser-env-var'?: boolean; + + force?: boolean; // used by install-extension + 'do-not-sync'?: boolean; // used by install-extension + + 'user-data-dir'?: string; + 'builtin-extensions-dir'?: string; + + // web + workspace: string; + folder: string; + 'web-user-data-dir'?: string; + 'enable-sync'?: boolean; + 'github-auth'?: string; + 'log'?: string; + 'logsPath'?: string; + + _: string[]; +} + +export const IServerEnvironmentService = refineServiceDecorator(IEnvironmentService); + +export interface IServerEnvironmentService extends INativeEnvironmentService { + readonly args: ServerParsedArgs; +} + +export class ServerEnvironmentService extends NativeEnvironmentService implements IServerEnvironmentService { + override get args(): ServerParsedArgs { return super.args as ServerParsedArgs; } +} diff --git a/src/vs/server/uriTransformer.js b/src/vs/server/uriTransformer.js new file mode 100644 index 0000000000..b4e0abb8a3 --- /dev/null +++ b/src/vs/server/uriTransformer.js @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check + +/** + * ``` + * -------------------------------- + * | UI SIDE | AGENT SIDE | + * |---------------|--------------| + * | vscode-remote | file | + * | file | vscode-local | + * -------------------------------- + * ``` + * @typedef { import('../base/common/uriIpc').IRawURITransformer } IRawURITransformer + * @typedef { import('../base/common/uriIpc').UriParts } UriParts + * @typedef { import('../base/common/uri').UriComponents } UriComponents + * @param {string} remoteAuthority + * @returns {IRawURITransformer} + */ +module.exports = function(remoteAuthority) { + return { + /** + * @param {UriParts} uri + * @returns {UriParts} + */ + transformIncoming: (uri) => { + if (uri.scheme === 'vscode-remote') { + return { scheme: 'file', path: uri.path, query: uri.query, fragment: uri.fragment }; + } + if (uri.scheme === 'file') { + return { scheme: 'vscode-local', path: uri.path, query: uri.query, fragment: uri.fragment }; + } + return uri; + }, + /** + * @param {UriParts} uri + * @returns {UriParts} + */ + transformOutgoing: (uri) => { + if (uri.scheme === 'file') { + return { scheme: 'vscode-remote', authority: remoteAuthority, path: uri.path, query: uri.query, fragment: uri.fragment }; + } + if (uri.scheme === 'vscode-local') { + return { scheme: 'file', path: uri.path, query: uri.query, fragment: uri.fragment }; + } + return uri; + }, + /** + * @param {string} scheme + * @returns {string} + */ + transformOutgoingScheme: (scheme) => { + if (scheme === 'file') { + return 'vscode-remote'; + } else if (scheme === 'vscode-local') { + return 'file'; + } + return scheme; + } + }; +}; diff --git a/src/vs/server/webClientServer.ts b/src/vs/server/webClientServer.ts new file mode 100644 index 0000000000..659cfa7b02 --- /dev/null +++ b/src/vs/server/webClientServer.ts @@ -0,0 +1,354 @@ +/*--------------------------------------------------------------------------------------------- + * 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 http from 'http'; +import * as url from 'url'; +import * as util from 'util'; +import * as cookie from 'cookie'; +import * as crypto from 'crypto'; +import { isEqualOrParent, sanitizeFilePath } from 'vs/base/common/extpath'; +import { getMediaMime } from 'vs/base/common/mime'; +import { isLinux } from 'vs/base/common/platform'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import { extname, dirname, join, normalize } from 'vs/base/common/path'; +import { FileAccess } from 'vs/base/common/network'; +import { generateUuid } from 'vs/base/common/uuid'; +import { cwd } from 'vs/base/common/process'; +import { IProductService } from 'vs/platform/product/common/productService'; + +const textMimeType = { + '.html': 'text/html', + '.js': 'text/javascript', + '.json': 'application/json', + '.css': 'text/css', + '.svg': 'image/svg+xml', +} as { [ext: string]: string | undefined }; + +/** + * Return an error to the client. + */ +export async function serveError(req: http.IncomingMessage, res: http.ServerResponse, errorCode: number, errorMessage: string): Promise { + res.writeHead(errorCode, { 'Content-Type': 'text/plain' }); + res.end(errorMessage); +} + +/** + * Serve a file at a given path or 404 if the file is missing. + */ +export async function serveFile(logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, filePath: string, responseHeaders: Record = Object.create(null)): Promise { + try { + const stat = await util.promisify(fs.stat)(filePath); + + // Check if file modified since + const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) + if (req.headers['if-none-match'] === etag) { + res.writeHead(304); + return res.end(); + } + + // Headers + responseHeaders['Content-Type'] = textMimeType[extname(filePath)] || getMediaMime(filePath) || 'text/plain'; + responseHeaders['Etag'] = etag; + + res.writeHead(200, responseHeaders); + + // Data + fs.createReadStream(filePath).pipe(res); + } catch (error) { + if (error.code !== 'ENOENT') { + logService.error(error); + console.error(error.toString()); + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + return res.end('Not found'); + } +} + +const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath); + +export class WebClientServer { + + private _mapCallbackUriToRequestId: Map = new Map(); + + constructor( + private readonly _connectionToken: string, + private readonly _environmentService: IServerEnvironmentService, + private readonly _logService: ILogService, + private readonly _productService: IProductService + ) { } + + async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + try { + const pathname = parsedUrl.pathname!; + + if (pathname === '/favicon.ico' || pathname === '/manifest.json' || pathname === '/code-192.png' || pathname === '/code-512.png') { + // always serve icons/manifest, even without a token + return serveFile(this._logService, req, res, join(APP_ROOT, 'resources', 'server', pathname.substr(1))); + } + if (/^\/static\//.test(pathname)) { + // always serve static requests, even without a token + return this._handleStatic(req, res, parsedUrl); + } + if (pathname === '/') { + // the token handling is done inside the handler + return this._handleRoot(req, res, parsedUrl); + } + if (pathname === '/callback') { + // callback support + return this._handleCallback(req, res, parsedUrl); + } + if (pathname === '/fetch-callback') { + // callback fetch support + return this._handleFetchCallback(req, res, parsedUrl); + } + + return serveError(req, res, 404, 'Not found.'); + } catch (error) { + this._logService.error(error); + console.error(error.toString()); + + return serveError(req, res, 500, 'Internal Server Error.'); + } + } + + private _hasCorrectTokenCookie(req: http.IncomingMessage): boolean { + const cookies = cookie.parse(req.headers.cookie || ''); + return (cookies['vscode-tkn'] === this._connectionToken); + } + + /** + * Handle HTTP requests for /static/* + */ + private async _handleStatic(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + const headers: Record = Object.create(null); + + // Strip `/static/` from the path + const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20) + const relativeFilePath = normalize(normalizedPathname.substr('/static/'.length)); + + const filePath = join(APP_ROOT, relativeFilePath); + if (!isEqualOrParent(filePath, APP_ROOT, !isLinux)) { + return serveError(req, res, 400, `Bad request.`); + } + + return serveFile(this._logService, req, res, filePath, headers); + } + + /** + * Handle HTTP requests for / + */ + private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + if (!req.headers.host) { + return serveError(req, res, 400, `Bad request.`); + } + + const queryTkn = parsedUrl.query['tkn']; + if (typeof queryTkn === 'string') { + // tkn came in via a query string + // => set a cookie and redirect to url without tkn + const responseHeaders: Record = Object.create(null); + responseHeaders['Set-Cookie'] = cookie.serialize('vscode-tkn', queryTkn, { sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 /* 1 week */ }); + + const newQuery = Object.create(null); + for (let key in parsedUrl.query) { + if (key !== 'tkn') { + newQuery[key] = parsedUrl.query[key]; + } + } + const newLocation = url.format({ pathname: '/', query: newQuery }); + responseHeaders['Location'] = newLocation; + + res.writeHead(302, responseHeaders); + return res.end(); + } + + if (this._environmentService.isBuilt && !this._hasCorrectTokenCookie(req)) { + return serveError(req, res, 403, `Forbidden.`); + } + + const remoteAuthority = req.headers.host; + const transformer = createRemoteURITransformer(remoteAuthority); + const { workspacePath, isFolder } = await this._getWorkspaceFromCLI(); + + function escapeAttribute(value: string): string { + return value.replace(/"/g, '"'); + } + + let _wrapWebWorkerExtHostInIframe: undefined | false = undefined; + if (this._environmentService.driverHandle) { + // integration tests run at a time when the built output is not yet published to the CDN + // so we must disable the iframe wrapping because the iframe URL will give a 404 + _wrapWebWorkerExtHostInIframe = false; + } + + const filePath = FileAccess.asFileUri(this._environmentService.isBuilt ? 'vs/code/browser/workbench/workbench.html' : 'vs/code/browser/workbench/workbench-dev.html', require).fsPath; + const authSessionInfo = !this._environmentService.isBuilt && this._environmentService.args['github-auth'] ? { + id: generateUuid(), + providerId: 'github', + accessToken: this._environmentService.args['github-auth'], + scopes: [['user:email'], ['repo']] + } : undefined; + const data = (await util.promisify(fs.readFile)(filePath)).toString() + .replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({ + folderUri: (workspacePath && isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, + workspaceUri: (workspacePath && !isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, + remoteAuthority, + _wrapWebWorkerExtHostInIframe, + developmentOptions: { enableSmokeTestDriver: this._environmentService.driverHandle === 'web' ? true : undefined }, + settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, + }))) + .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : ''); + + const cspDirectives = [ + 'default-src \'self\';', + 'img-src \'self\' https: data: blob:;', + 'media-src \'none\';', + `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-cb2sg39EJV8ABaSNFfWu/ou8o1xVXYK7jp90oZ9vpcg=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html + 'child-src \'self\';', + `frame-src 'self' https://*.vscode-webview.net ${this._productService.webEndpointUrl || ''} data:;`, + 'worker-src \'self\' data:;', + 'style-src \'self\' \'unsafe-inline\';', + 'connect-src \'self\' ws: wss: https:;', + 'font-src \'self\' blob:;', + 'manifest-src \'self\';' + ].join(' '); + + res.writeHead(200, { + 'Content-Type': 'text/html', + // At this point we know the client has a valid cookie + // and we want to set it prolong it to ensure that this + // client is valid for another 1 week at least + 'Set-Cookie': cookie.serialize('vscode-tkn', this._connectionToken, { sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 /* 1 week */ }), + 'Content-Security-Policy': cspDirectives + }); + return res.end(data); + } + + private _getScriptCspHashes(content: string): string[] { + // Compute the CSP hashes for line scripts. Uses regex + // which means it isn't 100% good. + const regex = / + ${coreDependencies} +
+
- `; } @@ -346,6 +377,7 @@ export class BackLayerWebView extends Disposable { mimeTypes: renderer.mimeTypes, extends: renderer.extends, messaging: renderer.messaging !== RendererMessagingSpec.Never, + isBuiltin: renderer.isBuiltin }; }); } @@ -376,11 +408,70 @@ export class BackLayerWebView extends Disposable { return !!this.webview; } - createWebview(): void { + async createWebview(): Promise { const baseUrl = this.asWebviewUri(dirname(this.documentUri), undefined); - const htmlContent = this.generateContent(baseUrl.toString()); - this._initialize(htmlContent); - return; + + // Python notebooks assume that requirejs is a global. + // For all other notebooks, they need to provide their own loader. + if (!this.documentUri.path.toLowerCase().endsWith('.ipynb')) { + const htmlContent = this.generateContent('', baseUrl.toString()); + this._initialize(htmlContent); + return; + } + + let coreDependencies = ''; + let resolveFunc: () => void; + + this._initalized = new Promise((resolve, reject) => { + resolveFunc = resolve; + }); + + + if (!isWeb) { + const loaderUri = FileAccess.asFileUri('vs/loader.js', require); + const loader = this.asWebviewUri(loaderUri, undefined); + + coreDependencies = ``; + const htmlContent = this.generateContent(coreDependencies, baseUrl.toString()); + this._initialize(htmlContent); + resolveFunc!(); + } else { + const loaderUri = FileAccess.asBrowserUri('vs/loader.js', require); + + fetch(loaderUri.toString(true)).then(async response => { + if (response.status !== 200) { + throw new Error(response.statusText); + } + + const loaderJs = await response.text(); + + coreDependencies = ` + + +`; + + const htmlContent = this.generateContent(coreDependencies, baseUrl.toString()); + this._initialize(htmlContent); + resolveFunc!(); + }, error => { + // the fetch request is rejected + const htmlContent = this.generateContent(coreDependencies, baseUrl.toString()); + this._initialize(htmlContent); + resolveFunc!(); + }); + } + + await this._initalized; } private _initialize(content: string) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts index 683708c19a..920469071f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CellEditState, CellFocusMode, CellViewModelStateChangeEvent, INotebookEditor, NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, CellViewModelStateChangeEvent, INotebookEditorDelegate, NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -29,7 +29,7 @@ export class CellContextKeyManager extends Disposable { constructor( private readonly contextKeyService: IContextKeyService, - private readonly notebookEditor: INotebookEditor, + private readonly notebookEditor: INotebookEditorDelegate, private element: CodeCellViewModel | MarkupCellViewModel ) { super(); @@ -118,7 +118,7 @@ export class CellContextKeyManager extends Disposable { private updateForInternalMetadata() { const internalMetadata = this.element.internalMetadata; - this.cellEditable.set(!this.notebookEditor.viewModel?.options.isReadOnly); + this.cellEditable.set(!this.notebookEditor.isReadOnly); const runState = internalMetadata.runState; if (this.element instanceof MarkupCellViewModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts index 7746c9f039..e7ea3c05ac 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts @@ -7,7 +7,8 @@ import * as DOM from 'vs/base/browser/dom'; import { Delayer } from 'vs/base/common/async'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; -import { BaseCellRenderTemplate, expandCellRangesWithHiddenCells, ICellViewModel, INotebookCellList, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { expandCellRangesWithHiddenCells, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { cloneNotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellEditType, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { cellRangesToIndexes, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -42,7 +43,7 @@ export class CellDragAndDropController extends Disposable { private readonly listOnWillScrollListener = this._register(new MutableDisposable()); constructor( - private readonly notebookEditor: INotebookEditor, + private readonly notebookEditor: INotebookEditorDelegate, insertionIndicatorContainer: HTMLElement ) { super(); @@ -185,7 +186,7 @@ export class CellDragAndDropController extends Disposable { private getCellRangeAroundDragTarget(draggedCellIndex: number) { const selections = this.notebookEditor.getSelections(); - const modelRanges = expandCellRangesWithHiddenCells(this.notebookEditor, this.notebookEditor.viewModel!, selections); + const modelRanges = expandCellRangesWithHiddenCells(this.notebookEditor, selections); const nearestRange = modelRanges.find(range => range.start <= draggedCellIndex && draggedCellIndex < range.end); if (nearestRange) { @@ -209,15 +210,20 @@ export class CellDragAndDropController extends Disposable { const isCopy = (ctx.ctrlKey && !platform.isMacintosh) || (ctx.altKey && platform.isMacintosh); + if (!this.notebookEditor.hasModel()) { + return; + } + + const textModel = this.notebookEditor.textModel; + if (isCopy) { - const viewModel = this.notebookEditor.viewModel!; - const draggedCellIndex = this.notebookEditor.viewModel!.getCellIndex(draggedCell); + const draggedCellIndex = this.notebookEditor.getCellIndex(draggedCell); const range = this.getCellRangeAroundDragTarget(draggedCellIndex); - let originalToIdx = viewModel.getCellIndex(draggedOverCell); + let originalToIdx = this.notebookEditor.getCellIndex(draggedOverCell); if (dropDirection === 'below') { - const relativeToIndex = viewModel.getCellIndex(draggedOverCell); - const newIdx = viewModel.getNextVisibleCellIndex(relativeToIndex); + const relativeToIndex = this.notebookEditor.getCellIndex(draggedOverCell); + const newIdx = this.notebookEditor.getNextVisibleCellIndex(relativeToIndex); originalToIdx = newIdx; } @@ -233,23 +239,22 @@ export class CellDragAndDropController extends Disposable { finalFocus = { start: draggedCellIndex + delta, end: draggedCellIndex + delta + 1 }; } - viewModel.notebookDocument.applyEdits([ + textModel.applyEdits([ { editType: CellEditType.Replace, index: originalToIdx, count: 0, - cells: cellRangesToIndexes([range]).map(index => cloneNotebookCellTextModel(viewModel.viewCells[index].model)) + cells: cellRangesToIndexes([range]).map(index => cloneNotebookCellTextModel(this.notebookEditor.cellAt(index)!.model)) } - ], true, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: viewModel.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), undefined, true); + ], true, { kind: SelectionStateType.Index, focus: this.notebookEditor.getFocus(), selections: this.notebookEditor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), undefined, true); this.notebookEditor.revealCellRangeInView(finalSelection); } else { - const viewModel = this.notebookEditor.viewModel!; - const draggedCellIndex = this.notebookEditor.viewModel!.getCellIndex(draggedCell); + const draggedCellIndex = this.notebookEditor.getCellIndex(draggedCell); const range = this.getCellRangeAroundDragTarget(draggedCellIndex); - let originalToIdx = viewModel.getCellIndex(draggedOverCell); + let originalToIdx = this.notebookEditor.getCellIndex(draggedOverCell); if (dropDirection === 'below') { - const relativeToIndex = viewModel.getCellIndex(draggedOverCell); - const newIdx = viewModel.getNextVisibleCellIndex(relativeToIndex); + const relativeToIndex = this.notebookEditor.getCellIndex(draggedOverCell); + const newIdx = this.notebookEditor.getNextVisibleCellIndex(relativeToIndex); originalToIdx = newIdx; } @@ -269,14 +274,14 @@ export class CellDragAndDropController extends Disposable { finalFocus = { start: draggedCellIndex + delta, end: draggedCellIndex + delta + 1 }; } - viewModel.notebookDocument.applyEdits([ + textModel.applyEdits([ { editType: CellEditType.Move, index: range.start, length: range.end - range.start, newIdx: originalToIdx <= range.start ? originalToIdx : (originalToIdx - (range.end - range.start)) } - ], true, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: viewModel.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), undefined, true); + ], true, { kind: SelectionStateType.Index, focus: this.notebookEditor.getFocus(), selections: this.notebookEditor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), undefined, true); this.notebookEditor.revealCellRangeInView(finalSelection); } } @@ -301,7 +306,7 @@ export class CellDragAndDropController extends Disposable { dragHandle.setAttribute('draggable', 'true'); templateData.disposables.add(DOM.addDisposableListener(dragHandle, DOM.EventType.DRAG_END, () => { - if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled || !!this.notebookEditor.viewModel?.options.isReadOnly) { + if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled || !!this.notebookEditor.isReadOnly) { return; } @@ -315,7 +320,7 @@ export class CellDragAndDropController extends Disposable { return; } - if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled || !!this.notebookEditor.viewModel?.options.isReadOnly) { + if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled || !!this.notebookEditor.isReadOnly) { return; } @@ -332,7 +337,7 @@ export class CellDragAndDropController extends Disposable { } public startExplicitDrag(cell: ICellViewModel, _dragOffsetY: number) { - if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled || !!this.notebookEditor.viewModel?.options.isReadOnly) { + if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled || !!this.notebookEditor.isReadOnly) { return; } @@ -341,7 +346,7 @@ export class CellDragAndDropController extends Disposable { } public explicitDrag(cell: ICellViewModel, dragOffsetY: number) { - if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled || !!this.notebookEditor.viewModel?.options.isReadOnly) { + if (!this.notebookEditor.notebookOptions.getLayoutConfiguration().dragAndDropEnabled || !!this.notebookEditor.isReadOnly) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts index 13541f69cd..31375d4359 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts @@ -14,11 +14,12 @@ import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'v import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ActiveEditorContext } from 'vs/workbench/common/editor'; +import { INotebookCellToolbarActionContext, INotebookCommandContext, NotebookMultiCellAction, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { ICellViewModel, INotebookEditorDelegate, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class CellEditorOptions extends Disposable { @@ -37,7 +38,7 @@ export class CellEditorOptions extends Disposable { selectOnLineNumbers: false, lineNumbers: 'off', lineDecorationsWidth: 0, - folding: false, + folding: true, fixedOverflowWidgets: true, minimap: { enabled: false }, renderValidationDecorations: 'on', @@ -50,7 +51,7 @@ export class CellEditorOptions extends Disposable { readonly onDidChange: Event = this._onDidChange.event; private _localDisposableStore = this._register(new DisposableStore()); - constructor(readonly notebookEditor: INotebookEditor, readonly notebookOptions: NotebookOptions, readonly configurationService: IConfigurationService, readonly language: string) { + constructor(readonly notebookEditor: INotebookEditorDelegate, readonly notebookOptions: NotebookOptions, readonly configurationService: IConfigurationService, readonly language: string) { super(); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { @@ -68,7 +69,7 @@ export class CellEditorOptions extends Disposable { this._localDisposableStore.clear(); if (this.notebookEditor.hasModel()) { - this._localDisposableStore.add(this.notebookEditor.viewModel.onDidChangeOptions(() => { + this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { this._recomputeOptions(); })); @@ -77,7 +78,7 @@ export class CellEditorOptions extends Disposable { })); if (this.notebookEditor.hasModel()) { - this._localDisposableStore.add(this.notebookEditor.viewModel.onDidChangeOptions(() => { + this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { this._recomputeOptions(); })); } @@ -105,15 +106,22 @@ export class CellEditorOptions extends Disposable { const computed = { ...editorOptions, ...CellEditorOptions.fixedEditorOptions, - ... { lineNumbers, folding: lineNumbers === 'on' }, + ... { lineNumbers }, ...editorOptionsOverride, ...{ padding: { top: 12, bottom: 12 } }, - readOnly: this.notebookEditor.viewModel?.options.isReadOnly ?? false + readOnly: this.notebookEditor.isReadOnly }; return computed; } + getUpdatedValue(internalMetadata?: NotebookCellInternalMetadata): IEditorOptions { + const options = this.getValue(internalMetadata); + delete options.hover; // This is toggled by a debug editor contribution + + return options; + } + getValue(internalMetadata?: NotebookCellInternalMetadata): IEditorOptions { return { ...this._value, @@ -131,10 +139,8 @@ export class CellEditorOptions extends Disposable { const renderLiNumbers = this.configurationService.getValue<'on' | 'off'>('notebook.lineNumbers') === 'on'; const lineNumbers: LineNumbersType = renderLiNumbers ? 'on' : 'off'; this._value.lineNumbers = lineNumbers; - this._value.folding = lineNumbers === 'on'; } else { this._value.lineNumbers = lineNumbers as LineNumbersType; - this._value.folding = lineNumbers === 'on'; } this._onDidChange.fire(); } @@ -161,12 +167,6 @@ registerAction2(class ToggleLineNumberAction extends Action2 { title: { value: localize('notebook.toggleLineNumbers', "Toggle Notebook Line Numbers"), original: 'Toggle Notebook Line Numbers' }, precondition: NOTEBOOK_EDITOR_FOCUSED, menu: [ - { - id: MenuId.NotebookEditorLayoutConfigure, - group: 'notebookLayoutDetails', - order: 1, - when: NOTEBOOK_IS_ACTIVE_EDITOR - }, { id: MenuId.NotebookToolbar, group: 'notebookLayout', @@ -194,12 +194,12 @@ registerAction2(class ToggleLineNumberAction extends Action2 { } }); -registerAction2(class ToggleActiveLineNumberAction extends Action2 { +registerAction2(class ToggleActiveLineNumberAction extends NotebookMultiCellAction { constructor() { super({ id: 'notebook.cell.toggleLineNumbers', - title: 'Show Cell Line Numbers', - precondition: NOTEBOOK_EDITOR_FOCUSED, + title: localize('notebook.cell.toggleLineNumbers.title', "Show Cell Line Numbers"), + precondition: ActiveEditorContext.isEqualTo(NotebookEditor.ID), menu: [{ id: MenuId.NotebookCellTitle, group: 'View', @@ -212,34 +212,33 @@ registerAction2(class ToggleActiveLineNumberAction extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: { cell: ICellViewModel; }): Promise { - let cell = context?.cell; - if (!cell) { - const editor = getNotebookEditorFromEditorPane(accessor.get(IEditorService).activeEditorPane); - if (!editor || !editor.hasModel()) { - return; - } - - cell = editor.getActiveCell(); - } - - if (cell) { + async runWithContext(accessor: ServicesAccessor, context: INotebookCommandContext | INotebookCellToolbarActionContext): Promise { + if (context.ui) { + this.updateCell(accessor.get(IConfigurationService), context.cell); + } else { const configurationService = accessor.get(IConfigurationService); - const renderLineNumbers = configurationService.getValue<'on' | 'off'>('notebook.lineNumbers') === 'on'; - const cellLineNumbers = cell.lineNumbers; - // 'on', 'inherit' -> 'on' - // 'on', 'off' -> 'off' - // 'on', 'on' -> 'on' - // 'off', 'inherit' -> 'off' - // 'off', 'off' -> 'off' - // 'off', 'on' -> 'on' - const currentLineNumberIsOn = cellLineNumbers === 'on' || (cellLineNumbers === 'inherit' && renderLineNumbers); - - if (currentLineNumberIsOn) { - cell.lineNumbers = 'off'; - } else { - cell.lineNumbers = 'on'; - } + context.selectedCells.forEach(cell => { + this.updateCell(configurationService, cell); + }); } } + + private updateCell(configurationService: IConfigurationService, cell: ICellViewModel) { + const renderLineNumbers = configurationService.getValue<'on' | 'off'>('notebook.lineNumbers') === 'on'; + const cellLineNumbers = cell.lineNumbers; + // 'on', 'inherit' -> 'on' + // 'on', 'off' -> 'off' + // 'on', 'on' -> 'on' + // 'off', 'inherit' -> 'off' + // 'off', 'off' -> 'off' + // 'off', 'on' -> 'on' + const currentLineNumberIsOn = cellLineNumbers === 'on' || (cellLineNumbers === 'inherit' && renderLineNumbers); + + if (currentLineNumberIsOn) { + cell.lineNumbers = 'off'; + } else { + cell.lineNumbers = 'on'; + } + + } }); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts index f910157ba9..4fe7d4e720 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { Action, IAction } from 'vs/base/common/actions'; @@ -21,18 +22,20 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { CodeCellRenderTemplate, ICellOutputViewModel, ICellViewModel, IInsetRenderOutput, INotebookEditor, IRenderOutput, JUPYTER_EXTENSION_ID, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { ICellOutputViewModel, ICellViewModel, IInsetRenderOutput, INotebookEditorDelegate, IRenderOutput, JUPYTER_EXTENSION_ID, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { CodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { BUILTIN_RENDERER_ID, CellUri, IOrderedMimeType, NotebookCellOutputsSplice, RENDERER_NOT_AVAILABLE } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { OutputInnerContainerTopPadding } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; - +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -61,15 +64,20 @@ export class CellOutputElement extends Disposable { private readonly _renderDisposableStore = this._register(new DisposableStore()); private readonly _actionsDisposable = this._register(new MutableDisposable()); - innerContainer!: HTMLElement; + innerContainer?: HTMLElement; renderedOutputContainer!: HTMLElement; renderResult?: IRenderOutput; public useDedicatedDOM: boolean = true; + private _height: number = -1; get domOffsetHeight() { if (this.useDedicatedDOM) { - return this.innerContainer.offsetHeight; + if (this._height === -1) { + return this.innerContainer?.offsetHeight ?? 0; + } else { + return this._height; + } } else { return 0; } @@ -78,9 +86,10 @@ export class CellOutputElement extends Disposable { private readonly contextKeyService: IContextKeyService; constructor( - private notebookEditor: INotebookEditor, + private notebookEditor: INotebookEditorDelegate, private viewCell: CodeCellViewModel, - private outputContainer: HTMLElement, + private cellOutputContainer: CellOutputContainer, + private outputContainer: FastDomNode, readonly output: ICellOutputViewModel, @INotebookService private readonly notebookService: INotebookService, @IQuickInputService private readonly quickInputService: IQuickInputService, @@ -88,7 +97,7 @@ export class CellOutputElement extends Disposable { @IKeybindingService private readonly keybindingService: IKeybindingService, @IContextKeyService parentContextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, - @IViewletService private readonly viewletService: IViewletService, + @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, ) { super(); @@ -128,6 +137,12 @@ export class CellOutputElement extends Disposable { } } + forceReadDOM() { + if (this.useDedicatedDOM && this.innerContainer) { + this._height = this.innerContainer.offsetHeight; + } + } + updateDOMTop(top: number) { if (this.useDedicatedDOM) { if (this.innerContainer) { @@ -139,7 +154,18 @@ export class CellOutputElement extends Disposable { updateOutputData() { // update the content inside the domNode, do not need to worry about streaming if (!this.innerContainer) { - return; + if (this.renderResult) { + return; + } else { + // init rendering didn't happen + const currOutputIndex = this.cellOutputContainer.renderedOutputEntries.findIndex(entry => entry.element === this); + const previousSibling = currOutputIndex > 0 && !!(this.cellOutputContainer.renderedOutputEntries[currOutputIndex - 1].element.innerContainer?.parentElement) + ? this.cellOutputContainer.renderedOutputEntries[currOutputIndex - 1].element.innerContainer + : undefined; + this.render(previousSibling); + this._relayoutCell(); + return; + } } // user chooses another mimetype @@ -157,8 +183,8 @@ export class CellOutputElement extends Disposable { } // insert after previousSibling - private _generateInnerOutputContainer(previousSibling: HTMLElement | undefined, pickedMimeTypeRenderer: IOrderedMimeType) { - if (this.output.supportAppend()) { + private _generateInnerOutputContainer(previousSibling: HTMLElement | undefined, pickedMimeTypeRenderer: IOrderedMimeType, forceBreakStreaming: boolean) { + if (this.output.supportAppend() && !forceBreakStreaming) { // current output support append if (previousSibling) { if (this._divSupportAppend(previousSibling as HTMLElement | null, pickedMimeTypeRenderer.mimeType)) { @@ -168,21 +194,21 @@ export class CellOutputElement extends Disposable { this.useDedicatedDOM = true; this.innerContainer = DOM.$('.output-inner-container'); if (previousSibling.nextElementSibling) { - this.outputContainer.insertBefore(this.innerContainer, previousSibling.nextElementSibling); + this.outputContainer.domNode.insertBefore(this.innerContainer, previousSibling.nextElementSibling); } else { - this.outputContainer.appendChild(this.innerContainer); + this.outputContainer.domNode.appendChild(this.innerContainer); } } } else { // no previousSibling, append it to the very last - if (this._divSupportAppend(this.outputContainer.lastChild as HTMLElement | null, pickedMimeTypeRenderer.mimeType)) { + if (this._divSupportAppend(this.outputContainer.domNode.lastChild as HTMLElement | null, pickedMimeTypeRenderer.mimeType)) { // last element allows append this.useDedicatedDOM = false; - this.innerContainer = this.outputContainer.lastChild as HTMLElement; + this.innerContainer = this.outputContainer.domNode.lastChild as HTMLElement; } else { this.useDedicatedDOM = true; this.innerContainer = DOM.$('.output-inner-container'); - this.outputContainer.appendChild(this.innerContainer); + this.outputContainer.domNode.appendChild(this.innerContainer); } } } else { @@ -190,16 +216,27 @@ export class CellOutputElement extends Disposable { this.innerContainer = DOM.$('.output-inner-container'); if (previousSibling && previousSibling.nextElementSibling) { - this.outputContainer.insertBefore(this.innerContainer, previousSibling.nextElementSibling); + this.outputContainer.domNode.insertBefore(this.innerContainer, previousSibling.nextElementSibling); } else if (this.useDedicatedDOM) { - this.outputContainer.appendChild(this.innerContainer); + this.outputContainer.domNode.appendChild(this.innerContainer); } } this.innerContainer.setAttribute('output-mime-type', pickedMimeTypeRenderer.mimeType); + return this.innerContainer; } - render(previousSibling?: HTMLElement): IRenderResult | undefined { + private _initHeightChecked = false; + + probeHeight(index: number) { + if (!this._initHeightChecked && this.renderResult?.type === RenderOutputType.Mainframe) { + // postponed DOM read + const offsetHeight = this.domOffsetHeight; + this.viewCell.updateOutputHeight(index, offsetHeight, 'CellOutputElement#renderResultInitHeight'); + } + } + + render(previousSibling: HTMLElement | undefined, forceBreakStreaming: boolean = false): IRenderResult | undefined { const index = this.viewCell.outputsViewModels.indexOf(this.output); if (this.viewCell.metadata.outputCollapsed || !this.notebookEditor.hasModel()) { @@ -211,7 +248,7 @@ export class CellOutputElement extends Disposable { return undefined; } - const notebookTextModel = this.notebookEditor.viewModel.notebookDocument; + const notebookTextModel = this.notebookEditor.textModel; const [mimeTypes, pick] = this.output.resolveMimeTypes(notebookTextModel, this.notebookEditor.activeKernel?.preloadProvides); @@ -223,10 +260,10 @@ export class CellOutputElement extends Disposable { const pickedMimeTypeRenderer = mimeTypes[pick]; // generate an innerOutputContainer only when needed, for text streaming, it will reuse the previous element's container - this._generateInnerOutputContainer(previousSibling, pickedMimeTypeRenderer); - this._attachToolbar(this.innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, mimeTypes); + const innerContainer = this._generateInnerOutputContainer(previousSibling, pickedMimeTypeRenderer, forceBreakStreaming); + this._attachToolbar(innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, mimeTypes); - this.renderedOutputContainer = DOM.append(this.innerContainer, DOM.$('.rendered-output')); + this.renderedOutputContainer = DOM.append(innerContainer, DOM.$('.rendered-output')); if (pickedMimeTypeRenderer.rendererId !== BUILTIN_RENDERER_ID) { const renderer = this.notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); @@ -246,10 +283,10 @@ export class CellOutputElement extends Disposable { if (this.renderResult.type !== RenderOutputType.Mainframe) { this.notebookEditor.createOutput(this.viewCell, this.renderResult, this.viewCell.getOutputOffset(index)); - this.innerContainer.classList.add('background'); + innerContainer.classList.add('background'); } else { - this.innerContainer.classList.add('foreground', 'output-element'); - this.innerContainer.style.position = 'absolute'; + innerContainer.classList.add('foreground', 'output-element'); + innerContainer.style.position = 'absolute'; } if (this.renderResult.type === RenderOutputType.Html || this.renderResult.type === RenderOutputType.Extension) { @@ -263,23 +300,40 @@ export class CellOutputElement extends Disposable { return { initRenderIsSynchronous: true }; } - // let's use resize listener for them - const offsetHeight = this.renderResult?.initHeight !== undefined ? this.renderResult?.initHeight : Math.ceil(this.innerContainer.offsetHeight); + let offsetHeight = 0; + if (this.renderResult?.initHeight) { + offsetHeight = this.renderResult.initHeight; + this._initHeightChecked = true; + } else { + const outputIndex = this.viewCell.outputsViewModels.indexOf(this.output); + const oldHeight = this.viewCell.getOutputHeight(outputIndex); + if (oldHeight > 0) { + offsetHeight = oldHeight; + this._initHeightChecked = true; + } else { + this._initHeightChecked = false; + } + } + const dimension = { width: this.viewCell.layoutInfo.editorWidth, height: offsetHeight }; - this._bindResizeListener(dimension); - this.viewCell.updateOutputHeight(index, offsetHeight, 'CellOutputElement#renderResultInitHeight'); + + // let's use resize listener for them + this._bindResizeListener(innerContainer, dimension); + if (this._initHeightChecked) { + this.viewCell.updateOutputHeight(index, offsetHeight, 'CellOutputElement#renderResultInitHeight'); + } const top = this.viewCell.getOutputOffsetInContainer(index); - this.innerContainer.style.top = `${top}px`; - return { initRenderIsSynchronous: true }; + innerContainer.style.top = `${top}px`; + return { initRenderIsSynchronous: this._initHeightChecked }; } - private _bindResizeListener(dimension: DOM.IDimension) { - const elementSizeObserver = getResizesObserver(this.innerContainer, dimension, () => { - if (this.outputContainer && document.body.contains(this.outputContainer)) { - const height = this.innerContainer.offsetHeight; + private _bindResizeListener(innerContainer: HTMLElement, dimension: DOM.IDimension) { + const elementSizeObserver = getResizesObserver(innerContainer, dimension, () => { + if (this.outputContainer && document.body.contains(this.outputContainer.domNode)) { + const height = elementSizeObserver.getHeight() + OutputInnerContainerTopPadding * 2; if (dimension.height === height) { return; @@ -295,6 +349,8 @@ export class CellOutputElement extends Disposable { height: height }; + this._initHeightChecked = true; + this._height = height; this._validateFinalOutputHeight(true); this.viewCell.updateOutputHeight(currIndex, height, 'CellOutputElement#outputResize'); this._relayoutCell(); @@ -332,7 +388,7 @@ export class CellOutputElement extends Disposable { const toolbar = this._renderDisposableStore.add(new ToolBar(mimeTypePicker, this.contextMenuService, { getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - renderDropdownAsChildElement: true + renderDropdownAsChildElement: false })); toolbar.context = { ui: true, @@ -343,7 +399,7 @@ export class CellOutputElement extends Disposable { // TODO: This could probably be a real registered action, but it has to talk to this output element const pickAction = new Action('notebook.output.pickMimetype', nls.localize('pickMimeType', "Choose Output Mimetype"), ThemeIcon.asClassName(mimetypeIcon), undefined, - async _context => this._pickActiveMimeTypeRenderer(notebookTextModel, kernel, this.output)); + async _context => this._pickActiveMimeTypeRenderer(outputItemDiv, notebookTextModel, kernel, this.output)); if (index === 0 && useConsolidatedButton) { const menu = this._renderDisposableStore.add(this.menuService.createMenu(MenuId.NotebookOutputToolbar, this.contextKeyService)); const updateMenuToolbar = () => { @@ -361,7 +417,7 @@ export class CellOutputElement extends Disposable { } } - private async _pickActiveMimeTypeRenderer(notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, viewModel: ICellOutputViewModel) { + private async _pickActiveMimeTypeRenderer(outputItemDiv: HTMLElement, notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, viewModel: ICellOutputViewModel) { const [mimeTypes, currIndex] = viewModel.resolveMimeTypes(notebookTextModel, kernel?.preloadProvides); const items: IMimeTypeRenderer[] = []; @@ -419,7 +475,7 @@ export class CellOutputElement extends Disposable { } // user chooses another mimetype - const nextElement = this.innerContainer.nextElementSibling; + const nextElement = outputItemDiv.nextElementSibling; this._renderDisposableStore.clear(); const element = this.innerContainer; if (element) { @@ -431,14 +487,14 @@ export class CellOutputElement extends Disposable { this.viewCell.updateOutputMinHeight(this.viewCell.layoutInfo.outputTotalHeight); const { mimeType, rendererId } = mimeTypes[pick.index]; - this.notebookService.updateMimePreferredRenderer(mimeType, rendererId); + this.notebookService.updateMimePreferredRenderer(notebookTextModel.viewType, mimeType, rendererId, mimeTypes.map(m => m.mimeType)); this.render(nextElement as HTMLElement); this._validateFinalOutputHeight(false); this._relayoutCell(); } private async _showJupyterExtension() { - const viewlet = await this.viewletService.openViewlet(EXTENSION_VIEWLET_ID, true); + const viewlet = await this.paneCompositeService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; view?.search(`@id:${JUPYTER_EXTENSION_ID}`); } @@ -466,12 +522,10 @@ export class CellOutputElement extends Disposable { } if (synchronous) { - this.viewCell.updateOutputMinHeight(0); - this.viewCell.layoutChange({ outputHeight: true }, 'CellOutputElement#_validateFinalOutputHeight_sync'); + this.viewCell.unlockOutputHeight(); } else { this._outputHeightTimer = setTimeout(() => { - this.viewCell.updateOutputMinHeight(0); - this.viewCell.layoutChange({ outputHeight: true }, 'CellOutputElement#_validateFinalOutputHeight_async_1000'); + this.viewCell.unlockOutputHeight(); }, 1000); } } @@ -481,9 +535,8 @@ export class CellOutputElement extends Disposable { } override dispose() { - this.viewCell.updateOutputMinHeight(0); - if (this._outputHeightTimer) { + this.viewCell.unlockOutputHeight(); clearTimeout(this._outputHeightTimer); } @@ -512,7 +565,7 @@ export class CellOutputContainer extends Disposable { } constructor( - private notebookEditor: INotebookEditor, + private notebookEditor: INotebookEditorDelegate, private viewCell: CodeCellViewModel, private readonly templateData: CodeCellRenderTemplate, private options: { limit: number; }, @@ -536,6 +589,15 @@ export class CellOutputContainer extends Disposable { })); } + probeHeight() { + this._outputEntries.forEach(entry => { + const index = this.viewCell.outputsViewModels.indexOf(entry.model); + if (index >= 0) { + entry.element.probeHeight(index); + } + }); + } + render(editorHeight: number) { if (this.viewCell.outputsViewModels.length > 0) { if (this.viewCell.layoutInfo.totalHeight !== 0 && this.viewCell.layoutInfo.editorHeight > editorHeight) { @@ -543,17 +605,17 @@ export class CellOutputContainer extends Disposable { this._relayoutCell(); } - DOM.show(this.templateData.outputContainer); + DOM.show(this.templateData.outputContainer.domNode); for (let index = 0; index < Math.min(this.options.limit, this.viewCell.outputsViewModels.length); index++) { const currOutput = this.viewCell.outputsViewModels[index]; - const entry = this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this.templateData.outputContainer, currOutput); + const entry = this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, currOutput); this._outputEntries.push(new OutputEntryViewHandler(currOutput, entry)); - entry.render(); + entry.render(undefined); } this.viewCell.editorHeight = editorHeight; if (this.viewCell.outputsViewModels.length > this.options.limit) { - DOM.show(this.templateData.outputShowMoreContainer); + DOM.show(this.templateData.outputShowMoreContainer.domNode); this.viewCell.updateOutputShowMoreContainerHeight(46); } @@ -563,30 +625,32 @@ export class CellOutputContainer extends Disposable { // noop this.viewCell.editorHeight = editorHeight; this._relayoutCell(); - DOM.hide(this.templateData.outputContainer); + DOM.hide(this.templateData.outputContainer.domNode); } - this.templateData.outputShowMoreContainer.innerText = ''; + this.templateData.outputShowMoreContainer.domNode.innerText = ''; if (this.viewCell.outputsViewModels.length > this.options.limit) { - this.templateData.outputShowMoreContainer.appendChild(this._generateShowMoreElement(this.templateData.disposables)); + this.templateData.outputShowMoreContainer.domNode.appendChild(this._generateShowMoreElement(this.templateData.disposables)); } else { - DOM.hide(this.templateData.outputShowMoreContainer); + DOM.hide(this.templateData.outputShowMoreContainer.domNode); this.viewCell.updateOutputShowMoreContainerHeight(0); } } - viewUpdateShowOutputs(): void { + viewUpdateShowOutputs(initRendering: boolean): void { for (let index = 0; index < this._outputEntries.length; index++) { const viewHandler = this._outputEntries[index]; const outputEntry = viewHandler.element; if (outputEntry.renderResult) { if (outputEntry.renderResult.type !== RenderOutputType.Mainframe) { this.notebookEditor.createOutput(this.viewCell, outputEntry.renderResult as IInsetRenderOutput, this.viewCell.getOutputOffset(index)); - } else { + } else if (!initRendering) { + // force read otherwise the real height is updated in next frame through resize observer + outputEntry.forceReadDOM(); this.viewCell.updateOutputHeight(index, outputEntry.domOffsetHeight, 'CellOutputContainer#viewUpdateShowOutputs'); } } else { - outputEntry.render(); + outputEntry.render(undefined); } } @@ -607,12 +671,10 @@ export class CellOutputContainer extends Disposable { } if (synchronous) { - this.viewCell.updateOutputMinHeight(0); - this.viewCell.layoutChange({ outputHeight: true }, 'CellOutputContainer#_validateFinalOutputHeight_sync'); + this.viewCell.unlockOutputHeight(); } else { this._outputHeightTimer = setTimeout(() => { - this.viewCell.updateOutputMinHeight(0); - this.viewCell.layoutChange({ outputHeight: true }, 'CellOutputContainer#_validateFinalOutputHeight_async_1000'); + this.viewCell.unlockOutputHeight(); }, 1000); } } @@ -624,9 +686,9 @@ export class CellOutputContainer extends Disposable { this.viewCell.updateOutputMinHeight(previousOutputHeight); if (this.viewCell.outputsViewModels.length) { - DOM.show(this.templateData.outputContainer); + DOM.show(this.templateData.outputContainer.domNode); } else { - DOM.hide(this.templateData.outputContainer); + DOM.hide(this.templateData.outputContainer.domNode); } this.viewCell.spliceOutputHeights(splice.start, splice.deleteCount, splice.newOutputs.map(_ => 0)); @@ -658,14 +720,14 @@ export class CellOutputContainer extends Disposable { newlyInserted = newlyInserted.slice(0, this.options.limit - firstGroupEntries.length); const newlyInsertedEntries = newlyInserted.map(insert => { - return new OutputEntryViewHandler(insert, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this.templateData.outputContainer, insert)); + return new OutputEntryViewHandler(insert, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, insert)); }); this._outputEntries = [...firstGroupEntries, ...newlyInsertedEntries]; // render newly inserted outputs for (let i = firstGroupEntries.length; i < this._outputEntries.length; i++) { - const renderResult = this._outputEntries[i].element.render(); + const renderResult = this._outputEntries[i].element.render(undefined, i >= 1 && !this._outputEntries[i - 1].element.innerContainer); if (renderResult) { outputHasDynamicHeight = outputHasDynamicHeight || !renderResult.initRenderIsSynchronous; } @@ -687,7 +749,7 @@ export class CellOutputContainer extends Disposable { if (!entry.element.useDedicatedDOM) { entry.element.detach(); entry.element.dispose(); - secondGroupEntries[j] = new OutputEntryViewHandler(entry.model, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this.templateData.outputContainer, entry.model)); + secondGroupEntries[j] = new OutputEntryViewHandler(entry.model, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, entry.model)); reRenderRightBoundary++; } else { break; @@ -695,14 +757,14 @@ export class CellOutputContainer extends Disposable { } const newlyInsertedEntries = newlyInserted.map(insert => { - return new OutputEntryViewHandler(insert, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this.templateData.outputContainer, insert)); + return new OutputEntryViewHandler(insert, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, insert)); }); this._outputEntries = [...firstGroupEntries, ...newlyInsertedEntries, ...secondGroupEntries.slice(0, this.options.limit - firstGroupEntries.length - newlyInserted.length)]; for (let i = firstGroupEntries.length; i < reRenderRightBoundary; i++) { - const previousSibling = i - 1 >= 0 && this._outputEntries[i - 1] && this._outputEntries[i - 1].element.innerContainer.parentElement !== null ? this._outputEntries[i - 1].element.innerContainer : undefined; - const renderResult = this._outputEntries[i].element.render(previousSibling); + const previousSibling = i - 1 >= 0 && this._outputEntries[i - 1] && !!(this._outputEntries[i - 1].element.innerContainer?.parentElement) ? this._outputEntries[i - 1].element.innerContainer : undefined; + const renderResult = this._outputEntries[i].element.render(previousSibling, i >= 1 && !this._outputEntries[i - 1].element.innerContainer); if (renderResult) { outputHasDynamicHeight = outputHasDynamicHeight || !renderResult.initRenderIsSynchronous; } @@ -722,7 +784,7 @@ export class CellOutputContainer extends Disposable { if (!entry.element.useDedicatedDOM) { entry.element.detach(); entry.element.dispose(); - secondGroupEntries[j] = new OutputEntryViewHandler(entry.model, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this.templateData.outputContainer, entry.model)); + secondGroupEntries[j] = new OutputEntryViewHandler(entry.model, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, entry.model)); reRenderRightBoundary++; } else { break; @@ -730,7 +792,7 @@ export class CellOutputContainer extends Disposable { } const newlyInsertedEntries = newlyInserted.map(insert => { - return new OutputEntryViewHandler(insert, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this.templateData.outputContainer, insert)); + return new OutputEntryViewHandler(insert, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, insert)); }); let outputsNewlyAvailable: OutputEntryViewHandler[] = []; @@ -738,7 +800,7 @@ export class CellOutputContainer extends Disposable { if (firstGroupEntries.length + newlyInsertedEntries.length + secondGroupEntries.length < this.viewCell.outputsViewModels.length) { const last = Math.min(this.options.limit, this.viewCell.outputsViewModels.length); outputsNewlyAvailable = this.viewCell.outputsViewModels.slice(firstGroupEntries.length + newlyInsertedEntries.length + secondGroupEntries.length, last).map(output => { - return new OutputEntryViewHandler(output, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this.templateData.outputContainer, output)); + return new OutputEntryViewHandler(output, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, output)); }); } @@ -754,15 +816,15 @@ export class CellOutputContainer extends Disposable { // } // } else { for (let i = firstGroupEntries.length; i < reRenderRightBoundary; i++) { - const previousSibling = i - 1 >= 0 && this._outputEntries[i - 1] && this._outputEntries[i - 1].element.innerContainer.parentElement !== null ? this._outputEntries[i - 1].element.innerContainer : undefined; - const renderResult = this._outputEntries[i].element.render(previousSibling); + const previousSibling = i - 1 >= 0 && this._outputEntries[i - 1] && !!(this._outputEntries[i - 1].element.innerContainer?.parentElement) ? this._outputEntries[i - 1].element.innerContainer : undefined; + const renderResult = this._outputEntries[i].element.render(previousSibling, i >= 1 && !this._outputEntries[i - 1].element.innerContainer); if (renderResult) { outputHasDynamicHeight = outputHasDynamicHeight || !renderResult.initRenderIsSynchronous; } } for (let i = 0; i < outputsNewlyAvailable.length; i++) { - const renderResult = this._outputEntries[firstGroupEntries.length + newlyInserted.length + secondGroupEntries.length + i].element.render(); + const renderResult = this._outputEntries[firstGroupEntries.length + newlyInserted.length + secondGroupEntries.length + i].element.render(undefined); if (renderResult) { outputHasDynamicHeight = outputHasDynamicHeight || !renderResult.initRenderIsSynchronous; } @@ -771,13 +833,13 @@ export class CellOutputContainer extends Disposable { } if (this.viewCell.outputsViewModels.length > this.options.limit) { - DOM.show(this.templateData.outputShowMoreContainer); - if (!this.templateData.outputShowMoreContainer.hasChildNodes()) { - this.templateData.outputShowMoreContainer.appendChild(this._generateShowMoreElement(this.templateData.disposables)); + DOM.show(this.templateData.outputShowMoreContainer.domNode); + if (!this.templateData.outputShowMoreContainer.domNode.hasChildNodes()) { + this.templateData.outputShowMoreContainer.domNode.appendChild(this._generateShowMoreElement(this.templateData.disposables)); } this.viewCell.updateOutputShowMoreContainerHeight(46); } else { - DOM.hide(this.templateData.outputShowMoreContainer); + DOM.hide(this.templateData.outputShowMoreContainer.domNode); } const editorHeight = this.templateData.editor.getContentHeight(); @@ -796,11 +858,11 @@ export class CellOutputContainer extends Disposable { supportThemeIcons: true }; - const element = renderMarkdown(md, { + const rendered = renderMarkdown(md, { actionHandler: { callback: (content) => { if (content === 'command:workbench.action.openLargeOutput') { - this.openerService.open(CellUri.generateCellUri(this.notebookEditor.viewModel!.uri, this.viewCell.handle, Schemas.vscodeNotebookCellOutput)); + this.openerService.open(CellUri.generateCellUri(this.notebookEditor.textModel!.uri, this.viewCell.handle, Schemas.vscodeNotebookCellOutput)); } return undefined; // {{SQL CARBON EDIT}} fixing build break @@ -808,9 +870,10 @@ export class CellOutputContainer extends Disposable { disposables } }); + disposables.add(rendered); - element.classList.add('output-show-more'); - return element; + rendered.element.classList.add('output-show-more'); + return rendered.element; } private _relayoutCell() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index cde831c7da..127aed19a9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -5,6 +5,7 @@ import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; +import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; @@ -35,8 +36,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { DeleteCellAction, INotebookActionContext, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { BaseCellRenderTemplate, CodeCellLayoutInfo, CodeCellRenderTemplate, EXPAND_CELL_OUTPUT_COMMAND_ID, ICellViewModel, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookActionContext, INotebookCellActionContext, INotebookCellToolbarActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { DeleteCellAction } from 'vs/workbench/contrib/notebook/browser/controller/editActions'; +import { CodeCellLayoutInfo, EXPAND_CELL_OUTPUT_COMMAND_ID, ICellViewModel, INotebookEditorDelegate, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CodeCellRenderTemplate, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellDnd'; @@ -72,6 +75,10 @@ export class NotebookCellListDelegate extends Disposable implements IListVirtual return element.hasDynamicHeight(); } + getDynamicHeight(element: CellViewModel): number | null { + return element.getDynamicHeight(); + } + getTemplateId(element: CellViewModel): string { if (element.cellKind === CellKind.Markup) { return MarkupCellRenderer.TEMPLATE_ID; @@ -86,7 +93,7 @@ abstract class AbstractCellRenderer { constructor( protected readonly instantiationService: IInstantiationService, - protected readonly notebookEditor: INotebookEditor, + protected readonly notebookEditor: INotebookEditorDelegate, protected readonly contextMenuService: IContextMenuService, protected readonly menuService: IMenuService, configurationService: IConfigurationService, @@ -142,11 +149,11 @@ abstract class AbstractCellRenderer { const container = templateData.bottomCellContainer; const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; - container.style.top = `${bottomToolbarOffset}px`; + container.style.transform = `translateY(${bottomToolbarOffset}px)`; templateData.elementDisposables.add(element.onDidChangeLayout(() => { const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; - container.style.top = `${bottomToolbarOffset}px`; + container.style.transform = `translateY(${bottomToolbarOffset}px)`; })); } @@ -190,14 +197,14 @@ abstract class AbstractCellRenderer { if (actions.primary.length || actions.secondary.length) { templateData.container.classList.add('cell-has-toolbar-actions'); if (isCodeCellRenderTemplate(templateData)) { - templateData.focusIndicatorLeft.style.top = `${layoutInfo.editorToolbarHeight + layoutInfo.cellTopMargin}px`; - templateData.focusIndicatorRight.style.top = `${layoutInfo.editorToolbarHeight + layoutInfo.cellTopMargin}px`; + templateData.focusIndicatorLeft.domNode.style.transform = `translateY(${layoutInfo.editorToolbarHeight + layoutInfo.cellTopMargin}px)`; + templateData.focusIndicatorRight.domNode.style.transform = `translateY(${layoutInfo.editorToolbarHeight + layoutInfo.cellTopMargin}px)`; } } else { templateData.container.classList.remove('cell-has-toolbar-actions'); if (isCodeCellRenderTemplate(templateData)) { - templateData.focusIndicatorLeft.style.top = `${layoutInfo.cellTopMargin}px`; - templateData.focusIndicatorRight.style.top = `${layoutInfo.cellTopMargin}px`; + templateData.focusIndicatorLeft.domNode.style.transform = `translateY(${layoutInfo.cellTopMargin}px)`; + templateData.focusIndicatorRight.domNode.style.transform = `translateY(${layoutInfo.cellTopMargin}px)`; } } }; @@ -256,7 +263,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen static readonly TEMPLATE_ID = 'markdown_cell'; constructor( - notebookEditor: INotebookEditor, + notebookEditor: INotebookEditorDelegate, dndController: CellDragAndDropController, private renderedEditors: Map, contextKeyServiceProvider: (container: HTMLElement) => IContextKeyService, @@ -288,8 +295,8 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen } DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top')); - const focusIndicatorLeft = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); - const focusIndicatorRight = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right')); + const focusIndicatorLeft = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'))); + const focusIndicatorRight = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right'))); const codeInnerContent = DOM.append(container, $('.cell.code')); const editorPart = DOM.append(codeInnerContent, $('.cell-editor-part')); @@ -298,7 +305,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen editorPart.style.display = 'none'; const innerContent = DOM.append(container, $('.cell.markdown')); - const foldingIndicator = DOM.append(focusIndicatorLeft, DOM.$('.notebook-folding-indicator')); + const foldingIndicator = DOM.append(focusIndicatorLeft.domNode, DOM.$('.notebook-folding-indicator')); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); const betweenCellToolbar = disposables.add(this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService, this.notebookEditor.notebookOptions)); @@ -413,7 +420,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen // render toolbar first this.setupCellToolbarActions(templateData, elementDisposables); - const toolbarContext = { + const toolbarContext = { ui: true, cell: element, notebookEditor: this.notebookEditor, @@ -427,16 +434,17 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService])); const markdownCell = scopedInstaService.createInstance(StatefulMarkdownCell, this.notebookEditor, element, templateData, cellEditorOptions.getValue(element.internalMetadata), this.renderedEditors,); elementDisposables.add(markdownCell); - elementDisposables.add(cellEditorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(cellEditorOptions.getValue(element.internalMetadata)))); + elementDisposables.add(cellEditorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(cellEditorOptions.getUpdatedValue(element.internalMetadata)))); templateData.statusBar.update(toolbarContext); } private updateForLayout(element: MarkupCellViewModel, templateData: MarkdownCellRenderTemplate): void { const indicatorPostion = this.notebookEditor.notebookOptions.computeIndicatorPosition(element.layoutInfo.totalHeight, this.notebookEditor.textModel?.viewType); - templateData.focusIndicatorBottom.style.top = `${indicatorPostion.bottomIndicatorTop}px`; - templateData.focusIndicatorLeft.style.height = `${indicatorPostion.verticalIndicatorHeight}px`; - templateData.focusIndicatorRight.style.height = `${indicatorPostion.verticalIndicatorHeight}px`; + templateData.focusIndicatorBottom.style.transform = `translateY(${indicatorPostion.bottomIndicatorTop}px)`; + + templateData.focusIndicatorLeft.setHeight(indicatorPostion.verticalIndicatorHeight); + templateData.focusIndicatorRight.setHeight(indicatorPostion.verticalIndicatorHeight); templateData.container.classList.toggle('cell-statusbar-hidden', this.notebookEditor.notebookOptions.computeEditorStatusbarHeight(element.internalMetadata) === 0); } @@ -581,7 +589,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende static readonly TEMPLATE_ID = 'code_cell'; constructor( - notebookEditor: INotebookEditor, + notebookEditor: INotebookEditorDelegate, private renderedEditors: Map, dndController: CellDragAndDropController, contextKeyServiceProvider: (container: HTMLElement) => IContextKeyService, @@ -612,8 +620,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende if (!this.notebookEditor.creationOptions.isReadOnly) { deleteToolbar.setActions([this.instantiationService.createInstance(DeleteCellAction)]); } - const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); - const dragHandle = DOM.append(container, DOM.$('.cell-drag-handle')); + const focusIndicator = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'))); + const dragHandle = new FastDomNode(DOM.append(container, DOM.$('.cell-drag-handle'))); const cellContainer = DOM.append(container, $('.cell.code')); const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); @@ -621,6 +629,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const runToolbar = this.setupRunToolbar(runButtonContainer, container, contextKeyService, disposables); const executionOrderLabel = DOM.append(cellContainer, $('div.execution-count-label')); + executionOrderLabel.title = localize('cellExecutionOrderCountLabel', 'Execution Order'); const editorPart = DOM.append(cellContainer, $('.cell-editor-part')); const editorContainer = DOM.append(editorPart, $('.cell-editor-container')); @@ -653,16 +662,16 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); - const outputContainer = DOM.append(container, $('.output')); - const cellOutputCollapsedContainer = DOM.append(outputContainer, $('.output-collapse-container')); - const outputShowMoreContainer = DOM.append(container, $('.output-show-more-container')); + const outputContainer = new FastDomNode(DOM.append(container, $('.output'))); + const cellOutputCollapsedContainer = DOM.append(outputContainer.domNode, $('.output-collapse-container')); + const outputShowMoreContainer = new FastDomNode(DOM.append(container, $('.output-show-more-container'))); - const focusIndicatorRight = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right')); + const focusIndicatorRight = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right'))); const focusSinkElement = DOM.append(container, $('.cell-editor-focus-sink')); focusSinkElement.setAttribute('tabindex', '0'); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); - const focusIndicatorBottom = DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom')); + const focusIndicatorBottom = new FastDomNode(DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom'))); const betweenCellToolbar = this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService, this.notebookEditor.notebookOptions); const titleMenu = disposables.add(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.cellTitleToolbar, contextKeyService)); @@ -700,7 +709,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende toJSON: () => { return {}; } }; - this.dndController?.registerDragHandle(templateData, rootContainer, dragHandle, () => new CodeCellDragImageRenderer().getDragImage(templateData, templateData.editor, 'code')); + this.dndController?.registerDragHandle(templateData, rootContainer, dragHandle.domNode, () => new CodeCellDragImageRenderer().getDragImage(templateData, templateData.editor, 'code')); disposables.add(this.addCollapseClickCollapseHandler(templateData)); disposables.add(DOM.addDisposableListener(focusSinkElement, DOM.EventType.FOCUS, () => { @@ -755,21 +764,21 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } private addCollapseClickCollapseHandler(templateData: CodeCellRenderTemplate): IDisposable { - const dragHandleListener = DOM.addDisposableListener(templateData.dragHandle, DOM.EventType.DBLCLICK, e => { + const dragHandleListener = DOM.addDisposableListener(templateData.dragHandle.domNode, DOM.EventType.DBLCLICK, e => { const cell = templateData.currentRenderedCell; - if (!cell) { + if (!cell || !this.notebookEditor.hasModel()) { return; } const clickedOnInput = e.offsetY < (cell.layoutInfo as CodeCellLayoutInfo).outputContainerOffset; - const viewModel = this.notebookEditor.viewModel!; + const textModel = this.notebookEditor.textModel; const metadata: Partial = clickedOnInput ? { inputCollapsed: !cell.metadata.inputCollapsed } : { outputCollapsed: !cell.metadata.outputCollapsed }; - viewModel.notebookDocument.applyEdits([ + textModel.applyEdits([ { editType: CellEditType.PartialMetadata, - index: viewModel.getCellIndex(cell), + index: this.notebookEditor.getCellIndex(cell), metadata } ], true, undefined, () => undefined, undefined); @@ -777,18 +786,19 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const collapsedPartListener = DOM.addDisposableListener(templateData.cellInputCollapsedContainer, DOM.EventType.DBLCLICK, e => { const cell = templateData.currentRenderedCell; - if (!cell) { + if (!cell || !this.notebookEditor.hasModel()) { return; } const metadata: Partial = cell.metadata.inputCollapsed ? { inputCollapsed: false } : { outputCollapsed: false }; - const viewModel = this.notebookEditor.viewModel!; - viewModel.notebookDocument.applyEdits([ + const textModel = this.notebookEditor.textModel; + + textModel.applyEdits([ { editType: CellEditType.PartialMetadata, - index: viewModel.getCellIndex(cell), + index: this.notebookEditor.getCellIndex(cell), metadata } ], true, undefined, () => undefined, undefined); @@ -796,7 +806,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const clickHandler = DOM.addDisposableListener(templateData.cellInputCollapsedContainer, DOM.EventType.CLICK, e => { const cell = templateData.currentRenderedCell; - if (!cell) { + if (!cell || !this.notebookEditor.hasModel()) { return; } @@ -804,11 +814,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende if (element && element.classList && element.classList.contains('expandInputIcon')) { // clicked on the expand icon - const viewModel = this.notebookEditor.viewModel!; - viewModel.notebookDocument.applyEdits([ + const textModel = this.notebookEditor.textModel; + textModel.applyEdits([ { editType: CellEditType.PartialMetadata, - index: viewModel.getCellIndex(cell), + index: this.notebookEditor.getCellIndex(cell), metadata: { inputCollapsed: false } @@ -831,21 +841,22 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende actionViewItemProvider: _action => { actionViewItemDisposables.clear(); - const menu = actionViewItemDisposables.add(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.cellExecuteToolbar, contextKeyService)); - const actions = this.getCellToolbarActions(menu); - const primary = actions.primary[0]; + const primaryMenu = actionViewItemDisposables.add(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.cellExecutePrimary!, contextKeyService)); + const primary = this.getCellToolbarActions(primaryMenu).primary[0]; if (!(primary instanceof MenuItemAction)) { return undefined; } - if (!actions.secondary.length) { + const menu = actionViewItemDisposables.add(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.cellExecuteToolbar, contextKeyService)); + const secondary = this.getCellToolbarActions(menu).secondary; + if (!secondary.length) { return undefined; } const item = this.instantiationService.createInstance(DropdownWithPrimaryActionViewItem, primary, dropdownAction, - actions.secondary, + secondary, 'notebook-cell-run-toolbar', this.contextMenuService, { @@ -864,11 +875,12 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } private setupRunToolbar(runButtonContainer: HTMLElement, cellContainer: HTMLElement, contextKeyService: IContextKeyService, disposables: DisposableStore): ToolBar { - const menu = disposables.add(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.cellExecuteToolbar, contextKeyService)); + const menu = disposables.add(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.cellExecutePrimary!, contextKeyService)); const runToolbar = this.createRunCellToolbar(runButtonContainer, cellContainer, contextKeyService, disposables); const updateActions = () => { const actions = this.getCellToolbarActions(menu); - runToolbar.setActions(actions.primary); + const primary = actions.primary[0]; // Only allow one primary action + runToolbar.setActions(primary ? [primary] : []); }; updateActions(); disposables.add(menu.onDidChange(updateActions)); @@ -927,17 +939,18 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } private updateForLayout(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { - const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); - const bottomToolbarDimensions = this.notebookEditor.notebookOptions.computeBottomToolbarDimensions(this.notebookEditor.textModel?.viewType); + templateData.disposables.add(DOM.scheduleAtNextAnimationFrame(() => { + const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); + const bottomToolbarDimensions = this.notebookEditor.notebookOptions.computeBottomToolbarDimensions(this.notebookEditor.textModel?.viewType); + templateData.focusIndicatorLeft.setHeight(element.layoutInfo.indicatorHeight); + templateData.focusIndicatorRight.setHeight(element.layoutInfo.indicatorHeight); + templateData.focusIndicatorBottom.domNode.style.transform = `translateY(${element.layoutInfo.totalHeight - bottomToolbarDimensions.bottomToolbarGap - layoutInfo.cellBottomMargin}px)`; + templateData.outputContainer.setTop(element.layoutInfo.outputContainerOffset); + templateData.outputShowMoreContainer.setTop(element.layoutInfo.outputShowMoreContainerOffset); + templateData.dragHandle.setHeight(element.layoutInfo.totalHeight - bottomToolbarDimensions.bottomToolbarGap); - templateData.focusIndicatorLeft.style.height = `${element.layoutInfo.indicatorHeight}px`; - templateData.focusIndicatorRight.style.height = `${element.layoutInfo.indicatorHeight}px`; - templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - bottomToolbarDimensions.bottomToolbarGap - layoutInfo.cellBottomMargin}px`; - templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; - templateData.outputShowMoreContainer.style.top = `${element.layoutInfo.outputShowMoreContainerOffset}px`; - templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - bottomToolbarDimensions.bottomToolbarGap}px`; - - templateData.container.classList.toggle('cell-statusbar-hidden', this.notebookEditor.notebookOptions.computeEditorStatusbarHeight(element.internalMetadata) === 0); + templateData.container.classList.toggle('cell-statusbar-hidden', this.notebookEditor.notebookOptions.computeEditorStatusbarHeight(element.internalMetadata) === 0); + })); } renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { @@ -966,8 +979,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende return; } - templateData.outputContainer.innerText = ''; - const cellOutputCollapsedContainer = DOM.append(templateData.outputContainer, $('.output-collapse-container')); + templateData.outputContainer.domNode.innerText = ''; + const cellOutputCollapsedContainer = DOM.append(templateData.outputContainer.domNode, $('.output-collapse-container')); templateData.cellOutputCollapsedContainer = cellOutputCollapsedContainer; this.setupOutputCollapsedPart(templateData, cellOutputCollapsedContainer, element); @@ -997,8 +1010,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const cellEditorOptions = new CellEditorOptions(this.notebookEditor, this.notebookEditor.notebookOptions, this.configurationService, element.language); elementDisposables.add(cellEditorOptions); - elementDisposables.add(cellEditorOptions.onDidChange(() => templateData.editor.updateOptions(cellEditorOptions.getValue(element.internalMetadata)))); - templateData.editor.updateOptions(cellEditorOptions.getValue(element.internalMetadata)); + elementDisposables.add(cellEditorOptions.onDidChange(() => templateData.editor.updateOptions(cellEditorOptions.getUpdatedValue(element.internalMetadata)))); + templateData.editor.updateOptions(cellEditorOptions.getUpdatedValue(element.internalMetadata)); elementDisposables.add(new CellContextKeyManager(templateData.contextKeyService, this.notebookEditor, element)); @@ -1082,7 +1095,7 @@ export class ListTopCellToolbar extends Disposable { private toolbar: ToolBar; private readonly _modelDisposables = this._register(new DisposableStore()); constructor( - protected readonly notebookEditor: INotebookEditor, + protected readonly notebookEditor: INotebookEditorDelegate, contextKeyService: IContextKeyService, insertionIndicatorContainer: HTMLElement, @@ -1118,8 +1131,8 @@ export class ListTopCellToolbar extends Disposable { this._register(this.notebookEditor.onDidChangeModel(() => { this._modelDisposables.clear(); - if (this.notebookEditor.viewModel) { - this._modelDisposables.add(this.notebookEditor.viewModel.onDidChangeViewCells(() => { + if (this.notebookEditor.hasModel()) { + this._modelDisposables.add(this.notebookEditor.onDidChangeViewCells(() => { this.updateClass(); })); @@ -1136,7 +1149,7 @@ export class ListTopCellToolbar extends Disposable { } private updateClass() { - if (this.notebookEditor.viewModel?.length === 0) { + if (this.notebookEditor.getLength() === 0) { this.topCellToolbar.classList.add('emptyNotebook'); } else { this.topCellToolbar.classList.remove('emptyNotebook'); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts index 7b4b47a696..fe195c4b39 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts @@ -19,7 +19,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, ThemeColor } from 'vs/platform/theme/common/themeService'; -import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { CodeCellLayoutInfo, MarkdownCellLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellStatusbarAlignment, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index e11f10cb7e..915b41f8e3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -8,15 +8,18 @@ import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon, CSSIcon } from 'vs/base/common/codicons'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/editorCommon'; import { IReadonlyTextBuffer } from 'vs/editor/common/model'; import { TokenizationRegistry } from 'vs/editor/common/modes'; import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { CellFocusMode, CodeCellRenderTemplate, EXPAND_CELL_INPUT_COMMAND_ID, IActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, IActiveNotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { CellOutputContainer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellOutput'; import { ClickTargetType } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; @@ -29,15 +32,17 @@ export class CodeCell extends Disposable { private _renderedInputCollapseState: boolean | undefined; private _renderedOutputCollapseState: boolean | undefined; + private _isDisposed: boolean = false; constructor( - private readonly notebookEditor: IActiveNotebookEditor, + private readonly notebookEditor: IActiveNotebookEditorDelegate, private readonly viewCell: CodeCellViewModel, private readonly templateData: CodeCellRenderTemplate, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotebookCellStatusBarService readonly notebookCellStatusBarService: INotebookCellStatusBarService, @IKeybindingService readonly keybindingService: IKeybindingService, - @IOpenerService readonly openerService: IOpenerService + @IOpenerService readonly openerService: IOpenerService, + @IModeService readonly modeService: IModeService, ) { super(); @@ -46,6 +51,11 @@ export class CodeCell extends Disposable { const lineHeight = this.viewCell.layoutInfo.fontInfo?.lineHeight || 17; const editorPadding = this.notebookEditor.notebookOptions.computeEditorPadding(this.viewCell.internalMetadata); + // patch up focusMode + if (this.viewCell.focusMode === CellFocusMode.Editor && this.notebookEditor.getActiveCell() !== this.viewCell) { + this.viewCell.focusMode = CellFocusMode.Container; + } + const editorHeight = this.viewCell.layoutInfo.editorHeight === 0 ? lineNum * lineHeight + editorPadding.top + editorPadding.bottom : this.viewCell.layoutInfo.editorHeight; @@ -60,6 +70,10 @@ export class CodeCell extends Disposable { const cts = new CancellationTokenSource(); this._register({ dispose() { cts.dispose(true); } }); raceCancellation(viewCell.resolveTextModel(), cts.token).then(model => { + if (this._isDisposed) { + return; + } + if (model && templateData.editor) { templateData.editor.setModel(model); viewCell.attachTextEditor(templateData.editor); @@ -76,7 +90,7 @@ export class CodeCell extends Disposable { const realContentHeight = templateData.editor?.getContentHeight(); if (realContentHeight !== undefined && realContentHeight !== editorHeight) { - this.onCellHeightChange(realContentHeight); + this.onCellEditorHeightChange(realContentHeight); } focusEditorIfNeeded(); @@ -84,10 +98,6 @@ export class CodeCell extends Disposable { }); const updateForFocusMode = () => { - if (this.notebookEditor.getFocus().start !== this.notebookEditor.viewModel.getCellIndex(viewCell)) { - templateData.container.classList.toggle('cell-editor-focus', viewCell.focusMode === CellFocusMode.Editor); - } - if (viewCell.focusMode === CellFocusMode.Editor && this.notebookEditor.getActiveCell() === this.viewCell) { templateData.editor?.focus(); } @@ -101,13 +111,29 @@ export class CodeCell extends Disposable { })); updateForFocusMode(); - const updateEditorOptions = () => templateData.editor?.updateOptions({ readOnly: notebookEditor.viewModel.options.isReadOnly, padding: notebookEditor.notebookOptions.computeEditorPadding(viewCell.internalMetadata) }); + const updateEditorOptions = () => { + const editor = templateData.editor; + if (!editor) { + return; + } + + const isReadonly = notebookEditor.isReadOnly; + const padding = notebookEditor.notebookOptions.computeEditorPadding(viewCell.internalMetadata); + const options = editor.getOptions(); + if (options.get(EditorOption.readOnly) !== isReadonly || options.get(EditorOption.padding) !== padding) { + editor.updateOptions({ readOnly: notebookEditor.isReadOnly, padding: notebookEditor.notebookOptions.computeEditorPadding(viewCell.internalMetadata) }); + } + }; + updateEditorOptions(); this._register(viewCell.onDidChangeState((e) => { if (e.metadataChanged || e.internalMetadataChanged) { updateEditorOptions(); - if (this.updateForCollapseState()) { + this.viewCell.pauseLayout(); + const updated = this.updateForCollapseState(); + this.viewCell.resumeLayout(); + if (updated) { this.relayoutCell(); } } @@ -120,9 +146,7 @@ export class CodeCell extends Disposable { this.onCellWidthChange(); } } - })); - this._register(viewCell.onDidChangeLayout((e) => { if (e.totalHeight) { this.relayoutCell(); } @@ -131,7 +155,7 @@ export class CodeCell extends Disposable { this._register(templateData.editor.onDidContentSizeChange((e) => { if (e.contentHeightChanged) { if (this.viewCell.layoutInfo.editorHeight !== e.contentHeight) { - this.onCellHeightChange(e.contentHeight); + this.onCellEditorHeightChange(e.contentHeight); } } })); @@ -225,11 +249,15 @@ export class CodeCell extends Disposable { this._outputContainerRenderer = this.instantiationService.createInstance(CellOutputContainer, notebookEditor, viewCell, templateData, { limit: 500 }); this._outputContainerRenderer.render(editorHeight); // Need to do this after the intial renderOutput - if (this.viewCell.metadata.outputCollapsed === undefined && this.viewCell.metadata.outputCollapsed === undefined) { - this.viewUpdateExpanded(); + if (this.viewCell.metadata.outputCollapsed === undefined && this.viewCell.metadata.inputCollapsed === undefined) { + this.initialViewUpdateExpanded(); this.viewCell.layoutChange({}); } + this._register(this.viewCell.onLayoutInfoRead(() => { + this._outputContainerRenderer.probeHeight(); + })); + this.updateForCollapseState(); } @@ -250,7 +278,7 @@ export class CodeCell extends Disposable { if (this.viewCell.metadata.outputCollapsed) { this._collapseOutput(); } else { - this._showOutput(); + this._showOutput(false); } this.relayoutCell(); @@ -296,7 +324,7 @@ export class CodeCell extends Disposable { } private _getRichText(buffer: IReadonlyTextBuffer, language: string) { - return tokenizeToString(buffer.getLineContent(1), TokenizationRegistry.get(language)!); + return tokenizeToString(buffer.getLineContent(1), this.modeService.languageIdCodec, TokenizationRegistry.get(language)!); } private _removeInputCollapsePreview() { @@ -314,7 +342,7 @@ export class CodeCell extends Disposable { } private _updateOutputInnertContainer(hide: boolean) { - const children = this.templateData.outputContainer.children; + const children = this.templateData.outputContainer.domNode.children; for (let i = 0; i < children.length; i++) { if (children[i].classList.contains('output-inner-container')) { if (hide) { @@ -333,19 +361,18 @@ export class CodeCell extends Disposable { this._outputContainerRenderer.viewUpdateHideOuputs(); } - private _showOutput() { + private _showOutput(initRendering: boolean) { this.templateData.container.classList.toggle('output-collapsed', false); DOM.hide(this.templateData.cellOutputCollapsedContainer); this._updateOutputInnertContainer(false); - this._outputContainerRenderer.viewUpdateShowOutputs(); + this._outputContainerRenderer.viewUpdateShowOutputs(initRendering); } - private viewUpdateExpanded(): void { - this._showInput(); - this._showOutput(); + private initialViewUpdateExpanded(): void { this.templateData.container.classList.toggle('input-collapsed', false); + this._showInput(); this.templateData.container.classList.toggle('output-collapsed', false); - this._outputContainerRenderer.viewUpdateShowOutputs(); + this._showOutput(true); this.relayoutCell(); } @@ -369,7 +396,7 @@ export class CodeCell extends Disposable { ); } - private onCellHeightChange(newHeight: number): void { + private onCellEditorHeightChange(newHeight: number): void { const viewLayout = this.templateData.editor.getLayoutInfo(); this.viewCell.editorHeight = newHeight; this.relayoutCell(); @@ -386,11 +413,13 @@ export class CodeCell extends Disposable { } override dispose() { + this._isDisposed = true; + this.viewCell.detachTextEditor(); this._removeInputCollapsePreview(); this._outputContainerRenderer.dispose(); this._untrustedStatusItem?.dispose(); - this.templateData.focusIndicatorLeft.style.height = 'initial'; + this.templateData.focusIndicatorLeft.setHeight(0); super.dispose(); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index d3d615c52c..c82c402811 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -10,7 +10,7 @@ import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellEditState, CellFocusMode, MarkdownCellRenderTemplate, ICellViewModel, IActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, ICellViewModel, IActiveNotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellFoldingState } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -23,6 +23,8 @@ import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IReadonlyTextBuffer } from 'vs/editor/common/model'; import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { TokenizationRegistry } from 'vs/editor/common/modes'; +import { MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; +import { IModeService } from 'vs/editor/common/services/modeService'; export class StatefulMarkdownCell extends Disposable { @@ -38,7 +40,7 @@ export class StatefulMarkdownCell extends Disposable { private foldingState: CellFoldingState; constructor( - private readonly notebookEditor: IActiveNotebookEditor, + private readonly notebookEditor: IActiveNotebookEditorDelegate, private readonly viewCell: MarkupCellViewModel, private readonly templateData: MarkdownCellRenderTemplate, private editorOptions: IEditorOptions, @@ -46,6 +48,7 @@ export class StatefulMarkdownCell extends Disposable { @IContextKeyService private readonly contextKeyService: IContextKeyService, @INotebookCellStatusBarService readonly notebookCellStatusBarService: INotebookCellStatusBarService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IModeService private readonly modeService: IModeService, ) { super(); @@ -197,7 +200,7 @@ export class StatefulMarkdownCell extends Disposable { } private getRichText(buffer: IReadonlyTextBuffer, language: string) { - return tokenizeToString(buffer.getLineContent(1), TokenizationRegistry.get(language)!); + return tokenizeToString(buffer.getLineContent(1), this.modeService.languageIdCodec, TokenizationRegistry.get(language)!); } private viewUpdateEditing(): void { @@ -392,7 +395,7 @@ export class StatefulMarkdownCell extends Disposable { const primarySelection = editor.getSelection(); if (primarySelection) { - this.notebookEditor.revealLineInViewAsync(this.viewCell, primarySelection.positionLineNumber); + this.notebookEditor.revealRangeInViewAsync(this.viewCell, primarySelection); } })); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 6c969ce2d9..3693d32636 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -170,10 +170,13 @@ export interface IOutputRequestDto { readonly outputId: string; } +export type ICreationContent = + | { type: RenderOutputType.Html; htmlContent: string; } + | { type: RenderOutputType.Extension; outputId: string; valueBytes: Uint8Array; metadata: unknown; mimeType: string; }; + export interface ICreationRequestMessage { readonly type: 'html'; - readonly content: { type: RenderOutputType.Html; htmlContent: string; } | - { type: RenderOutputType.Extension; outputId: string; valueBytes: Uint8Array; metadata: unknown; mimeType: string; }; + readonly content: ICreationContent; readonly cellId: string; readonly outputId: string; cellTop: number; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 95dae1f767..94b39ff447 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -168,8 +168,24 @@ async function webviewPreloads(ctx: PreloadContext) { readonly workspace: { readonly isTrusted: boolean }; } - interface ScriptModule { - activate(ctx?: RendererContext): Promise | RendererApi | undefined | any; + interface RendererModule { + activate(ctx: RendererContext): Promise | RendererApi | undefined | any; + } + + interface KernelPreloadContext { + readonly onDidReceiveKernelMessage: Event; + postKernelMessage(data: unknown): void; + } + + interface KernelPreloadModule { + activate(ctx: KernelPreloadContext): Promise | void; + } + + function createKernelContext(): KernelPreloadContext { + return { + onDidReceiveKernelMessage: onDidReceiveKernelMessage.event, + postKernelMessage: (data: unknown) => postNotebookMessage('customKernelMessage', { message: data }), + }; } const invokeSourceWithGlobals = (functionSrc: string, globals: { [name: string]: unknown }) => { @@ -177,18 +193,20 @@ async function webviewPreloads(ctx: PreloadContext) { return new Function(...args.map(([k]) => k), functionSrc)(...args.map(([, v]) => v)); }; - const runPreload = async (url: string, originalUri: string): Promise => { + const runKernelPreload = async (url: string, originalUri: string): Promise => { const text = await loadScriptSource(url, originalUri); - return { - activate: () => { - try { - return invokeSourceWithGlobals(text, { ...kernelPreloadGlobals, scriptUrl: url }); - } catch (e) { - console.error(e); - throw e; - } + const isModule = /\bexport\b.*\bactivate\b/.test(text); + try { + if (isModule) { + const module: KernelPreloadModule = await __import(url); + return module.activate(createKernelContext()); + } else { + return invokeSourceWithGlobals(text, { ...kernelPreloadGlobals, scriptUrl: url }); } - }; + } catch (e) { + console.error(e); + throw e; + } }; const dimensionUpdater = new class { @@ -240,7 +258,7 @@ async function webviewPreloads(ctx: PreloadContext) { if (entry.target.id === observedElementInfo.id && entry.contentRect) { if (observedElementInfo.output) { if (entry.contentRect.height !== 0) { - entry.target.style.padding = `${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodePadding}px 0`; + entry.target.style.padding = `${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodeLeftPadding}px`; } else { entry.target.style.padding = `0px`; } @@ -549,60 +567,8 @@ async function webviewPreloads(ctx: PreloadContext) { case 'html': { const data = event.data; - const outputId = data.outputId; - - outputRunner.enqueue(event.data.outputId, async (state) => { - const preloadsAndErrors = await Promise.all([ - data.rendererId ? renderers.load(data.rendererId) : undefined, - ...data.requiredPreloads.map(p => kernelPreloads.waitFor(p.uri)), - ].map(p => p?.catch(err => err))); - - if (state.cancelled) { - return; - } - - const cellOutput = viewModel.ensureOutputCell(data.cellId, data.cellTop); - const outputNode = cellOutput.createOutputNode(outputId, data.outputOffset, data.left); - - const content = data.content; - if (content.type === RenderOutputType.Html) { - const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent; - outputNode.innerHTML = trustedHtml as string; - domEval(outputNode); - } else if (preloadsAndErrors.some(e => e instanceof Error)) { - const errors = preloadsAndErrors.filter((e): e is Error => e instanceof Error); - showPreloadErrors(outputNode, ...errors); - } else { - const rendererApi = preloadsAndErrors[0] as RendererApi; - try { - rendererApi.renderOutputItem(new OutputItem(outputId, outputNode, content.mimeType, content.mimeType, content.valueBytes), outputNode); - } catch (e) { - showPreloadErrors(outputNode, e); - } - } - - resizeObserver.observe(outputNode, outputId, true); - - const offsetHeight = outputNode.offsetHeight; - const cps = document.defaultView!.getComputedStyle(outputNode); - if (offsetHeight !== 0 && cps.padding === '0px') { - // we set padding to zero if the output height is zero (then we can have a zero-height output DOM node) - // thus we need to ensure the padding is accounted when updating the init height of the output - dimensionUpdater.updateHeight(outputId, offsetHeight + ctx.style.outputNodePadding * 2, { - isOutput: true, - init: true, - }); - - outputNode.style.padding = `${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodePadding}px 0`; - } else { - dimensionUpdater.updateHeight(outputId, outputNode.offsetHeight, { - isOutput: true, - init: true, - }); - } - - // don't hide until after this step so that the height is right - cellOutput.element.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; + outputRunner.enqueue(data.outputId, (state) => { + return viewModel.renderOutputCell(data, state); }); break; } @@ -611,7 +577,11 @@ async function webviewPreloads(ctx: PreloadContext) { // const date = new Date(); // console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); - viewModel.updateOutputsScroll(event.data.widgets); + event.data.widgets.forEach(widget => { + outputRunner.enqueue(widget.outputId, () => { + viewModel.updateOutputsScroll([widget]); + }); + }); viewModel.updateMarkupScrolls(event.data.markupCells); break; } @@ -663,7 +633,11 @@ async function webviewPreloads(ctx: PreloadContext) { break; case 'decorations': { - const outputContainer = document.getElementById(event.data.cellId); + let outputContainer = document.getElementById(event.data.cellId); + if (!outputContainer) { + viewModel.ensureOutputCell(event.data.cellId, -100000); + outputContainer = document.getElementById(event.data.cellId); + } outputContainer?.classList.add(...event.data.addedClassNames); outputContainer?.classList.remove(...event.data.removedClassNames); } @@ -698,7 +672,7 @@ async function webviewPreloads(ctx: PreloadContext) { break; case 'updateWorkspaceTrust': { isWorkspaceTrusted = event.data.isTrusted; - viewModel.rerenderMarkupCells(); + viewModel.rerender(); break; } } @@ -759,7 +733,7 @@ async function webviewPreloads(ctx: PreloadContext) { /** Inner function cached in the _loadPromise(). */ private async _load(): Promise { - const module = await __import(this.data.entrypoint); + const module: RendererModule = await __import(this.data.entrypoint); if (!module) { return undefined; // {{SQL CARBON EDIT}} strict-nulls } @@ -795,9 +769,9 @@ async function webviewPreloads(ctx: PreloadContext) { */ public load(uri: string, originalUri: string) { const promise = Promise.all([ - runPreload(uri, originalUri), + runKernelPreload(uri, originalUri), this.waitForAllCurrent(), - ]).then(([module]) => module.activate()); + ]); this.preloads.set(uri, promise); return promise; @@ -860,7 +834,6 @@ async function webviewPreloads(ctx: PreloadContext) { if (!ext) { throw new Error(`Could not find extending renderer: ${extensionId}`); } - await ext.load(); })); } @@ -879,7 +852,6 @@ async function webviewPreloads(ctx: PreloadContext) { return renderer.load(); } - public clearAll() { outputRunner.cancelAll(); for (const renderer of this._renderers.values()) { @@ -916,9 +888,10 @@ async function webviewPreloads(ctx: PreloadContext) { return; } - await Promise.all(renderers.map(x => x.load())); + // De-prioritize built-in renderers + renderers.sort((a, b) => +a.data.isBuiltin - +b.data.isBuiltin); - renderers[0].api?.renderOutputItem(info, element); + (await renderers[0].load())?.renderOutputItem(info, element); } }(); @@ -930,6 +903,16 @@ async function webviewPreloads(ctx: PreloadContext) { private readonly _markupCells = new Map(); private readonly _outputCells = new Map(); + public clearAll() { + this._markupCells.clear(); + this._outputCells.clear(); + } + + public rerender() { + this.rerenderMarkupCells(); + this.renderOutputCells(); + } + private async createMarkupCell(init: webviewMessages.IMarkupCellInitialization, top: number, visible: boolean): Promise { const existing = this._markupCells.get(init.cellId); if (existing) { @@ -983,7 +966,7 @@ async function webviewPreloads(ctx: PreloadContext) { cell?.unhide(); } - public rerenderMarkupCells() { + private rerenderMarkupCells() { for (const cell of this._markupCells.values()) { cell.rerender(); } @@ -1020,9 +1003,28 @@ async function webviewPreloads(ctx: PreloadContext) { } } - public clearAll() { - this._markupCells.clear(); - this._outputCells.clear(); + private renderOutputCells() { + for (const outputCell of this._outputCells.values()) { + outputCell.rerender(); + } + } + + public async renderOutputCell(data: webviewMessages.ICreationRequestMessage, state: { cancelled: boolean }): Promise { + const preloadsAndErrors = await Promise.all([ + data.rendererId ? renderers.load(data.rendererId) : undefined, + ...data.requiredPreloads.map(p => kernelPreloads.waitFor(p.uri)), + ].map(p => p?.catch(err => err))); + + if (state.cancelled) { + return; + } + + const cellOutput = this.ensureOutputCell(data.cellId, data.cellTop); + const outputNode = cellOutput.createOutputElement(data.outputId, data.outputOffset, data.left); + outputNode.render(data.content, preloadsAndErrors); + + // don't hide until after this step so that the height is right + cellOutput.element.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; } public ensureOutputCell(cellId: string, cellTop: number): OutputCell { @@ -1160,7 +1162,7 @@ async function webviewPreloads(ctx: PreloadContext) { await renderers.render(this, this.element); - if (this.mime === 'text/markdown') { + if (this.mime === 'text/markdown' || this.mime === 'text/latex') { const root = this.element.shadowRoot; if (root) { if (!hasPostedRenderedMathTelemetry) { @@ -1259,7 +1261,7 @@ async function webviewPreloads(ctx: PreloadContext) { public readonly element: HTMLElement; - public readonly outputElements = new Map(); + private readonly outputElements = new Map(); constructor(cellId: string) { const container = document.getElementById('container')!; @@ -1280,45 +1282,19 @@ async function webviewPreloads(ctx: PreloadContext) { container.appendChild(lowerWrapperElement); } - public createOutputNode(outputId: string, outputOffset: number, left: number): HTMLElement { + public createOutputElement(outputId: string, outputOffset: number, left: number): OutputElement { let outputContainer = this.outputElements.get(outputId); if (!outputContainer) { - outputContainer = document.createElement('div'); - outputContainer.classList.add('output_container'); - outputContainer.style.position = 'absolute'; - outputContainer.style.overflow = 'hidden'; - this.element.appendChild(outputContainer); + outputContainer = new OutputContainer(outputId); + this.element.appendChild(outputContainer.element); this.outputElements.set(outputId, outputContainer); } - outputContainer.innerText = ''; - outputContainer.style.maxHeight = '0px'; - outputContainer.style.top = `${outputOffset}px`; - const outputNode = document.createElement('div'); - outputNode.id = outputId; - outputNode.classList.add('output'); - outputNode.style.position = 'absolute'; - outputNode.style.top = `0px`; - outputNode.style.left = left + 'px'; - outputNode.style.padding = '0px'; - outputContainer.appendChild(outputNode); - - addMouseoverListeners(outputNode, outputId); - addOutputFocusTracker(outputNode, outputId); - - return outputNode; + return outputContainer.createOutputElement(outputId, outputOffset, left); } public clearOutput(outputId: string, rendererId: string | undefined) { - const outputContainer = this.outputElements.get(outputId); - if (!outputContainer) { - return; - } - - if (rendererId) { - renderers.clearOutput(rendererId, outputId); - } - outputContainer.remove(); + this.outputElements.get(outputId)?.clear(rendererId); this.outputElements.delete(outputId); } @@ -1331,7 +1307,7 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.visibility = 'visible'; this.element.style.top = `${top}px`; - dimensionUpdater.updateHeight(outputId, outputContainer.offsetHeight, { + dimensionUpdater.updateHeight(outputId, outputContainer.element.offsetHeight, { isOutput: true, }); } @@ -1340,23 +1316,20 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.visibility = 'hidden'; } - public updateOutputHeight(outputId: string, height: number) { - const outputContainer = this.outputElements.get(outputId); - if (!outputContainer) { - return; + public rerender() { + for (const outputElement of this.outputElements.values()) { + outputElement.rerender(); } + } - outputContainer.style.maxHeight = `${height}px`; - outputContainer.style.height = `${height}px`; + public updateOutputHeight(outputId: string, height: number) { + this.outputElements.get(outputId)?.updateHeight(height); } public updateScroll(request: webviewMessages.IContentWidgetTopRequest) { this.element.style.top = `${request.cellTop}px`; - const outputContainer = this.outputElements.get(request.outputId); - if (outputContainer) { - outputContainer.style.top = `${request.outputOffset}px`; - } + this.outputElements.get(request.outputId)?.updateScroll(request.outputOffset); if (request.forceDisplay) { this.element.style.visibility = 'visible'; @@ -1364,6 +1337,52 @@ async function webviewPreloads(ctx: PreloadContext) { } } + class OutputContainer { + + public readonly element: HTMLElement; + + private _outputNode?: OutputElement; + + constructor( + private readonly outputId: string, + ) { + this.element = document.createElement('div'); + this.element.classList.add('output_container'); + this.element.style.position = 'absolute'; + this.element.style.overflow = 'hidden'; + } + + public clear(rendererId: string | undefined) { + if (rendererId) { + renderers.clearOutput(rendererId, this.outputId); + } + this.element.remove(); + } + + public updateHeight(height: number) { + this.element.style.maxHeight = `${height}px`; + this.element.style.height = `${height}px`; + } + + public updateScroll(outputOffset: number) { + this.element.style.top = `${outputOffset}px`; + } + + public createOutputElement(outputId: string, outputOffset: number, left: number): OutputElement { + this.element.innerText = ''; + this.element.style.maxHeight = '0px'; + this.element.style.top = `${outputOffset}px`; + + this._outputNode = new OutputElement(outputId, left); + this.element.appendChild(this._outputNode.element); + return this._outputNode; + } + + public rerender() { + this._outputNode?.rerender(); + } + } + vscode.postMessage({ __vscode_notebook_message: true, type: 'initialized' @@ -1380,10 +1399,87 @@ async function webviewPreloads(ctx: PreloadContext) { }); } + class OutputElement { + + public readonly element: HTMLElement; + + private _content?: { content: webviewMessages.ICreationContent, preloadsAndErrors: unknown[] }; + private hasResizeObserver = false; + + constructor( + private readonly outputId: string, + left: number, + ) { + this.element = document.createElement('div'); + this.element.id = outputId; + this.element.classList.add('output'); + this.element.style.position = 'absolute'; + this.element.style.top = `0px`; + this.element.style.left = left + 'px'; + this.element.style.padding = '0px'; + + addMouseoverListeners(this.element, outputId); + addOutputFocusTracker(this.element, outputId); + } + + + public render(content: webviewMessages.ICreationContent, preloadsAndErrors: unknown[]) { + this._content = { content, preloadsAndErrors }; + if (content.type === RenderOutputType.Html) { + const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent; + this.element.innerHTML = trustedHtml as string; + domEval(this.element); + } else if (preloadsAndErrors.some(e => e instanceof Error)) { + const errors = preloadsAndErrors.filter((e): e is Error => e instanceof Error); + showPreloadErrors(this.element, ...errors); + } else { + const rendererApi = preloadsAndErrors[0] as RendererApi; + try { + rendererApi.renderOutputItem(new OutputItem(this.outputId, this.element, content.mimeType, content.metadata, content.valueBytes), this.element); + } catch (e) { + showPreloadErrors(this.element, e); + } + } + + if (!this.hasResizeObserver) { + this.hasResizeObserver = true; + resizeObserver.observe(this.element, this.outputId, true); + } + + const offsetHeight = this.element.offsetHeight; + const cps = document.defaultView!.getComputedStyle(this.element); + if (offsetHeight !== 0 && cps.padding === '0px') { + // we set padding to zero if the output height is zero (then we can have a zero-height output DOM node) + // thus we need to ensure the padding is accounted when updating the init height of the output + dimensionUpdater.updateHeight(this.outputId, offsetHeight + ctx.style.outputNodePadding * 2, { + isOutput: true, + init: true, + }); + + this.element.style.padding = `${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodeLeftPadding}`; + } else { + dimensionUpdater.updateHeight(this.outputId, this.element.offsetHeight, { + isOutput: true, + init: true, + }); + } + } + + public rerender() { + if (this._content) { + this.render(this._content.content, this._content.preloadsAndErrors); + } + } + } + const markupCellDragManager = new class MarkupCellDragManager { private currentDrag: { cellId: string, clientY: number } | undefined; + // Transparent overlay that prevents elements from inside the webview from eating + // drag events. + private dragOverlay?: HTMLElement; + constructor() { document.addEventListener('dragover', e => { // Allow dropping dragged markup cells @@ -1419,6 +1515,19 @@ async function webviewPreloads(ctx: PreloadContext) { this.currentDrag = { cellId, clientY: e.clientY }; + const overlayZIndex = 9999; + if (!this.dragOverlay) { + this.dragOverlay = document.createElement('div'); + this.dragOverlay.style.position = 'absolute'; + this.dragOverlay.style.top = '0'; + this.dragOverlay.style.left = '0'; + this.dragOverlay.style.zIndex = `${overlayZIndex}`; + this.dragOverlay.style.width = '100%'; + this.dragOverlay.style.height = '100%'; + this.dragOverlay.style.background = 'transparent'; + document.body.appendChild(this.dragOverlay); + } + (e.target as HTMLElement).style.zIndex = `${overlayZIndex + 1}`; (e.target as HTMLElement).classList.add('dragging'); postNotebookMessage('cell-drag-start', { @@ -1456,8 +1565,14 @@ async function webviewPreloads(ctx: PreloadContext) { postNotebookMessage('cell-drag-end', { cellId: cellId }); - } + if (this.dragOverlay) { + document.body.removeChild(this.dragOverlay); + this.dragOverlay = undefined; + } + + (e.target as HTMLElement).style.zIndex = ''; + } }(); } @@ -1467,6 +1582,7 @@ export interface RendererMetadata { readonly mimeTypes: readonly string[]; readonly extends: string | undefined; readonly messaging: boolean; + readonly isBuiltin: boolean; } export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly RendererMetadata[], isWorkspaceTrusted: boolean, nonce: string) { @@ -1477,7 +1593,7 @@ export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOp isWorkspaceTrusted, nonce, }; - // TS will try compiling `import()` in webviewPreloads, so use an helper function instead + // TS will try compiling `import()` in webviewPreloads, so use a helper function instead // of using `import(...)` directly return ` const __import = (x) => import(x); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping.ts index 8490aacecc..f2276cd0d0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping.ts @@ -56,6 +56,7 @@ const mapping: ReadonlyMap = new Map([ ['theme-info-foreground', 'vscode-foreground'], // Notebook: ['theme-notebook-output-background', 'vscode-notebook-outputContainerBackgroundColor'], + ['theme-notebook-output-border', 'vscode-notebook-outputContainerBorderColor'], ['theme-notebook-cell-selected-background', 'vscode-notebook-selectedCellBackground'], ['theme-notebook-symbol-highlight-background', 'vscode-notebook-symbolHighlightBackground'], ['theme-notebook-diff-removed-background', 'vscode-diffEditor-removedTextBackground'], diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 309acb5316..043e34e72b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -20,6 +20,7 @@ import { CellEditState, CellFocusMode, CellViewModelStateChangeEvent, CursorAtBo import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookCellStatusBarItem, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookOptions'; export abstract class BaseCellViewModel extends Disposable { @@ -178,7 +179,7 @@ export abstract class BaseCellViewModel extends Disposable { } - + abstract updateOptions(e: NotebookOptionsChangeEvent): void; abstract hasDynamicHeight(): boolean; abstract getHeight(lineHeight: number): number; abstract onDeselect(): void; @@ -531,7 +532,7 @@ export abstract class BaseCellViewModel extends Disposable { options.regex || false, options.caseSensitive || false, options.wholeWord ? options.wordSeparators || null : null, - false); + options.regex || false); } else { const lineCount = this.textBuffer.getLineCount(); const fullRange = new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); @@ -542,7 +543,7 @@ export abstract class BaseCellViewModel extends Disposable { return null; } - cellMatches = this.textBuffer.findMatchesLineByLine(fullRange, searchData, false, 1000); + cellMatches = this.textBuffer.findMatchesLineByLine(fullRange, searchData, options.regex || false, 1000); } return cellMatches; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index f31519bb21..647da04844 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; @@ -17,11 +17,14 @@ import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/vie import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookSearchOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKeymapService } from 'vs/workbench/contrib/notebook/common/notebookKeymapService'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { BaseCellViewModel } from './baseCellViewModel'; export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Code; + protected readonly _onLayoutInfoRead = this._register(new Emitter()); + readonly onLayoutInfoRead = this._onLayoutInfoRead.event; protected readonly _onDidChangeOutputs = this._register(new Emitter()); readonly onDidChangeOutputs = this._onDidChangeOutputs.event; @@ -38,8 +41,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod private _outputsTop: PrefixSumComputer | null = null; - protected readonly _onDidChangeLayout = this._register(new Emitter()); - readonly onDidChangeLayout = this._onDidChangeLayout.event; + protected _pauseableEmitter = this._register(new PauseableEmitter()); + + readonly onDidChangeLayout = this._pauseableEmitter.event; private _editorHeight = 0; set editorHeight(height: number) { @@ -134,12 +138,6 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } })); - this._register(this.viewContext.notebookOptions.onDidChangeOptions(e => { - if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) { - this.layoutChange({}); - } - })); - this._outputCollection = new Array(this.model.outputs.length); this._layoutInfo = { @@ -159,6 +157,20 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod }; } + updateOptions(e: NotebookOptionsChangeEvent) { + if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) { + this.layoutChange({}); + } + } + + pauseLayout() { + this._pauseableEmitter.pause(); + } + + resumeLayout() { + this._pauseableEmitter.resume(); + } + layoutChange(state: CodeCellLayoutChangeEvent, source?: string) { // recompute this._ensureOutputsTop(); @@ -257,7 +269,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } private _fireOnDidChangeLayout(state: CodeCellLayoutChangeEvent) { - this._onDidChangeLayout.fire(state); + this._pauseableEmitter.fire(state); } override restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null, totalHeight?: number) { @@ -284,6 +296,11 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod return false; } + getDynamicHeight() { + this._onLayoutInfoRead.fire(); + return this._layoutInfo.totalHeight; + } + firstLine(): string { return this.getText().split('\n')[0]; } @@ -350,6 +367,11 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this.outputMinHeight = height; } + unlockOutputHeight() { + this.outputMinHeight = 0; + this.layoutChange({ outputHeight: true }); + } + updateOutputHeight(index: number, height: number, source?: string) { if (index >= this._outputCollection.length) { throw new Error('Output index out of range!'); @@ -361,11 +383,20 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } this._outputCollection[index] = height; - if (this._outputsTop!.changeValue(index, height)) { + if (this._outputsTop!.setValue(index, height)) { this.layoutChange({ outputHeight: true }, source); } } + getOutputHeight(index: number) { + if (index >= this._outputCollection.length) { + return -1; + } + + this._ensureOutputsTop(); + return this._outputCollection[index]; + } + getOutputOffsetInContainer(index: number) { this._ensureOutputsTop(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts index aca10903cf..f5289c7f2f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts @@ -4,44 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookLayoutChangeEvent, NotebookLayoutInfo, CellViewModelStateChangeEvent, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookLayoutChangedEvent, NotebookMetadataChangedEvent, NotebookCellStateChangedEvent, NotebookViewEvent, NotebookViewEventType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Disposable } from 'vs/base/common/lifecycle'; -export enum NotebookViewEventType { - LayoutChanged = 1, - MetadataChanged = 2, - CellStateChanged = 3 -} - -export class NotebookLayoutChangedEvent { - public readonly type = NotebookViewEventType.LayoutChanged; - - constructor(readonly source: NotebookLayoutChangeEvent, readonly value: NotebookLayoutInfo) { - - } -} - - -export class NotebookMetadataChangedEvent { - public readonly type = NotebookViewEventType.MetadataChanged; - - constructor(readonly source: NotebookDocumentMetadata) { - - } -} - -export class NotebookCellStateChangedEvent { - public readonly type = NotebookViewEventType.CellStateChanged; - - constructor(readonly source: CellViewModelStateChangeEvent, readonly cell: ICellViewModel) { - - } -} - - -export type NotebookViewEvent = NotebookLayoutChangedEvent | NotebookMetadataChangedEvent | NotebookCellStateChangedEvent; - export class NotebookEventDispatcher extends Disposable { private readonly _onDidChangeLayout = this._register(new Emitter()); readonly onDidChangeLayout = this._onDidChangeLayout.event; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index 103c95b150..dacdd8568b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -8,15 +8,15 @@ import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; -import { CellEditState, CellFindMatch, CellLayoutState, ICellOutputViewModel, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatch, CellLayoutState, ICellOutputViewModel, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookCellStateChangedEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; -import { NotebookCellStateChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookOptions'; export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewModel { @@ -99,10 +99,6 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM this._onDidChangeState.fire({ cellIsHoveredChanged: true }); } - public get contentHash(): number { - return this.model.getHashValue(); - } - private readonly _onDidHideInput = this._register(new Emitter()); readonly onDidHideInput = this._onDidHideInput.event; @@ -142,27 +138,27 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM this._onDidHideInput.fire(); } })); + } - this._register(this.viewContext.notebookOptions.onDidChangeOptions(e => { - if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) { - const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration(); - const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); + updateOptions(e: NotebookOptionsChangeEvent) { + if (e.cellStatusBarVisibility || e.insertToolbarPosition || e.cellToolbarLocation) { + const layoutConfiguration = this.viewContext.notebookOptions.getLayoutConfiguration(); + const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); - if (this.getEditState() === CellEditState.Editing) { - this._updateTotalHeight(this._editorHeight - + layoutConfiguration.markdownCellTopMargin - + layoutConfiguration.markdownCellBottomMargin - + bottomToolbarGap - + this.viewContext.notebookOptions.computeStatusBarHeight()); - } else { - // @rebornix - // On file open, the previewHeight + bottomToolbarGap for a cell out of viewport can be 0 - // When it's 0, the list view will never try to render it anymore even if we scroll the cell into view. - // Thus we make sure it's greater than 0 - this._updateTotalHeight(Math.max(1, this._previewHeight + bottomToolbarGap)); - } + if (this.getEditState() === CellEditState.Editing) { + this._updateTotalHeight(this._editorHeight + + layoutConfiguration.markdownCellTopMargin + + layoutConfiguration.markdownCellBottomMargin + + bottomToolbarGap + + this.viewContext.notebookOptions.computeStatusBarHeight()); + } else { + // @rebornix + // On file open, the previewHeight + bottomToolbarGap for a cell out of viewport can be 0 + // When it's 0, the list view will never try to render it anymore even if we scroll the cell into view. + // Thus we make sure it's greater than 0 + this._updateTotalHeight(Math.max(1, this._previewHeight + bottomToolbarGap)); } - })); + } } /** @@ -250,6 +246,10 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM return false; } + getDynamicHeight() { + return null; + } + getHeight(lineHeight: number) { if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) { return 100; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 2c8c948733..70a7ecbe1b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -11,10 +11,9 @@ import { clamp } from 'vs/base/common/numbers'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IModelDecorationOptions, IModelDeltaDecoration, IReadonlyTextBuffer, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/editor/common/model/editStack'; import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -23,18 +22,16 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; -import { CellEditState, CellFindMatch, CellFindMatchWithIndex, CellFocusMode, ICellViewModel, INotebookDeltaCellStatusBarItems, INotebookDeltaDecoration, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatch, CellFindMatchWithIndex, ICellViewModel, INotebookDeltaCellStatusBarItems, INotebookDeltaDecoration, NotebookLayoutInfo, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellEditType, CellKind, ICell, INotebookSearchOptions, IOutputDto, ISelectionState, NotebookCellMetadata, NotebookCellsChangeType, NotebookCellTextModelSplice, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { CellKind, ICell, INotebookSearchOptions, ISelectionState, NotebookCellsChangeType, NotebookCellTextModelSplice, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean; }; @@ -196,7 +193,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private get selectionHandles() { const handlesSet = new Set(); const handles: number[] = []; - cellRangesToIndexes(this._selectionCollection.selections).map(index => this.cellAt(index)).forEach(cell => { + cellRangesToIndexes(this._selectionCollection.selections).map(index => index < this.length ? this.cellAt(index) : undefined).forEach(cell => { if (cell && !handlesSet.has(cell.handle)) { handles.push(cell.handle); } @@ -229,7 +226,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD constructor( public viewType: string, private _notebook: NotebookTextModel, - readonly viewContext: ViewContext, + private _viewContext: ViewContext, private _layoutInfo: NotebookLayoutInfo | null, private _options: NotebookViewModelOptions, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -246,7 +243,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const compute = (changes: NotebookCellTextModelSplice[], synchronous: boolean) => { const diffs = changes.map(splice => { return [splice[0], splice[1], splice[2].map(cell => { - return createCellViewModel(this._instantiationService, this, cell as NotebookCellTextModel); + return createCellViewModel(this._instantiationService, this, cell as NotebookCellTextModel, this._viewContext); })] as [number, number, CellViewModel[]]; }); @@ -326,7 +323,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._register(this._notebook.onDidChangeContent(contentChanges => { contentChanges.rawEvents.forEach(e => { if (e.kind === NotebookCellsChangeType.ChangeDocumentMetadata) { - this.viewContext.eventDispatcher.emit([new NotebookMetadataChangedEvent(this._notebook.metadata)]); + this._viewContext.eventDispatcher.emit([new NotebookMetadataChangedEvent(this._notebook.metadata)]); } }); @@ -335,7 +332,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } })); - this._register(this.viewContext.eventDispatcher.onDidChangeLayout((e) => { + this._register(this._viewContext.eventDispatcher.onDidChangeLayout((e) => { this._layoutInfo = e.value; this._viewCells.forEach(cell => { @@ -351,12 +348,20 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); })); + this._register(this._viewContext.notebookOptions.onDidChangeOptions(e => { + for (let i = 0; i < this.length; i++) { + const cell = this._viewCells[i]; + cell.updateOptions(e); + } + })); + + this._register(this._selectionCollection.onDidChangeSelection(e => { this._onDidChangeSelection.fire(e); })); this._viewCells = this._notebook.cells.map(cell => { - return createCellViewModel(this._instantiationService, this, cell); + return createCellViewModel(this._instantiationService, this, cell, this._viewContext); }); this._viewCells.forEach(cell => { @@ -412,13 +417,13 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const selections = cellIndexesToRanges(state.selections.map(sel => this.getCellIndexByHandle(sel))) .map(range => this.validateRange(range)) .filter(range => range !== null) as ICellRange[]; - this._selectionCollection.setState(primarySelection, reduceRanges(selections), true, source); + this._selectionCollection.setState(primarySelection, reduceCellRanges(selections), true, source); } else { const primarySelection = this.validateRange(state.focus); const selections = state.selections .map(range => this.validateRange(range)) .filter(range => range !== null) as ICellRange[]; - this._selectionCollection.setState(primarySelection, reduceRanges(selections), true, source); + this._selectionCollection.setState(primarySelection, reduceCellRanges(selections), true, source); } } } @@ -518,7 +523,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._viewCells[index]; } - getCells(range?: ICellRange): ReadonlyArray { + getCellsInRange(range?: ICellRange): ReadonlyArray { if (!range) { return this._viewCells.slice(0); } @@ -583,8 +588,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return index + 1; } - hasCell(handle: number) { - return this._handleToViewCellMapping.has(handle); + hasCell(cell: ICellViewModel) { + return this._handleToViewCellMapping.has(cell.handle); } getVersionId() { @@ -768,192 +773,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } } - createCell(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, outputs: IOutputDto[], synchronous: boolean, pushUndoStop: boolean = true, previouslyPrimary: number | null = null, previouslyFocused: ICellViewModel[] = []): CellViewModel { - const beforeSelections = previouslyFocused.map(e => e.handle); - const endSelections: ISelectionState = { kind: SelectionStateType.Index, focus: { start: index, end: index + 1 }, selections: [{ start: index, end: index + 1 }] }; - this._notebook.applyEdits([ - { - editType: CellEditType.Replace, - index, - count: 0, - cells: [ - { - cellKind: type, - language: language, - mime: undefined, - outputs: outputs, - metadata: metadata, - source: source - } - ] - } - ], synchronous, { kind: SelectionStateType.Handle, primary: previouslyPrimary, selections: beforeSelections }, () => endSelections, undefined); - return this._viewCells[index]; - } - - deleteCell(index: number, synchronous: boolean, pushUndoStop: boolean = true) { - const focusSelectionIndex = this.getFocus()?.start ?? null; - let endPrimarySelection: number | null = null; - - if (index === focusSelectionIndex) { - if (focusSelectionIndex < this.length - 1) { - endPrimarySelection = this._viewCells[focusSelectionIndex + 1].handle; - } else if (focusSelectionIndex === this.length - 1 && this.length > 1) { - endPrimarySelection = this._viewCells[focusSelectionIndex - 1].handle; - } - } - - let endSelections: number[] = this.selectionHandles.filter(handle => handle !== endPrimarySelection && handle !== this._viewCells[index]?.handle); - - this._notebook.applyEdits([ - { - editType: CellEditType.Replace, - index: index, - count: 1, - cells: [] - }], - synchronous, - { kind: SelectionStateType.Index, focus: this.getFocus(), selections: this.getSelections() }, - () => ({ kind: SelectionStateType.Handle, primary: endPrimarySelection, selections: endSelections }), - undefined, - pushUndoStop - ); - } - - /** - * - * @param index - * @param length - * @param newIdx in an index scheme for the state of the tree after the current cell has been "removed" - * @param synchronous - * @param pushedToUndoStack - */ - moveCellToIdx(index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean { - const viewCell = this.viewCells[index] as CellViewModel; - if (!viewCell) { - return false; - } - - this._notebook.applyEdits([ - { - editType: CellEditType.Move, - index, - length, - newIdx - } - ], synchronous, { kind: SelectionStateType.Index, focus: this.getFocus(), selections: this.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: { start: newIdx, end: newIdx + 1 }, selections: [{ start: newIdx, end: newIdx + 1 }] }), undefined); - return true; - } - - private _pushIfAbsent(positions: IPosition[], p: IPosition) { - const last = positions.length > 0 ? positions[positions.length - 1] : undefined; - if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) { - positions.push(p); - } - } - - /** - * Add split point at the beginning and the end; - * Move end of line split points to the beginning of the next line; - * Avoid duplicate split points - */ - private _splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null { - const boundaries: IPosition[] = []; - const lineCnt = textBuffer.getLineCount(); - const getLineLen = (lineNumber: number) => { - return textBuffer.getLineLength(lineNumber); - }; - - // split points need to be sorted - splitPoints = splitPoints.sort((l, r) => { - const lineDiff = l.lineNumber - r.lineNumber; - const columnDiff = l.column - r.column; - return lineDiff !== 0 ? lineDiff : columnDiff; - }); - - for (let sp of splitPoints) { - if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.column !== 1 /** empty line */ && sp.lineNumber < lineCnt) { - sp = new Position(sp.lineNumber + 1, 1); - } - this._pushIfAbsent(boundaries, sp); - } - - if (boundaries.length === 0) { - return null; - } - - // boundaries already sorted and not empty - const modelStart = new Position(1, 1); - const modelEnd = new Position(lineCnt, getLineLen(lineCnt) + 1); - return [modelStart, ...boundaries, modelEnd]; - } - - computeCellLinesContents(cell: ICellViewModel, splitPoints: IPosition[]): string[] | null { - const rangeBoundaries = this._splitPointsToBoundaries(splitPoints, cell.textBuffer); - if (!rangeBoundaries) { - return null; - } - const newLineModels: string[] = []; - for (let i = 1; i < rangeBoundaries.length; i++) { - const start = rangeBoundaries[i - 1]; - const end = rangeBoundaries[i]; - - newLineModels.push(cell.textBuffer.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column), EndOfLinePreference.TextDefined)); - } - - return newLineModels; - } - - async splitNotebookCell(index: number): Promise { - const cell = this.viewCells[index] as CellViewModel; - - if (this._options.isReadOnly) { - return null; - } - - const splitPoints = cell.focusMode === CellFocusMode.Container ? [{ lineNumber: 1, column: 1 }] : cell.getSelectionsStartPosition(); - - if (splitPoints && splitPoints.length > 0) { - await cell.resolveTextModel(); - - if (!cell.hasModel()) { - return null; - } - - const newLinesContents = this.computeCellLinesContents(cell, splitPoints); - if (newLinesContents) { - const language = cell.language; - const kind = cell.cellKind; - const mime = cell.mime; - - const textModel = await cell.resolveTextModel(); - await this._bulkEditService.apply( - [ - new ResourceTextEdit(cell.uri, { range: textModel.getFullModelRange(), text: newLinesContents[0] }), - new ResourceNotebookCellEdit(this._notebook.uri, - { - editType: CellEditType.Replace, - index: index + 1, - count: 0, - cells: newLinesContents.slice(1).map(line => ({ - cellKind: kind, - language, - mime, - source: line, - outputs: [], - metadata: {} - })) - } - ) - ], - { quotableLabel: 'Split Notebook Cell' } - ); - } - } - - return null; - } - getEditorViewState(): INotebookEditorViewState { const editingCells: { [key: number]: boolean; } = {}; this._viewCells.forEach((cell, i) => { @@ -1060,11 +879,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return ret; } - - /** - * Search in notebook text model - * @param value - */ + //#region Find find(value: string, options: INotebookSearchOptions): CellFindMatchWithIndex[] { const matches: CellFindMatchWithIndex[] = []; this._viewCells.forEach((cell, index) => { @@ -1092,7 +907,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); } - async replaceAll(matches: CellFindMatch[], text: string): Promise { + async replaceAll(matches: CellFindMatch[], texts: string[]): Promise { if (!matches.length) { return; } @@ -1101,9 +916,9 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._lastNotebookEditResource.push(matches[0].cell.uri); matches.forEach(match => { - match.matches.forEach(singleMatch => { + match.matches.forEach((singleMatch, index) => { textEdits.push({ - edit: { range: singleMatch.range, text: text }, + edit: { range: singleMatch.range, text: texts[index] }, resource: match.cell.uri }); }); @@ -1117,7 +932,11 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); } - async withElement(element: SingleModelEditStackElement | MultiModelEditStackElement, callback: () => Promise) { + //#endregion + + //#region Undo/Redo + + private async _withElement(element: SingleModelEditStackElement | MultiModelEditStackElement, callback: () => Promise) { const viewCells = this._viewCells.filter(cell => element.matchesResource(cell.uri)); const refs = await Promise.all(viewCells.map(cell => this._textModelService.createModelReference(cell.uri))); await callback(); @@ -1133,7 +952,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const element = editStack.past.length ? editStack.past[editStack.past.length - 1] : undefined; if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) { - await this.withElement(element, async () => { + await this._withElement(element, async () => { await this._undoService.undo(this.uri); }); @@ -1153,7 +972,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const element = editStack.future[0]; if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) { - await this.withElement(element, async () => { + await this._withElement(element, async () => { await this._undoService.redo(this.uri); }); @@ -1165,6 +984,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return []; } + //#endregion + equal(notebook: NotebookTextModel) { return this._notebook === notebook; } @@ -1181,10 +1002,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD export type CellViewModel = CodeCellViewModel | MarkupCellViewModel; -export function createCellViewModel(instantiationService: IInstantiationService, notebookViewModel: NotebookViewModel, cell: NotebookCellTextModel) { +export function createCellViewModel(instantiationService: IInstantiationService, notebookViewModel: NotebookViewModel, cell: NotebookCellTextModel, viewContext: ViewContext) { if (cell.cellKind === CellKind.Code) { - return instantiationService.createInstance(CodeCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, notebookViewModel.viewContext); + return instantiationService.createInstance(CodeCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, viewContext); } else { - return instantiationService.createInstance(MarkupCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, notebookViewModel, notebookViewModel.viewContext); + return instantiationService.createInstance(MarkupCellViewModel, notebookViewModel.viewType, cell, notebookViewModel.layoutInfo, notebookViewModel, viewContext); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorDecorations.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorDecorations.ts similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/notebookEditorDecorations.ts rename to src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorDecorations.ts diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts similarity index 93% rename from src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts rename to src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 3d9cd334a1..a7922f91c9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -15,17 +15,18 @@ import { IMenu, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platf 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 { IInstantiationService } 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 { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { INotebookEditorDelegate, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem'; import { ActionViewWithLabel } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; -import { GlobalToolbar, GlobalToolbarShowLabel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { GlobalToolbarShowLabel } 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'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; interface IActionModel { action: IAction; size: number; visible: boolean; @@ -58,8 +59,9 @@ export class NotebookEditorToolbar extends Disposable { private _pendingLayout: IDisposable | undefined; constructor( - readonly notebookEditor: INotebookEditor, + readonly notebookEditor: INotebookEditorDelegate, readonly contextKeyService: IContextKeyService, + readonly notebookOptions: NotebookOptions, readonly domNode: HTMLElement, @IInstantiationService readonly instantiationService: IInstantiationService, @IConfigurationService readonly configurationService: IConfigurationService, @@ -67,7 +69,7 @@ export class NotebookEditorToolbar extends Disposable { @IMenuService readonly menuService: IMenuService, @IEditorService private readonly editorService: IEditorService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService + @ITASExperimentService private readonly experimentService: ITASExperimentService ) { super(); @@ -77,7 +79,7 @@ export class NotebookEditorToolbar extends Disposable { this._register(this.editorService.onDidActiveEditorChange(() => { if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; + const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditorDelegate; if (notebookEditor === this.notebookEditor) { // this is the active editor this._showNotebookActionsinEditorToolbar(); @@ -111,7 +113,7 @@ export class NotebookEditorToolbar extends Disposable { this._notebookGlobalActionsMenu = this._register(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.notebookToolbar, this.contextKeyService)); this._register(this._notebookGlobalActionsMenu); - this._useGlobalToolbar = this.configurationService.getValue(GlobalToolbar) ?? false; + this._useGlobalToolbar = this.notebookOptions.getLayoutConfiguration().globalToolbar; this._renderLabel = this.configurationService.getValue(GlobalToolbarShowLabel); const context = { @@ -174,6 +176,13 @@ export class NotebookEditorToolbar extends Disposable { } })); + this._register(this.notebookOptions.onDidChangeOptions(e => { + if (e.globalToolbar !== undefined) { + this._useGlobalToolbar = this.notebookOptions.getLayoutConfiguration().globalToolbar; + this._showNotebookActionsinEditorToolbar(); + } + })); + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(GlobalToolbarShowLabel)) { this._renderLabel = this.configurationService.getValue(GlobalToolbarShowLabel); @@ -190,11 +199,6 @@ export class NotebookEditorToolbar extends Disposable { this._showNotebookActionsinEditorToolbar(); return; } - - if (e.affectsConfiguration(GlobalToolbar)) { - this._useGlobalToolbar = this.configurationService.getValue(GlobalToolbar); - this._showNotebookActionsinEditorToolbar(); - } })); if (this.experimentService) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts similarity index 84% rename from src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts rename to src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index cbc0297a26..9963e1ba2f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -5,7 +5,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ICellViewModel, INotebookEditor, KERNEL_EXTENSIONS, NOTEBOOK_MISSING_KERNEL_EXTENSION, 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 { ICellViewModel, KERNEL_EXTENSIONS, NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE, INotebookEditorDelegate, NOTEBOOK_CELL_TOOLBAR_LOCATION } 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'; @@ -21,6 +21,7 @@ export class NotebookEditorContextKeys { private readonly _useConsolidatedOutputButton: IContextKey; private readonly _viewType!: IContextKey; private readonly _missingKernelExtension: IContextKey; + private readonly _cellToolbarLocation: IContextKey<'left' | 'right' | 'hidden'>; private readonly _disposables = new DisposableStore(); private readonly _viewModelDisposables = new DisposableStore(); @@ -28,7 +29,7 @@ export class NotebookEditorContextKeys { private readonly _cellOutputsListeners: IDisposable[] = []; constructor( - private readonly _editor: INotebookEditor, + private readonly _editor: INotebookEditorDelegate, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @IContextKeyService contextKeyService: IContextKeyService, @IExtensionService private readonly _extensionService: IExtensionService @@ -41,6 +42,7 @@ export class NotebookEditorContextKeys { this._hasOutputs = NOTEBOOK_HAS_OUTPUTS.bindTo(contextKeyService); this._viewType = NOTEBOOK_VIEW_TYPE.bindTo(contextKeyService); this._missingKernelExtension = NOTEBOOK_MISSING_KERNEL_EXTENSION.bindTo(contextKeyService); + this._cellToolbarLocation = NOTEBOOK_CELL_TOOLBAR_LOCATION.bindTo(contextKeyService); this._handleDidChangeModel(); this._updateForNotebookOptions(); @@ -68,6 +70,7 @@ export class NotebookEditorContextKeys { private _handleDidChangeModel(): void { this._updateKernelContext(); + this._updateForNotebookOptions(); this._viewModelDisposables.clear(); dispose(this._cellStateListeners); @@ -98,8 +101,8 @@ export class NotebookEditorContextKeys { 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) { + for (let i = 0; i < this._editor.getLength(); i++) { + if (this._editor.cellAt(i).outputsViewModels.length > 0) { hasOutputs = true; break; } @@ -115,7 +118,8 @@ export class NotebookEditorContextKeys { }); }; - for (const cell of this._editor.viewModel.viewCells) { + for (let i = 0; i < this._editor.getLength(); i++) { + const cell = this._editor.cellAt(i); this._cellStateListeners.push(addCellStateListener(cell)); this._cellOutputsListeners.push(addCellOutputsListener(cell)); } @@ -123,7 +127,7 @@ export class NotebookEditorContextKeys { recomputeOutputsExistence(); this._updateForInstalledExtension(); - this._viewModelDisposables.add(this._editor.viewModel.onDidChangeViewCells(e => { + this._viewModelDisposables.add(this._editor.onDidChangeViewCells(e => { e.splices.reverse().forEach(splice => { const [start, deleted, newCells] = splice; const deletedCellStates = this._cellStateListeners.splice(start, deleted, ...newCells.map(addCellStateListener)); @@ -132,7 +136,7 @@ export class NotebookEditorContextKeys { dispose(deletedCellOutputStates); }); })); - this._viewType.set(this._editor.viewModel.viewType); + this._viewType.set(this._editor.textModel.viewType); } private async _updateForInstalledExtension(): Promise { @@ -140,7 +144,7 @@ export class NotebookEditorContextKeys { return; } - const viewType = this._editor.viewModel.viewType; + const viewType = this._editor.textModel.viewType; const kernelExtensionId = KERNEL_EXTENSIONS.get(viewType); this._missingKernelExtension.set( !!kernelExtensionId && !(await this._extensionService.getExtension(kernelExtensionId))); @@ -162,5 +166,6 @@ export class NotebookEditorContextKeys { private _updateForNotebookOptions(): void { const layout = this._editor.notebookOptions.getLayoutConfiguration(); this._useConsolidatedOutputButton.set(layout.consolidatedOutputButton); + this._cellToolbarLocation.set(this._editor.notebookOptions.computeCellToolbarLocation(this._editor.textModel?.viewType)); } } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.css similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/media/notebookKernelActionViewItem.css rename to src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.css diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts similarity index 94% rename from src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts rename to src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts index 4e9cab6ec1..1823e2a982 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/notebookKernelActionViewItem'; +import 'vs/css!./notebookKernelActionViewItem'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Action, IAction } from 'vs/base/common/actions'; import { localize } from 'vs/nls'; @@ -58,7 +58,7 @@ export class NotebooKernelActionViewItem extends ActionViewItem { } protected _update(): void { - const notebook = this._editor.viewModel?.notebookDocument; + const notebook = this._editor.textModel; if (!notebook) { this._resetAction(); @@ -72,7 +72,7 @@ export class NotebooKernelActionViewItem extends ActionViewItem { private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { this._action.enabled = true; - const selectedOrSuggested = info.selected ?? info.suggested; + const selectedOrSuggested = info.selected ?? info.suggestions[0]; if (selectedOrSuggested) { // selected or suggested kernel this._action.label = selectedOrSuggested.label; diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 489580274b..ae7db9b22b 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -74,9 +74,23 @@ export class NotebookCellTextModel extends Disposable implements ICell { } set language(newLanguage: string) { - if (this._textModel && this._textModel.getLanguageIdentifier().language !== newLanguage) { - const newMode = this._modeService.create(newLanguage); - this._textModel.setMode(newMode.languageIdentifier); + if (this._textModel + // 1. the language update is from workspace edit, checking if it's the same as text model's mode + && this._textModel.getLanguageId() === this._modeService.getModeIdForLanguageName(newLanguage) + // 2. the text model's mode might be the same as the `this.language`, even if the language friendly name is not the same, we should not trigger an update + && this._textModel.getLanguageId() === this._modeService.getModeIdForLanguageName(this.language)) { + return; + } + + const newMode = this._modeService.getModeIdForLanguageName(newLanguage); + + if (newMode === null) { + return; + } + + if (this._textModel) { + const languageId = this._modeService.create(newMode); + this._textModel.setMode(languageId.languageId); } if (this._language === newLanguage) { @@ -149,7 +163,11 @@ export class NotebookCellTextModel extends Disposable implements ICell { this._textModel = m; if (this._textModel) { // Init language from text model - this.language = this._textModel.getLanguageIdentifier().language; + // The language defined in the cell might not be supported in the editor so the text model might be using the default fallback (plaintext) + // If so let's not modify the language + if (!(this._modeService.getModeId(this.language) === null && this._textModel.getLanguageId() === 'plaintext')) { + this.language = this._textModel.getLanguageId(); + } // Listen to language changes on the model this._textModelDisposables.add(this._textModel.onDidChangeLanguage(e => { @@ -188,6 +206,10 @@ export class NotebookCellTextModel extends Disposable implements ICell { this._internalMetadata = internalMetadata ?? {}; } + resetTextBuffer(textBuffer: model.ITextBuffer) { + this._textBuffer = textBuffer; + } + getValue(): string { const fullRange = this.getFullModelRange(); const eol = this.textBuffer.getEOL(); @@ -204,7 +226,10 @@ export class NotebookCellTextModel extends Disposable implements ICell { } this._hash = hash([hash(this.language), hash(this.getValue()), this._getPersisentMetadata(), this.transientOptions.transientOutputs ? [] : this._outputs.map(op => ({ - outputs: op.outputs, + outputs: op.outputs.map(output => ({ + mime: output.mime, + data: Array.from(output.data.buffer) + })), metadata: op.metadata }))]); return this._hash; @@ -238,6 +263,54 @@ export class NotebookCellTextModel extends Disposable implements ICell { this.outputs.splice(splice.start, splice.deleteCount, ...splice.newOutputs); this._onDidChangeOutputs.fire(splice); } + + private _outputNotEqualFastCheck(left: ICellOutput[], right: ICellOutput[]) { + if (left.length !== right.length) { + return false; + } + + for (let i = 0; i < this.outputs.length; i++) { + const l = left[i]; + const r = right[i]; + + if (l.outputs.length !== r.outputs.length) { + return false; + } + + for (let k = 0; k < l.outputs.length; k++) { + if (l.outputs[k].mime !== r.outputs[k].mime) { + return false; + } + + if (l.outputs[k].data.byteLength !== r.outputs[k].data.byteLength) { + return false; + } + } + } + + return true; + } + + equal(b: NotebookCellTextModel): boolean { + if (this.language !== b.language) { + return false; + } + + if (this.getTextLength() !== b.getTextLength()) { + return false; + } + + if (!this.transientOptions.transientOutputs) { + // compare outputs + + if (!this._outputNotEqualFastCheck(this.outputs, b.outputs)) { + return false; + } + } + + return this.getHashValue() === b.getHashValue(); + } + override dispose() { dispose(this._outputs); // Manually release reference to previous text buffer to avoid large leaks @@ -260,7 +333,7 @@ export function cloneNotebookCellTextModel(cell: NotebookCellTextModel) { /* paste should generate new outputId */ outputId: UUID.generateUuid() })), metadata: { ...cell.metadata }, - internalMetadata: { ...cell.internalMetadata }, + // Don't include internalMetadata, ie execution state, this is not to be shared }; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 93b34be109..8ab7ff0342 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -18,7 +18,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { ITextBuffer, ITextModel } from 'vs/editor/common/model'; +import { ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { isDefined } from 'vs/base/common/types'; @@ -555,6 +555,15 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel && last.cellIndex === edit.cellIndex ) { last.edit.outputs = [...last.edit.outputs, ...edit.edit.outputs]; + } else if (last.edit.editType === CellEditType.Output + && !last.edit.append // last cell is not append + && last.edit.outputs.length === 0 // last cell is clear outputs + && edit.edit.editType === CellEditType.Output + && edit.edit.append + && last.cellIndex === edit.cellIndex + ) { + last.edit.append = false; + last.edit.outputs = edit.edit.outputs; } else { mergedEdits.push(edit); } @@ -598,9 +607,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel if (textModel && textModel instanceof TextModel) { cell.textModel = textModel; cell.language = cellDto.language; - if (!cell.textModel.equalsTextBuffer(cell.textBuffer as ITextBuffer)) { - cell.textModel.setValue(cellDto.source); - } + cell.textModel.setValue(cellDto.source); + cell.resetTextBuffer(cell.textModel.getTextBuffer()); } const dirtyStateListener = cell.onDidChangeContent((e) => { this._bindCellContentHandler(cell, e); @@ -658,17 +666,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._notebookSpecificAlternativeId = Number(newAlternativeVersionId.substr(0, newAlternativeVersionId.indexOf('_'))); } - private _isDocumentMetadataChangeTransient(a: NotebookDocumentMetadata, b: NotebookDocumentMetadata) { - const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); - for (let key of keys) { - if (key !== 'trusted') { - return true; - } - } - - return false; - } - private _updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean) { const oldMetadata = this.metadata; const triggerDirtyChange = this._isDocumentMetadataChanged(this.metadata, metadata); @@ -694,7 +691,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.metadata = metadata; this._pauseableEmitter.fire({ - rawEvents: [{ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata, transient: this._isDocumentMetadataChangeTransient(oldMetadata, metadata) }], + rawEvents: [{ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata, transient: !triggerDirtyChange }], versionId: this.versionId, synchronous: true, endSelectionState: undefined diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 386bc484bf..4e8221fa6c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -8,6 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDiffResult, ISequence } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; +import { Iterable } from 'vs/base/common/iterator'; import { Mimes } from 'vs/base/common/mime'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; @@ -21,7 +22,8 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; -import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -31,18 +33,20 @@ export enum CellKind { Code = 2 } -export const NOTEBOOK_DISPLAY_ORDER = [ +export const NOTEBOOK_DISPLAY_ORDER: readonly string[] = [ 'application/json', 'application/javascript', 'text/html', 'image/svg+xml', + Mimes.latex, Mimes.markdown, 'image/png', 'image/jpeg', Mimes.text ]; -export const ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER = [ +export const ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER: readonly string[] = [ + Mimes.latex, Mimes.markdown, 'application/json', Mimes.text, @@ -94,6 +98,7 @@ export interface NotebookCellInternalMetadata { runStartTimeAdjustment?: number; runEndTime?: number; isPaused?: boolean; + didPause?: boolean; } export type TransientCellMetadata = { [K in keyof NotebookCellMetadata]?: boolean }; @@ -105,8 +110,6 @@ export interface TransientOptions { transientDocumentMetadata: TransientDocumentMetadata; } - - /** Note: enum values are used for sorting */ export const enum NotebookRendererMatch { /** Renderer has a hard dependency on an available kernel */ @@ -145,6 +148,8 @@ export interface INotebookRendererInfo { readonly dependencies: readonly string[]; + readonly isBuiltin: boolean; + matchesWithoutKernel(mimeType: string): NotebookRendererMatch; matches(mimeType: string, kernelProvides: ReadonlyArray): NotebookRendererMatch; } @@ -565,6 +570,7 @@ const _mimeTypeInfo = new Map([ ['image/git', { alwaysSecure: true, supportedByCore: true }], ['image/svg+xml', { supportedByCore: true }], ['application/json', { alwaysSecure: true, supportedByCore: true }], + [Mimes.latex, { alwaysSecure: true, supportedByCore: true }], [Mimes.markdown, { alwaysSecure: true, supportedByCore: true }], [Mimes.text, { alwaysSecure: true, supportedByCore: true }], ['text/html', { supportedByCore: true }], @@ -586,38 +592,93 @@ export function mimeTypeIsMergeable(mimeType: string): boolean { return _mimeTypeInfo.get(mimeType)?.mergeable ?? false; } -function matchGlobUniversal(pattern: string, path: string) { - if (isWindows) { - pattern = pattern.replace(/\//g, '\\'); - path = path.replace(/\//g, '\\'); - } +const normalizeSlashes = (str: string) => isWindows ? str.replace(/\//g, '\\') : str; - return glob.match(pattern, path); +interface IMimeTypeWithMatcher { + pattern: string; + matches: glob.ParsedPattern; } +export class MimeTypeDisplayOrder { + private readonly order: IMimeTypeWithMatcher[]; -function getMimeTypeOrder(mimeType: string, userDisplayOrder: string[], defaultOrder: string[]) { - let order = 0; - for (let i = 0; i < userDisplayOrder.length; i++) { - if (matchGlobUniversal(userDisplayOrder[i], mimeType)) { - return order; - } - order++; + constructor( + initialValue: readonly string[] = [], + private readonly defaultOrder = NOTEBOOK_DISPLAY_ORDER, + ) { + this.order = [...new Set(initialValue)].map(pattern => ({ + pattern, + matches: glob.parse(normalizeSlashes(pattern)) + })); } - for (let i = 0; i < defaultOrder.length; i++) { - if (matchGlobUniversal(defaultOrder[i], mimeType)) { - return order; + /** + * Returns a sorted array of the input mimetypes. + */ + public sort(mimetypes: Iterable): string[] { + const remaining = new Map(Iterable.map(mimetypes, m => [m, normalizeSlashes(m)])); + let sorted: string[] = []; + + for (const { matches } of this.order) { + for (const [original, normalized] of remaining) { + if (matches(normalized)) { + sorted.push(original); + remaining.delete(original); + break; + } + } } - order++; + if (remaining.size) { + sorted = sorted.concat([...remaining.keys()].sort( + (a, b) => this.defaultOrder.indexOf(a) - this.defaultOrder.indexOf(b), + )); + } + + return sorted; } - return order; -} + /** + * Records that the user selected the given mimetype over the other + * possible mimetypes, prioritizing it for future reference. + */ + public prioritize(chosenMimetype: string, otherMimetypes: readonly string[]) { + const chosenIndex = this.findIndex(chosenMimetype); + if (chosenIndex === -1) { + // always first, nothing more to do + this.order.unshift({ pattern: chosenMimetype, matches: glob.parse(normalizeSlashes(chosenMimetype)) }); + return; + } -export function sortMimeTypes(mimeTypes: string[], userDisplayOrder: string[], defaultOrder: string[]) { - return mimeTypes.sort((a, b) => getMimeTypeOrder(a, userDisplayOrder, defaultOrder) - getMimeTypeOrder(b, userDisplayOrder, defaultOrder)); + // Get the other mimetypes that are before the chosenMimetype. Then, move + // them after it, retaining order. + const uniqueIndicies = new Set(otherMimetypes.map(m => this.findIndex(m, chosenIndex))); + uniqueIndicies.delete(-1); + const otherIndices = Array.from(uniqueIndicies).sort(); + this.order.splice(chosenIndex + 1, 0, ...otherIndices.map(i => this.order[i])); + + for (let oi = otherIndices.length - 1; oi >= 0; oi--) { + this.order.splice(otherIndices[oi], 1); + } + } + + /** + * Gets an array of in-order mimetype preferences. + */ + public toArray() { + return this.order.map(o => o.pattern); + } + + private findIndex(mimeType: string, maxIndex = this.order.length) { + const normalized = normalizeSlashes(mimeType); + for (let i = 0; i < maxIndex; i++) { + if (this.order[i].matches(normalized)) { + return i; + } + } + + return -1; + } } interface IMutableSplice extends ISplice { @@ -713,7 +774,7 @@ export interface INotebookEditorModel extends IEditorModel { hasAssociatedFilePath(): boolean; load(options?: INotebookLoadOptions): Promise; save(options?: ISaveOptions): Promise; - saveAs(target: URI): Promise; + saveAs(target: URI): Promise; revert(options?: IRevertOptions): Promise; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 12b0b48e11..c00055574b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; -import { IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorInputCapabilities, Verbosity, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorInputCapabilities, Verbosity, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; import { isEqual, joinPath } from 'vs/base/common/resources'; @@ -120,15 +121,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return this._editorModelReference.object.isDirty(); } - override isOrphaned() { - if (!this._editorModelReference) { - return super.isOrphaned(); - } - - return this._editorModelReference.object.isOrphaned(); - } - - override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { + override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { if (this._editorModelReference) { if (this.hasCapability(EditorInputCapabilities.Untitled)) { @@ -143,7 +136,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return undefined; } - override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { if (!this._editorModelReference) { return undefined; } @@ -193,7 +186,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } // called when users rename a notebook document - override rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { + override async rename(group: GroupIdentifier, target: URI): Promise { if (this._editorModelReference) { const contributedNotebookProviders = this._notebookService.getContributedNotebookTypes(target); @@ -204,7 +197,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return undefined; } - private _move(_group: GroupIdentifier, newResource: URI): { editor: IEditorInput; } { + private _move(_group: GroupIdentifier, newResource: URI): { editor: EditorInput; } { const editorInput = NotebookEditorInput.create(this._instantiationService, newResource, this.viewType); return { editor: editorInput }; } @@ -241,7 +234,6 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return null; } this._register(this._editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._register(this._editorModelReference.object.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); this._register(this._editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); if (this._editorModelReference.object.isDirty()) { this._onDidChangeDirty.fire(); @@ -286,7 +278,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { }; } - override matches(otherInput: IEditorInput | IUntypedEditorInput): boolean { + override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { if (super.matches(otherInput)) { return true; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 0bb5992e94..ae39f97510 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { Emitter, Event } from 'vs/base/common/event'; import { ICellDto2, INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookData, NotebookDocumentBackupData } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -392,7 +393,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook }); } - async saveAs(targetResource: URI): Promise { + async saveAs(targetResource: URI): Promise { if (!this.isResolved()) { return undefined; @@ -522,7 +523,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE this._workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource }); } } else { - this._workingCopy = await this._workingCopyManager.resolve(this.resource, { forceReadFromFile: options?.forceReadFromFile }); + this._workingCopy = await this._workingCopyManager.resolve(this.resource, options?.forceReadFromFile ? { reload: { async: false, force: true } } : undefined); this._workingCopyListeners.add(this._workingCopy.onDidSave(() => this._onDidSave.fire())); this._workingCopyListeners.add(this._workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); this._workingCopyListeners.add(this._workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire())); @@ -535,8 +536,10 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE })); } else { await this._workingCopyManager.resolve(this.resource, { - forceReadFromFile: options?.forceReadFromFile, - reload: { async: !options?.forceReadFromFile } + reload: { + async: !options?.forceReadFromFile, + force: options?.forceReadFromFile + } }); } @@ -544,7 +547,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE return this; } - async saveAs(target: URI): Promise { + async saveAs(target: URI): Promise { const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target); if (!newWorkingCopy) { return undefined; diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index ea6f518f22..4049ffd6cc 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -17,7 +17,7 @@ export interface ISelectedNotebooksChangeEvent { export interface INotebookKernelMatchResult { readonly selected: INotebookKernel | undefined; - readonly suggested: INotebookKernel | undefined; + readonly suggestions: INotebookKernel[]; readonly all: INotebookKernel[]; } @@ -26,6 +26,7 @@ export interface INotebookKernelChangeEvent { label?: true; description?: true; detail?: true; + kind?: true; supportedLanguages?: true; hasExecutionOrder?: true; } @@ -44,6 +45,7 @@ export interface INotebookKernel { label: string; description?: string; detail?: string; + kind?: string; supportedLanguages: string[]; implementsInterrupt?: boolean; implementsExecutionOrder?: boolean; diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index 9c6d7a18d9..0404088132 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -24,6 +24,8 @@ export function getEditorTopPadding() { return EDITOR_TOP_PADDING; } +export const OutputInnerContainerTopPadding = 4; + export interface NotebookLayoutConfiguration { cellRightMargin: number; cellRunGutter: number; @@ -61,7 +63,7 @@ export interface NotebookLayoutConfiguration { editorOptionsCustomizations: any | undefined; } -interface NotebookOptionsChangeEvent { +export interface NotebookOptionsChangeEvent { cellStatusBarVisibility?: boolean; cellToolbarLocation?: boolean; cellToolbarInteraction?: boolean; @@ -105,15 +107,15 @@ export class NotebookOptions extends Disposable { protected readonly _onDidChangeOptions = this._register(new Emitter()); readonly onDidChangeOptions = this._onDidChangeOptions.event; - constructor(private readonly configurationService: IConfigurationService) { + constructor(private readonly configurationService: IConfigurationService, private readonly overrides?: { cellToolbarInteraction: string, globalToolbar: boolean }) { super(); const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBar); - const globalToolbar = this.configurationService.getValue(GlobalToolbar) ?? true; + const globalToolbar = overrides?.globalToolbar ?? this.configurationService.getValue(GlobalToolbar) ?? true; const consolidatedOutputButton = this.configurationService.getValue(ConsolidatedOutputButton) ?? true; const consolidatedRunButton = this.configurationService.getValue(ConsolidatedRunButton) ?? false; const dragAndDropEnabled = this.configurationService.getValue(DragAndDropEnabled) ?? true; const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocation) ?? { 'default': 'right' }; - const cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); + const cellToolbarInteraction = overrides?.cellToolbarInteraction ?? this.configurationService.getValue(CellToolbarVisibility); const compactView = this.configurationService.getValue(CompactView) ?? true; const focusIndicator = this._computeFocusIndicatorOption(); const insertToolbarPosition = this._computeInsertToolbarPositionOption(); @@ -210,7 +212,7 @@ export class NotebookOptions extends Disposable { configuration.cellToolbarLocation = this.configurationService.getValue(CellToolbarLocation) ?? { 'default': 'right' }; } - if (cellToolbarInteraction) { + if (cellToolbarInteraction && !this.overrides?.cellToolbarInteraction) { configuration.cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); } @@ -234,7 +236,7 @@ export class NotebookOptions extends Disposable { configuration.insertToolbarPosition = this._computeInsertToolbarPositionOption(); } - if (globalToolbar) { + if (globalToolbar && this.overrides?.globalToolbar === undefined) { configuration.globalToolbar = this.configurationService.getValue(GlobalToolbar) ?? true; } @@ -461,10 +463,10 @@ export class NotebookOptions extends Disposable { computeDiffWebviewOptions() { return { outputNodePadding: this._layoutConfiguration.cellOutputPadding, - outputNodeLeftPadding: 32, + outputNodeLeftPadding: 0, previewNodePadding: this._layoutConfiguration.markdownPreviewPadding, markdownLeftMargin: 0, - leftMargin: 0, + leftMargin: 32, rightMargin: 0, runGutter: 0, dragAndDropEnabled: false, diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index 6d37ba5716..b0ec3620b0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -49,6 +49,8 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { readonly mimeTypes: readonly string[]; private readonly mimeTypeGlobs: glob.ParsedPattern[]; + readonly isBuiltin: boolean; + constructor(descriptor: { readonly id: string; readonly displayName: string; @@ -62,6 +64,7 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { this.id = descriptor.id; this.extensionId = descriptor.extension.identifier; this.extensionLocation = descriptor.extension.extensionLocation; + this.isBuiltin = descriptor.extension.isBuiltin; if (typeof descriptor.entrypoint === 'string') { this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); @@ -78,11 +81,11 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { this.messaging = descriptor.requiresMessaging ?? RendererMessagingSpec.Never; } - get dependencies(): string[] { + public get dependencies(): string[] { return this.hardDependencies.values(); } - matchesWithoutKernel(mimeType: string) { + public matchesWithoutKernel(mimeType: string) { if (!this.matchesMimeTypeOnly(mimeType)) { return NotebookRendererMatch.Never; } @@ -98,7 +101,7 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { return NotebookRendererMatch.Pure; } - matches(mimeType: string, kernelProvides: ReadonlyArray) { + public matches(mimeType: string, kernelProvides: ReadonlyArray) { if (!this.matchesMimeTypeOnly(mimeType)) { return NotebookRendererMatch.Never; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookRange.ts b/src/vs/workbench/contrib/notebook/common/notebookRange.ts index 738387c99a..def5111564 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookRange.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookRange.ts @@ -56,12 +56,8 @@ export function cellRangesToIndexes(ranges: ICellRange[]) { return indexes; } -/** - * todo@rebornix notebookBrowser.reduceCellRanges - * @returns - */ -export function reduceRanges(ranges: ICellRange[]) { +export function reduceCellRanges(ranges: ICellRange[]): ICellRange[] { const sorted = ranges.sort((a, b) => a.start - b.start); const first = sorted[0]; @@ -79,6 +75,23 @@ export function reduceRanges(ranges: ICellRange[]) { return prev; }, [first] as ICellRange[]); } + +export function cellRangesEqual(a: ICellRange[], b: ICellRange[]) { + a = reduceCellRanges(a); + b = reduceCellRanges(b); + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i++) { + if (a[i].start !== b[i].start || a[i].end !== b[i].end) { + return false; + } + } + + return true; +} + /** * todo@rebornix test and sort * @param range diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 2a9c09357d..7b52218ce0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -14,6 +14,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { IDisposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; export const INotebookService = createDecorator('notebookService'); @@ -76,7 +77,8 @@ export interface INotebookService { getRenderers(): INotebookRendererInfo[]; /** Updates the preferred renderer for the given mimetype in the workspace. */ - updateMimePreferredRenderer(mimeType: string, rendererId: string): void; + updateMimePreferredRenderer(viewType: string, mimeType: string, rendererId: string, otherMimetypes: readonly string[]): void; + saveMimeDisplayOrder(target: ConfigurationTarget): void; createNotebookTextModel(viewType: string, uri: URI, data: NotebookData, transientOptions: TransientOptions): NotebookTextModel; getNotebookTextModel(uri: URI): NotebookTextModel | undefined; diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts index d690b87c90..8c4fea49c1 100644 --- a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts +++ b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; -import { hash } from 'vs/base/common/hash'; +import { doHash, hash, numberHash } from 'vs/base/common/hash'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; @@ -12,6 +12,16 @@ import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeText import { CellKind, ICellDto2, IMainCellDto, INotebookDiffResult, IOutputDto, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Range } from 'vs/editor/common/core/range'; import { EditorWorkerHost } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; +import { VSBuffer } from 'vs/base/common/buffer'; + +function bufferHash(buffer: VSBuffer): number { + let initialHashVal = numberHash(104579, 0); + for (let k = 0; k < buffer.buffer.length; k++) { + initialHashVal = doHash(buffer.buffer[k], initialHashVal); + } + + return initialHashVal; +} class MirrorCell { private _textBuffer!: model.IReadonlyTextBuffer; @@ -74,7 +84,7 @@ class MirrorCell { this._hash = hash([hash(this.language), hash(this.getValue()), this.metadata, this.internalMetadata, this.outputs.map(op => ({ outputs: op.outputs.map(output => ({ mime: output.mime, - data: output.data + data: bufferHash(output.data) })), metadata: op.metadata }))]); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts b/src/vs/workbench/contrib/notebook/test/cellOperations.test.ts similarity index 83% rename from src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts rename to src/vs/workbench/contrib/notebook/test/cellOperations.test.ts index 58c3a71caf..f86fc5560e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellOperations/test/cellOperations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/cellOperations.test.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { Range } from 'vs/editor/common/core/range'; -import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; -import { copyCellRange, joinNotebookCells, moveCellRange } from 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations'; -import { runDeleteAction } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; +import { changeCellToKind, computeCellLinesContents, copyCellRange, joinNotebookCells, moveCellRange, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { Range } from 'vs/editor/common/core/range'; +import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; // {{SQL CARBON EDIT}} Disable failing VS Notebook tests since we don't use their stuff suite.skip('CellOperations', () => { @@ -75,6 +74,7 @@ suite.skip('CellOperations', () => { }); }); + test('Copy/duplicate cells - single cell', async function () { await withTestNotebook( [ @@ -170,7 +170,7 @@ suite.skip('CellOperations', () => { ], async (editor, viewModel, accessor) => { viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 3, end: 4 }, selections: [{ start: 3, end: 4 }] }); - const ret = await joinNotebookCells(viewModel, { start: 3, end: 4 }, 'below'); + const ret = await joinNotebookCells(editor, { start: 3, end: 4 }, 'below'); assert.strictEqual(ret?.edits.length, 2); assert.deepStrictEqual(ret?.edits[0], new ResourceTextEdit(viewModel.cellAt(3)!.uri, { range: new Range(1, 11, 1, 11), text: viewModel.cellAt(4)!.textBuffer.getEOL() + 'var c = 3;' @@ -197,7 +197,7 @@ suite.skip('CellOperations', () => { ], async (editor, viewModel, accessor) => { viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 3, end: 4 }, selections: [{ start: 3, end: 4 }] }); - const ret = await joinNotebookCells(viewModel, { start: 4, end: 5 }, 'above'); + const ret = await joinNotebookCells(editor, { start: 4, end: 5 }, 'above'); assert.strictEqual(ret?.edits.length, 2); assert.deepStrictEqual(ret?.edits[0], new ResourceTextEdit(viewModel.cellAt(3)!.uri, { range: new Range(1, 11, 1, 11), text: viewModel.cellAt(4)!.textBuffer.getEOL() + 'var c = 3;' @@ -222,7 +222,7 @@ suite.skip('CellOperations', () => { ], async (editor, viewModel, accessor) => { viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 0, end: 2 }] }); - const ret = await joinNotebookCells(viewModel, { start: 0, end: 2 }, 'below'); + const ret = await joinNotebookCells(editor, { start: 0, end: 2 }, 'below'); assert.strictEqual(ret?.edits.length, 2); assert.deepStrictEqual(ret?.edits[0], new ResourceTextEdit(viewModel.cellAt(0)!.uri, { range: new Range(1, 11, 1, 11), text: viewModel.cellAt(1)!.textBuffer.getEOL() + 'var b = 2;' + viewModel.cellAt(2)!.textBuffer.getEOL() + 'var c = 3;' @@ -247,7 +247,7 @@ suite.skip('CellOperations', () => { ], async (editor, viewModel, accessor) => { viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 2, end: 3 }, selections: [{ start: 1, end: 3 }] }); - const ret = await joinNotebookCells(editor.viewModel, { start: 1, end: 3 }, 'above'); + const ret = await joinNotebookCells(editor, { start: 1, end: 3 }, 'above'); assert.strictEqual(ret?.edits.length, 2); assert.deepStrictEqual(ret?.edits[0], new ResourceTextEdit(viewModel.cellAt(0)!.uri, { range: new Range(1, 11, 1, 11), text: viewModel.cellAt(1)!.textBuffer.getEOL() + 'var b = 2;' + viewModel.cellAt(2)!.textBuffer.getEOL() + 'var c = 3;' @@ -273,7 +273,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 0, end: 1 }); editor.setSelections([{ start: 0, end: 1 }]); - runDeleteAction(viewModel, viewModel.cellAt(0)!); + runDeleteAction(editor, viewModel.cellAt(0)!); assert.strictEqual(viewModel.length, 2); }); }); @@ -288,7 +288,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 0, end: 1 }); editor.setSelections([{ start: 0, end: 2 }]); - runDeleteAction(viewModel, viewModel.cellAt(0)!); + runDeleteAction(editor, viewModel.cellAt(0)!); assert.strictEqual(viewModel.length, 1); }); }); @@ -304,7 +304,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 0, end: 1 }); editor.setSelections([{ start: 2, end: 4 }]); - runDeleteAction(viewModel, viewModel.cellAt(0)!); + runDeleteAction(editor, viewModel.cellAt(0)!); assert.strictEqual(viewModel.length, 3); }); }); @@ -319,7 +319,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 0, end: 1 }); editor.setSelections([{ start: 0, end: 1 }]); - runDeleteAction(viewModel, viewModel.cellAt(2)!); + runDeleteAction(editor, viewModel.cellAt(2)!); assert.strictEqual(viewModel.length, 2); assert.strictEqual(viewModel.cellAt(0)?.getText(), 'var a = 1;'); assert.strictEqual(viewModel.cellAt(1)?.getText(), 'var b = 2;'); @@ -338,7 +338,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 0, end: 1 }); editor.setSelections([{ start: 0, end: 1 }, { start: 3, end: 5 }]); - runDeleteAction(viewModel, viewModel.cellAt(1)!); + runDeleteAction(editor, viewModel.cellAt(1)!); assert.strictEqual(viewModel.length, 4); assert.deepStrictEqual(editor.getFocus(), { start: 0, end: 1 }); assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }, { start: 2, end: 4 }]); @@ -357,7 +357,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 0, end: 1 }); editor.setSelections([{ start: 2, end: 3 }]); - runDeleteAction(viewModel, viewModel.cellAt(0)!); + runDeleteAction(editor, viewModel.cellAt(0)!); assert.strictEqual(viewModel.length, 4); assert.deepStrictEqual(editor.getFocus(), { start: 0, end: 1 }); assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]); @@ -376,7 +376,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 2, end: 3 }); editor.setSelections([{ start: 3, end: 5 }]); - runDeleteAction(viewModel, viewModel.cellAt(0)!); + runDeleteAction(editor, viewModel.cellAt(0)!); assert.strictEqual(viewModel.length, 4); assert.deepStrictEqual(editor.getFocus(), { start: 1, end: 2 }); assert.deepStrictEqual(viewModel.getSelections(), [{ start: 2, end: 4 }]); @@ -394,7 +394,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 2, end: 3 }); editor.setSelections([{ start: 2, end: 3 }]); - runDeleteAction(viewModel, viewModel.cellAt(2)!); + runDeleteAction(editor, viewModel.cellAt(2)!); assert.strictEqual(viewModel.length, 2); assert.deepStrictEqual(editor.getFocus(), { start: 1, end: 2 }); }); @@ -411,7 +411,7 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 0, end: 1 }); editor.setSelections([{ start: 0, end: 1 }, { start: 3, end: 4 }]); - runDeleteAction(viewModel, viewModel.cellAt(0)!); + runDeleteAction(editor, viewModel.cellAt(0)!); assert.strictEqual(viewModel.length, 2); assert.deepStrictEqual(editor.getFocus(), { start: 0, end: 1 }); }); @@ -429,9 +429,74 @@ suite.skip('CellOperations', () => { async (editor, viewModel) => { editor.setFocus({ start: 1, end: 2 }); editor.setSelections([{ start: 1, end: 2 }, { start: 3, end: 5 }]); - runDeleteAction(viewModel, viewModel.cellAt(1)!); + runDeleteAction(editor, viewModel.cellAt(1)!); assert.strictEqual(viewModel.length, 2); assert.deepStrictEqual(editor.getFocus(), { start: 1, end: 2 }); }); }); + + test('Change cell kind - single cell', async function () { + await withTestNotebook( + [ + ['# header a', 'markdown', CellKind.Markup, [], {}], + ['var b = 1;', 'javascript', CellKind.Code, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}], + ['var c = 3;', 'javascript', CellKind.Code, [], {}] + ], + async (editor, viewModel) => { + viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 1, end: 2 }] }); + await changeCellToKind(CellKind.Markup, { notebookEditor: editor, cell: viewModel.cellAt(1)!, ui: true }); + assert.strictEqual(viewModel.cellAt(1)?.cellKind, CellKind.Markup); + }); + }); + + test('Change cell kind - multi cells', async function () { + await withTestNotebook( + [ + ['# header a', 'markdown', CellKind.Markup, [], {}], + ['var b = 1;', 'javascript', CellKind.Code, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}], + ['var c = 3;', 'javascript', CellKind.Code, [], {}] + ], + async (editor, viewModel) => { + viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 1, end: 2 }] }); + await changeCellToKind(CellKind.Markup, { notebookEditor: editor, selectedCells: [viewModel.cellAt(3)!, viewModel.cellAt(4)!], ui: false }); + assert.strictEqual(viewModel.cellAt(3)?.cellKind, CellKind.Markup); + assert.strictEqual(viewModel.cellAt(4)?.cellKind, CellKind.Markup); + }); + }); + + + test('split cell', async function () { + await withTestNotebook( + [ + ['var b = 1;', 'javascript', CellKind.Code, [], {}] + ], + (editor, viewModel) => { + assert.deepStrictEqual(computeCellLinesContents(viewModel.cellAt(0)!, [{ lineNumber: 1, column: 4 }]), [ + 'var', + ' b = 1;' + ]); + + assert.deepStrictEqual(computeCellLinesContents(viewModel.cellAt(0)!, [{ lineNumber: 1, column: 4 }, { lineNumber: 1, column: 6 }]), [ + 'var', + ' b', + ' = 1;' + ]); + + assert.deepStrictEqual(computeCellLinesContents(viewModel.cellAt(0)!, [{ lineNumber: 1, column: 1 }]), [ + '', + 'var b = 1;' + ]); + + assert.deepStrictEqual(computeCellLinesContents(viewModel.cellAt(0)!, [{ lineNumber: 1, column: 11 }]), [ + 'var b = 1;', + '', + ]); + } + ); + }); + }); diff --git a/src/vs/workbench/contrib/notebook/test/cellOutput.test.ts b/src/vs/workbench/contrib/notebook/test/cellOutput.test.ts index 77ab092ca7..8cff6b097b 100644 --- a/src/vs/workbench/contrib/notebook/test/cellOutput.test.ts +++ b/src/vs/workbench/contrib/notebook/test/cellOutput.test.ts @@ -5,12 +5,15 @@ import * as assert from 'assert'; import * as DOM from 'vs/base/browser/dom'; +import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; import { IMenuService } from 'vs/platform/actions/common/actions'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { CodeCellRenderTemplate, ICellOutputViewModel, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellOutputViewModel, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CodeCellRenderTemplate, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { OutputRendererRegistry } from 'vs/workbench/contrib/notebook/browser/view/output/rendererRegistry'; import { getStringValue } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; import { CellOutputContainer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellOutput'; @@ -41,42 +44,52 @@ OutputRendererRegistry.registerOutputTransform(class implements IOutputTransform }); suite('NotebookViewModel Outputs', async () => { - const instantiationService = setupInstantiationService(); - instantiationService.stub(INotebookService, new class extends mock() { - override getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: [], output: IOutputDto) { - if (output.outputId === 'output_id_err') { + + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let openerService: IOpenerService; + + suiteSetup(() => { + disposables = new DisposableStore(); + instantiationService = setupInstantiationService(disposables); + instantiationService.stub(INotebookService, new class extends mock() { + override getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: [], output: IOutputDto) { + if (output.outputId === 'output_id_err') { + return [{ + mimeType: 'application/vnd.code.notebook.stderr', + rendererId: BUILTIN_RENDERER_ID, + isTrusted: true + }]; + } return [{ - mimeType: 'application/vnd.code.notebook.stderr', + mimeType: 'application/vnd.code.notebook.stdout', rendererId: BUILTIN_RENDERER_ID, isTrusted: true }]; } - return [{ - mimeType: 'application/vnd.code.notebook.stdout', - rendererId: BUILTIN_RENDERER_ID, - isTrusted: true - }]; - } + }); + + instantiationService.stub(IMenuService, new class extends mock() { + override createMenu(arg: any, context: any): any { + return { + onDidChange: () => { }, + getActions: (arg: any) => { + return []; + } + }; + } + }); + + instantiationService.stub(IKeybindingService, new class extends mock() { + override lookupKeybinding(arg: any): any { + return null; + } + }); + + openerService = instantiationService.stub(IOpenerService, {}); }); - instantiationService.stub(IMenuService, new class extends mock() { - override createMenu(arg: any, context: any): any { - return { - onDidChange: () => { }, - getActions: (arg: any) => { - return []; - } - }; - } - }); - - instantiationService.stub(IKeybindingService, new class extends mock() { - override lookupKeybinding(arg: any): any { - return null; - } - }); - - const openerService = instantiationService.stub(IOpenerService, {}); + suiteTeardown(() => disposables.dispose()); test('stream outputs reuse output container', async () => { await withTestNotebook( @@ -90,8 +103,8 @@ suite('NotebookViewModel Outputs', async () => { ], (editor, viewModel, accessor) => { const container = new CellOutputContainer(editor, viewModel.viewCells[0] as CodeCellViewModel, { - outputContainer: document.createElement('div'), - outputShowMoreContainer: document.createElement('div'), + outputContainer: new FastDomNode(document.createElement('div')), + outputShowMoreContainer: new FastDomNode(document.createElement('div')), editor: { getContentHeight: () => { return 100; @@ -169,8 +182,8 @@ suite('NotebookViewModel Outputs', async () => { ], (editor, viewModel, accessor) => { const container = new CellOutputContainer(editor, viewModel.viewCells[0] as CodeCellViewModel, { - outputContainer: document.createElement('div'), - outputShowMoreContainer: document.createElement('div'), + outputContainer: new FastDomNode(document.createElement('div')), + outputShowMoreContainer: new FastDomNode(document.createElement('div')), editor: { getContentHeight: () => { return 100; @@ -182,14 +195,14 @@ suite('NotebookViewModel Outputs', async () => { assert.strictEqual(container.renderedOutputEntries.length, 5); assert.strictEqual(container.renderedOutputEntries[0].element.useDedicatedDOM, true); assert.strictEqual(container.renderedOutputEntries[1].element.useDedicatedDOM, false); - assert.strictEqual(container.renderedOutputEntries[0].element.innerContainer.innerText, '12'); + assert.strictEqual(container.renderedOutputEntries[0].element.innerContainer?.innerText, '12'); assert.strictEqual(container.renderedOutputEntries[2].element.useDedicatedDOM, true); - assert.strictEqual(container.renderedOutputEntries[2].element.innerContainer.innerText, '1000'); + assert.strictEqual(container.renderedOutputEntries[2].element.innerContainer?.innerText, '1000'); assert.strictEqual(container.renderedOutputEntries[3].element.useDedicatedDOM, true); assert.strictEqual(container.renderedOutputEntries[4].element.useDedicatedDOM, false); - assert.strictEqual(container.renderedOutputEntries[3].element.innerContainer.innerText, '45'); + assert.strictEqual(container.renderedOutputEntries[3].element.innerContainer?.innerText, '45'); editor.textModel.applyEdits([{ @@ -222,7 +235,7 @@ suite('NotebookViewModel Outputs', async () => { assert.strictEqual(container.renderedOutputEntries[0].element.innerContainer, container.renderedOutputEntries[3].element.innerContainer); assert.strictEqual(container.renderedOutputEntries[0].element.innerContainer, container.renderedOutputEntries[4].element.innerContainer); - assert.strictEqual(container.renderedOutputEntries[0].element.innerContainer.innerText, '12756'); + assert.strictEqual(container.renderedOutputEntries[0].element.innerContainer?.innerText, '12756'); }, instantiationService ); diff --git a/src/vs/workbench/contrib/notebook/test/notebookBrowser.test.ts b/src/vs/workbench/contrib/notebook/test/notebookBrowser.test.ts index 0d5f37c4c9..9f0a35effc 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookBrowser.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookBrowser.test.ts @@ -4,17 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { getRanges, ICellViewModel, reduceCellRanges } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { formatCellDuration, getRanges, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; suite('notebookBrowser', () => { - test('Reduce ranges', function () { - assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 1 }, { start: 1, end: 2 }]), [{ start: 0, end: 2 }]); - assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 2 }, { start: 1, end: 3 }]), [{ start: 0, end: 3 }]); - assert.deepStrictEqual(reduceCellRanges([{ start: 1, end: 3 }, { start: 0, end: 2 }]), [{ start: 0, end: 3 }]); - assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 2 }, { start: 4, end: 5 }]), [{ start: 0, end: 2 }, { start: 4, end: 5 }]); - }); - suite('getRanges', function () { const predicate = (cell: ICellViewModel) => cell.cellKind === CellKind.Code; @@ -55,4 +48,13 @@ suite('notebookBrowser', () => { assert.deepStrictEqual(getRanges(cells as ICellViewModel[], predicate), [{ start: 0, end: 2 }, { start: 3, end: 4 }, { start: 6, end: 7 }]); }); }); + + test('formatCellDuration', function () { + assert.strictEqual(formatCellDuration(0), '0.0s'); + assert.strictEqual(formatCellDuration(10), '0.1s'); + assert.strictEqual(formatCellDuration(200), '0.2s'); + assert.strictEqual(formatCellDuration(3300), '3.3s'); + assert.strictEqual(formatCellDuration(180000), '3m 0.0s'); + assert.strictEqual(formatCellDuration(189412), '3m 9.4s'); + }); }); diff --git a/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts index c17183e391..207b3add3c 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts @@ -4,15 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('NotebookCellList', () => { - const instantiationService = setupInstantiationService(); - const notebookDefaultOptions = new NotebookOptions(instantiationService.get(IConfigurationService)); - const topInsertToolbarHeight = notebookDefaultOptions.computeTopInserToolbarHeight(); + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let notebookDefaultOptions: NotebookOptions; + let topInsertToolbarHeight: number; + + suiteSetup(() => { + disposables = new DisposableStore(); + instantiationService = setupInstantiationService(disposables); + notebookDefaultOptions = new NotebookOptions(instantiationService.get(IConfigurationService)); + topInsertToolbarHeight = notebookDefaultOptions.computeTopInserToolbarHeight(); + + }); + + suiteTeardown(() => disposables.dispose()); test('revealElementsInView: reveal fully visible cell should not scroll', async function () { await withTestNotebook( diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts index 2e5bd1a95c..4c33cd8320 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -4,36 +4,47 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { CellKind, CellUri, diff, NotebookWorkingCopyTypeIdentifier, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { CellKind, CellUri, diff, MimeTypeDisplayOrder, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { cellIndexesToRanges, cellRangesToIndexes, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { setupInstantiationService, TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('NotebookCommon', () => { - const instantiationService = setupInstantiationService(); - const modeService = instantiationService.get(IModeService); + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let modeService: IModeService; + + suiteSetup(() => { + disposables = new DisposableStore(); + instantiationService = setupInstantiationService(disposables); + modeService = instantiationService.get(IModeService); + }); + + suiteTeardown(() => disposables.dispose()); test('sortMimeTypes default orders', function () { - const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER; - - assert.deepStrictEqual(sortMimeTypes( + assert.deepStrictEqual(new MimeTypeDisplayOrder().sort( [ 'application/json', 'application/javascript', 'text/html', 'image/svg+xml', + Mimes.latex, Mimes.markdown, 'image/png', 'image/jpeg', Mimes.text - ], [], defaultDisplayOrder), + ]), [ 'application/json', 'application/javascript', 'text/html', 'image/svg+xml', + Mimes.latex, Mimes.markdown, 'image/png', 'image/jpeg', @@ -41,9 +52,10 @@ suite('NotebookCommon', () => { ] ); - assert.deepStrictEqual(sortMimeTypes( + assert.deepStrictEqual(new MimeTypeDisplayOrder().sort( [ 'application/json', + Mimes.latex, Mimes.markdown, 'application/javascript', 'text/html', @@ -51,12 +63,13 @@ suite('NotebookCommon', () => { 'image/png', 'image/jpeg', 'image/svg+xml' - ], [], defaultDisplayOrder), + ]), [ 'application/json', 'application/javascript', 'text/html', 'image/svg+xml', + Mimes.latex, Mimes.markdown, 'image/png', 'image/jpeg', @@ -64,7 +77,7 @@ suite('NotebookCommon', () => { ] ); - assert.deepStrictEqual(sortMimeTypes( + assert.deepStrictEqual(new MimeTypeDisplayOrder().sort( [ Mimes.markdown, 'application/json', @@ -74,7 +87,7 @@ suite('NotebookCommon', () => { 'text/html', 'image/png', 'image/svg+xml' - ], [], defaultDisplayOrder), + ]), [ 'application/json', 'application/javascript', @@ -91,25 +104,25 @@ suite('NotebookCommon', () => { test('sortMimeTypes user orders', function () { - const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER; - assert.deepStrictEqual(sortMimeTypes( - [ - 'application/json', - 'application/javascript', - 'text/html', - 'image/svg+xml', - Mimes.markdown, - 'image/png', - 'image/jpeg', - Mimes.text - ], - [ + assert.deepStrictEqual( + new MimeTypeDisplayOrder([ 'image/png', Mimes.text, Mimes.markdown, 'text/html', 'application/json' - ], defaultDisplayOrder), + ]).sort( + [ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + Mimes.markdown, + 'image/png', + 'image/jpeg', + Mimes.text + ] + ), [ 'image/png', Mimes.text, @@ -122,8 +135,14 @@ suite('NotebookCommon', () => { ] ); - assert.deepStrictEqual(sortMimeTypes( - [ + assert.deepStrictEqual( + new MimeTypeDisplayOrder([ + 'application/json', + 'text/html', + 'text/html', + Mimes.markdown, + 'application/json' + ]).sort([ Mimes.markdown, 'application/json', Mimes.text, @@ -132,14 +151,7 @@ suite('NotebookCommon', () => { 'image/svg+xml', 'image/jpeg', 'image/png' - ], - [ - 'application/json', - 'text/html', - 'text/html', - Mimes.markdown, - 'application/json' - ], defaultDisplayOrder), + ]), [ 'application/json', 'text/html', @@ -153,53 +165,62 @@ suite('NotebookCommon', () => { ); }); + test('prioritizes mimetypes', () => { + const m = new MimeTypeDisplayOrder([ + Mimes.markdown, + 'text/html', + 'application/json' + ]); + assert.deepStrictEqual(m.toArray(), [Mimes.markdown, 'text/html', 'application/json']); + + // no-op if already in the right order + m.prioritize('text/html', ['application/json']); + assert.deepStrictEqual(m.toArray(), [Mimes.markdown, 'text/html', 'application/json']); + + // sorts to highest priority + m.prioritize('text/html', ['application/json', Mimes.markdown]); + assert.deepStrictEqual(m.toArray(), ['text/html', Mimes.markdown, 'application/json']); + + // adds in new type + m.prioritize('text/plain', ['application/json', Mimes.markdown]); + assert.deepStrictEqual(m.toArray(), ['text/plain', 'text/html', Mimes.markdown, 'application/json']); + + // moves multiple, preserves order + m.prioritize(Mimes.markdown, ['text/plain', 'application/json', Mimes.markdown]); + assert.deepStrictEqual(m.toArray(), ['text/html', Mimes.markdown, 'text/plain', 'application/json']); + + // deletes multiple + m.prioritize('text/plain', ['text/plain', 'text/html', Mimes.markdown]); + assert.deepStrictEqual(m.toArray(), ['text/plain', 'text/html', Mimes.markdown, 'application/json']); + + // handles multiple mimetypes, unknown mimetype + const m2 = new MimeTypeDisplayOrder(['a', 'b']); + m2.prioritize('b', ['a', 'b', 'a', 'q']); + assert.deepStrictEqual(m2.toArray(), ['b', 'a']); + }); + test('sortMimeTypes glob', function () { - const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER; - - // unknown mime types come last - assert.deepStrictEqual(sortMimeTypes( - [ - 'application/json', - 'application/vnd-vega.json', - 'application/vnd-plot.json', - 'application/javascript', - 'text/html' - ], - [ - Mimes.markdown, - 'text/html', - 'application/json' - ], defaultDisplayOrder), - [ - 'text/html', - 'application/json', - 'application/javascript', - 'application/vnd-vega.json', - 'application/vnd-plot.json' - ], - 'unknown mimetypes keep the ordering' - ); - - assert.deepStrictEqual(sortMimeTypes( - [ - 'application/json', - 'application/javascript', - 'text/html', - 'application/vnd-plot.json', - 'application/vnd-vega.json' - ], - [ + assert.deepStrictEqual( + new MimeTypeDisplayOrder([ 'application/vnd-vega*', Mimes.markdown, 'text/html', 'application/json' - ], defaultDisplayOrder), + ]).sort( + [ + 'application/json', + 'application/javascript', + 'text/html', + 'application/vnd-plot.json', + 'application/vnd-vega.json' + ] + ), [ 'application/vnd-vega.json', 'text/html', 'application/json', + 'application/vnd-plot.json', 'application/javascript', - 'application/vnd-plot.json' ], 'glob *' ); @@ -323,6 +344,30 @@ suite('CellRange', function () { assert.deepStrictEqual(cellIndexesToRanges([9, 10]), [{ start: 9, end: 11 }]); assert.deepStrictEqual(cellIndexesToRanges([10, 9]), [{ start: 9, end: 11 }]); }); + + test('Reduce ranges', function () { + assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 1 }, { start: 1, end: 2 }]), [{ start: 0, end: 2 }]); + assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 2 }, { start: 1, end: 3 }]), [{ start: 0, end: 3 }]); + assert.deepStrictEqual(reduceCellRanges([{ start: 1, end: 3 }, { start: 0, end: 2 }]), [{ start: 0, end: 3 }]); + assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 2 }, { start: 4, end: 5 }]), [{ start: 0, end: 2 }, { start: 4, end: 5 }]); + + assert.deepStrictEqual(reduceCellRanges([ + { start: 0, end: 1 }, + { start: 1, end: 2 }, + { start: 4, end: 6 } + ]), [ + { start: 0, end: 2 }, + { start: 4, end: 6 } + ]); + + assert.deepStrictEqual(reduceCellRanges([ + { start: 0, end: 1 }, + { start: 1, end: 3 }, + { start: 3, end: 4 } + ]), [ + { start: 0, end: 4 } + ]); + }); }); suite('NotebookWorkingCopyTypeIdentifier', function () { diff --git a/src/vs/workbench/contrib/notebook/test/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/notebookDiff.test.ts index 0a5d03444a..766e2dc4d9 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookDiff.test.ts @@ -7,12 +7,14 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; import { LcsDiff } from 'vs/base/common/diff/diff'; import { Mimes } from 'vs/base/common/mime'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; import { CellKind, CellSequence } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebookDiffModel } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('NotebookCommon', () => { + const configurationService = new TestConfigurationService(); test('diff different source', async () => { await withTestNotebookDiffModel([ @@ -36,7 +38,7 @@ suite('NotebookCommon', () => { }]); const eventDispatcher = new NotebookDiffEditorEventDispatcher(); - const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, model, eventDispatcher, { + const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }); assert.strictEqual(diffViewModels.viewModels.length, 1); @@ -68,7 +70,7 @@ suite('NotebookCommon', () => { }]); const eventDispatcher = new NotebookDiffEditorEventDispatcher(); - const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, model, eventDispatcher, { + const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }); assert.strictEqual(diffViewModels.viewModels.length, 2); @@ -99,7 +101,7 @@ suite('NotebookCommon', () => { }]); const eventDispatcher = new NotebookDiffEditorEventDispatcher(); - const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, model, eventDispatcher, { + const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }); assert.strictEqual(diffViewModels.viewModels.length, 1); @@ -137,7 +139,7 @@ suite('NotebookCommon', () => { }]); const eventDispatcher = new NotebookDiffEditorEventDispatcher(); - const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, model, eventDispatcher, { + const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }); assert.strictEqual(diffViewModels.viewModels.length, 1); @@ -158,7 +160,7 @@ suite('NotebookCommon', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); const eventDispatcher = new NotebookDiffEditorEventDispatcher(); - const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, model, eventDispatcher, { + const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }); assert.strictEqual(diffViewModels.viewModels.length, 3); @@ -181,7 +183,7 @@ suite('NotebookCommon', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); const diffResult = diff.ComputeDiff(false); const eventDispatcher = new NotebookDiffEditorEventDispatcher(); - const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, model, eventDispatcher, { + const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: diffResult }); assert.strictEqual(diffViewModels.viewModels.length, 3); @@ -201,7 +203,7 @@ suite('NotebookCommon', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}] ], (model, accessor) => { const eventDispatcher = new NotebookDiffEditorEventDispatcher(); - const diffResult = NotebookTextDiffEditor.computeDiff(accessor, model, eventDispatcher, { + const diffResult = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: { changes: [{ originalStart: 0, @@ -241,7 +243,7 @@ suite('NotebookCommon', () => { ['var g = 7;', 'javascript', CellKind.Code, [], {}], ], async (model, accessor) => { const eventDispatcher = new NotebookDiffEditorEventDispatcher(); - const diffResult = NotebookTextDiffEditor.computeDiff(accessor, model, eventDispatcher, { + const diffResult = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { cellsDiff: { changes: [{ originalStart: 0, @@ -353,4 +355,46 @@ suite('NotebookCommon', () => { }]); }); }); + + test('diff output', async () => { + await withTestNotebookDiffModel([ + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ], [ + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ], (model, accessor) => { + const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); + const diffResult = diff.ComputeDiff(false); + const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { + cellsDiff: diffResult + }); + assert.strictEqual(diffViewModels.viewModels.length, 2); + assert.strictEqual(diffViewModels.viewModels[0].type, 'unchanged'); + assert.strictEqual(diffViewModels.viewModels[0].checkIfOutputsModified(), false); + assert.strictEqual(diffViewModels.viewModels[1].type, 'modified'); + assert.strictEqual(diffViewModels.viewModels[1].checkIfOutputsModified(), true); + }); + }); + + test('diff output fast check', async () => { + await withTestNotebookDiffModel([ + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ], [ + ['x', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ['y', 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { custom: { metadata: { collapsed: false } }, executionOrder: 3 }], + ], (model, accessor) => { + const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); + const diffResult = diff.ComputeDiff(false); + const eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const diffViewModels = NotebookTextDiffEditor.computeDiff(accessor, configurationService, model, eventDispatcher, { + cellsDiff: diffResult + }); + assert.strictEqual(diffViewModels.viewModels.length, 2); + assert.strictEqual(diffViewModels.viewModels[0].original!.textModel.equal(diffViewModels.viewModels[0].modified!.textModel), true); + assert.strictEqual(diffViewModels.viewModels[1].original!.textModel.equal(diffViewModels.viewModels[1].modified!.textModel), false); + }); + }); }); diff --git a/src/vs/workbench/contrib/notebook/test/notebookEditor.test.ts b/src/vs/workbench/contrib/notebook/test/notebookEditor.test.ts index 12d0b6aaca..d6963693c5 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookEditor.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookEditor.test.ts @@ -4,15 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; -import { expandCellRangesWithHiddenCells, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { expandCellRangesWithHiddenCells, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('ListViewInfoAccessor', () => { - const instantiationService = setupInstantiationService(); + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + + suiteSetup(() => { + disposables = new DisposableStore(); + instantiationService = setupInstantiationService(disposables); + }); + + suiteTeardown(() => disposables.dispose()); test('basics', async function () { await withTestNotebook( @@ -52,17 +62,18 @@ suite('ListViewInfoAccessor', () => { assert.deepStrictEqual(listViewInfoAccessor.getCellRangeFromViewRange(0, 1), { start: 0, end: 2 }); assert.deepStrictEqual(listViewInfoAccessor.getCellRangeFromViewRange(1, 2), { start: 2, end: 5 }); - assert.deepStrictEqual(listViewInfoAccessor.getCellsFromViewRange(0, 1), viewModel.getCells({ start: 0, end: 2 })); - assert.deepStrictEqual(listViewInfoAccessor.getCellsFromViewRange(1, 2), viewModel.getCells({ start: 2, end: 5 })); + assert.deepStrictEqual(listViewInfoAccessor.getCellsFromViewRange(0, 1), viewModel.getCellsInRange({ start: 0, end: 2 })); + assert.deepStrictEqual(listViewInfoAccessor.getCellsFromViewRange(1, 2), viewModel.getCellsInRange({ start: 2, end: 5 })); const notebookEditor = new class extends mock() { - override getViewIndex(cell: ICellViewModel) { return listViewInfoAccessor.getViewIndex(cell); } + override getViewIndexByModelIndex(index: number) { return listViewInfoAccessor.getViewIndex(viewModel.viewCells[index]!); } override getCellRangeFromViewRange(startIndex: number, endIndex: number) { return listViewInfoAccessor.getCellRangeFromViewRange(startIndex, endIndex); } + override cellAt(index: number) { return viewModel.cellAt(index); } }; - assert.deepStrictEqual(expandCellRangesWithHiddenCells(notebookEditor, viewModel, [{ start: 0, end: 1 }]), [{ start: 0, end: 2 }]); - assert.deepStrictEqual(expandCellRangesWithHiddenCells(notebookEditor, viewModel, [{ start: 2, end: 3 }]), [{ start: 2, end: 5 }]); - assert.deepStrictEqual(expandCellRangesWithHiddenCells(notebookEditor, viewModel, [{ start: 0, end: 1 }, { start: 2, end: 3 }]), [{ start: 0, end: 5 }]); + assert.deepStrictEqual(expandCellRangesWithHiddenCells(notebookEditor, [{ start: 0, end: 1 }]), [{ start: 0, end: 2 }]); + assert.deepStrictEqual(expandCellRangesWithHiddenCells(notebookEditor, [{ start: 2, end: 3 }]), [{ start: 2, end: 5 }]); + assert.deepStrictEqual(expandCellRangesWithHiddenCells(notebookEditor, [{ start: 0, end: 1 }, { start: 2, end: 3 }]), [{ start: 0, end: 5 }]); }); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts b/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts index ea9756d9fc..8ca70ee5aa 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookEditorKernelManager.test.ts @@ -21,18 +21,19 @@ import { mock } from 'vs/base/test/common/mock'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; +import { insertCellAtIndex } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; suite('NotebookEditorKernelManager', () => { let instantiationService: TestInstantiationService; let kernelService: INotebookKernelService; - const dispoables = new DisposableStore(); + let disposables: DisposableStore; setup(function () { - dispoables.clear(); + disposables = new DisposableStore(); - instantiationService = setupInstantiationService(); + instantiationService = setupInstantiationService(disposables); instantiationService.stub(INotebookService, new class extends mock() { override onDidAddNotebookDocument = Event.None; @@ -45,8 +46,12 @@ suite('NotebookEditorKernelManager', () => { }); + teardown(() => { + disposables.dispose(); + }); + async function withTestNotebook(cells: [string, string, CellKind, IOutputDto[], NotebookCellMetadata][], callback: (viewModel: NotebookViewModel, textModel: NotebookTextModel) => void | Promise) { - return _withTestNotebook(cells, (editor) => callback(editor.viewModel, editor.viewModel.notebookDocument)); + return _withTestNotebook(cells, (editor, viewModel) => callback(viewModel, viewModel.notebookDocument)); } // test('ctor', () => { @@ -62,7 +67,7 @@ suite('NotebookEditorKernelManager', () => { async (viewModel) => { const kernelManager = instantiationService.createInstance(NotebookEditorKernelManager); - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); }); }); @@ -74,7 +79,7 @@ suite('NotebookEditorKernelManager', () => { kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] })); const kernelManager = instantiationService.createInstance(NotebookEditorKernelManager); - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); await assertThrowsAsync(async () => await kernelManager.executeNotebookCell(cell)); }); @@ -90,7 +95,7 @@ suite('NotebookEditorKernelManager', () => { const executeSpy = sinon.spy(); kernel.executeNotebookCellsRequest = executeSpy; - const cell = viewModel.createCell(0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); await kernelManager.executeNotebookCells(viewModel.notebookDocument, [cell]); assert.strictEqual(executeSpy.calledOnce, true); }); @@ -121,7 +126,7 @@ suite('NotebookEditorKernelManager', () => { let event: ISelectedNotebooksChangeEvent | undefined; kernelService.onDidChangeSelectedNotebooks(e => event = e); - const cell = viewModel.createCell(0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); await kernelManager.executeNotebookCells(viewModel.notebookDocument, [cell]); assert.strictEqual(didExecute, true); diff --git a/src/vs/workbench/contrib/notebook/test/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookEditorModel.test.ts index 048c89b013..9c48610c0d 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookEditorModel.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; @@ -26,10 +26,19 @@ import { setupInstantiationService } from 'vs/workbench/contrib/notebook/test/te import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Mimes } from 'vs/base/common/mime'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; suite('NotebookFileWorkingCopyModel', function () { - const instantiationService = setupInstantiationService(); + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + + suiteSetup(() => { + disposables = new DisposableStore(); + instantiationService = setupInstantiationService(disposables); + }); + + suiteTeardown(() => disposables.dispose()); test('no transient output is send to serializer', function () { diff --git a/src/vs/workbench/contrib/notebook/test/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/notebookKernelService.test.ts index 40786aa94b..7cc528f338 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookKernelService.test.ts @@ -21,17 +21,17 @@ suite('NotebookKernelService', () => { let instantiationService: TestInstantiationService; let kernelService: INotebookKernelService; - const dispoables = new DisposableStore(); + let disposables: DisposableStore; let onDidAddNotebookDocument: Emitter; setup(function () { - dispoables.clear(); + disposables = new DisposableStore(); onDidAddNotebookDocument = new Emitter(); - dispoables.add(onDidAddNotebookDocument); + disposables.add(onDidAddNotebookDocument); - instantiationService = setupInstantiationService(); + instantiationService = setupInstantiationService(disposables); instantiationService.stub(INotebookService, new class extends mock() { override onDidAddNotebookDocument = onDidAddNotebookDocument.event; override onWillRemoveNotebookDocument = Event.None; @@ -41,6 +41,9 @@ suite('NotebookKernelService', () => { instantiationService.set(INotebookKernelService, kernelService); }); + teardown(() => { + disposables.dispose(); + }); test('notebook priorities', function () { diff --git a/src/vs/workbench/contrib/notebook/test/notebookSelection.test.ts b/src/vs/workbench/contrib/notebook/test/notebookSelection.test.ts index c16c5b1589..736ecdb349 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookSelection.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookSelection.test.ts @@ -4,8 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; +import { runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, TestCell, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; @@ -21,8 +24,18 @@ suite('NotebookSelection', () => { }); suite('NotebookCellList focus/selection', () => { - const instantiationService = setupInstantiationService(); - const modeService = instantiationService.get(IModeService); + + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let modeService: IModeService; + + suiteSetup(() => { + disposables = new DisposableStore(); + instantiationService = setupInstantiationService(disposables); + modeService = instantiationService.get(IModeService); + }); + + suiteTeardown(() => disposables.dispose()); test('notebook cell list setFocus', async function () { await withTestNotebook( @@ -288,9 +301,10 @@ suite('NotebookCellList focus/selection', () => { ], (editor, viewModel) => { viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 1, end: 2 }, selections: [{ start: 1, end: 2 }] }); - viewModel.deleteCell(1, true, false); + runDeleteAction(editor, viewModel.cellAt(1)!); + // viewModel.deleteCell(1, true, false); assert.deepStrictEqual(viewModel.getFocus(), { start: 0, end: 1 }); - assert.deepStrictEqual(viewModel.getSelections(), []); + assert.deepStrictEqual(viewModel.getSelections(), [{ start: 0, end: 1 }]); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/notebookServiceImpl.test.ts b/src/vs/workbench/contrib/notebook/test/notebookServiceImpl.test.ts index 8f32577e4d..934140364b 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookServiceImpl.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookServiceImpl.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -22,8 +23,8 @@ import { workbenchInstantiationService } from 'vs/workbench/test/browser/workben suite('NotebookProviderInfoStore', function () { test('Can\'t open untitled notebooks in test #119363', function () { - - const instantiationService = workbenchInstantiationService(); + const disposables = new DisposableStore(); + const instantiationService = workbenchInstantiationService(undefined, disposables); const store = new NotebookProviderInfoStore( new class extends mock() { override get() { return ''; } @@ -37,7 +38,7 @@ suite('NotebookProviderInfoStore', function () { new class extends mock() { }, instantiationService, new class extends mock() { - override canHandleResource() { return true; } + override hasProvider() { return true; } }, new class extends mock() { } ); @@ -84,6 +85,8 @@ suite('NotebookProviderInfoStore', function () { providers = store.getContributedNotebook(URI.parse('untitled:///test/nb.bar')); assert.strictEqual(providers.length, 1); assert.strictEqual(providers[0] === barInfo, true); + + disposables.dispose(); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index b76bceda9e..e0e4a2f9dc 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -5,16 +5,27 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { CellEditType, CellKind, ICellEditOperation, NotebookTextModelChangedEvent, NotebookTextModelWillAddRemoveEvent, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { setupInstantiationService, TestCell, valueBytesFromString, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('NotebookTextModel', () => { - const instantiationService = setupInstantiationService(); - const modeService = instantiationService.get(IModeService); - instantiationService.spy(IUndoRedoService, 'pushElement'); + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let modeService: IModeService; + + suiteSetup(() => { + disposables = new DisposableStore(); + instantiationService = setupInstantiationService(disposables); + modeService = instantiationService.get(IModeService); + instantiationService.spy(IUndoRedoService, 'pushElement'); + }); + + suiteTeardown(() => disposables.dispose()); test('insert', async function () { await withTestNotebook( diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 76f61d20e7..6b530570e5 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; @@ -12,10 +13,11 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { reduceCellRanges } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { insertCellAtIndex, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; @@ -26,15 +28,28 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { NotebookEditorTestModel, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('NotebookViewModel', () => { - const instantiationService = setupInstantiationService(); - const textModelService = instantiationService.get(ITextModelService); - const bulkEditService = instantiationService.get(IBulkEditService); - const undoRedoService = instantiationService.get(IUndoRedoService); - const modelService = instantiationService.get(IModelService); - const modeService = instantiationService.get(IModeService); + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let textModelService: ITextModelService; + let bulkEditService: IBulkEditService; + let undoRedoService: IUndoRedoService; + let modelService: IModelService; + let modeService: IModeService; - instantiationService.stub(IConfigurationService, new TestConfigurationService()); - instantiationService.stub(IThemeService, new TestThemeService()); + suiteSetup(() => { + disposables = new DisposableStore(); + instantiationService = setupInstantiationService(disposables); + textModelService = instantiationService.get(ITextModelService); + bulkEditService = instantiationService.get(IBulkEditService); + undoRedoService = instantiationService.get(IUndoRedoService); + modelService = instantiationService.get(IModelService); + modeService = instantiationService.get(IModeService); + + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IThemeService, new TestThemeService()); + }); + + suiteTeardown(() => disposables.dispose()); test('ctor', function () { const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false }, undoRedoService, modelService, modeService); @@ -51,12 +66,12 @@ suite('NotebookViewModel', () => { ['var b = 2;', 'javascript', CellKind.Code, [], {}] ], (editor, viewModel) => { - const cell = viewModel.createCell(1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true, null, []); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); assert.strictEqual(viewModel.length, 3); assert.strictEqual(viewModel.notebookDocument.cells.length, 3); assert.strictEqual(viewModel.getCellIndex(cell), 1); - viewModel.deleteCell(1, true); + runDeleteAction(editor, viewModel.cellAt(1)!); assert.strictEqual(viewModel.length, 2); assert.strictEqual(viewModel.notebookDocument.cells.length, 2); assert.strictEqual(viewModel.getCellIndex(cell), -1); @@ -64,56 +79,6 @@ suite('NotebookViewModel', () => { ); }); - test('move cells down', async function () { - await withTestNotebook( - [ - ['//a', 'javascript', CellKind.Code, [], {}], - ['//b', 'javascript', CellKind.Code, [], {}], - ['//c', 'javascript', CellKind.Code, [], {}], - ], - (editor, viewModel) => { - viewModel.moveCellToIdx(0, 1, 0, true); - // no-op - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//b'); - - viewModel.moveCellToIdx(0, 1, 1, true); - // b, a, c - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//c'); - - viewModel.moveCellToIdx(0, 1, 2, true); - // a, c, b - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//c'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//b'); - } - ); - }); - - test('move cells up', async function () { - await withTestNotebook( - [ - ['//a', 'javascript', CellKind.Code, [], {}], - ['//b', 'javascript', CellKind.Code, [], {}], - ['//c', 'javascript', CellKind.Code, [], {}], - ], - (editor, viewModel) => { - viewModel.moveCellToIdx(1, 1, 0, true); - // b, a, c - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//a'); - - viewModel.moveCellToIdx(2, 1, 0, true); - // c, b, a - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//c'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//a'); - } - ); - }); - test('index', async function () { await withTestNotebook( [ @@ -125,13 +90,13 @@ suite('NotebookViewModel', () => { const lastViewCell = viewModel.cellAt(viewModel.length - 1)!; const insertIndex = viewModel.getCellIndex(firstViewCell) + 1; - const cell = viewModel.createCell(insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true); const addedCellIndex = viewModel.getCellIndex(cell); - viewModel.deleteCell(addedCellIndex, true); + runDeleteAction(editor, viewModel.cellAt(addedCellIndex)!); const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1; - const cell2 = viewModel.createCell(secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true); + const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true); assert.strictEqual(viewModel.length, 3); assert.strictEqual(viewModel.notebookDocument.cells.length, 3); @@ -184,35 +149,35 @@ suite('NotebookViewModel Decorations', () => { end: 2, }); - viewModel.createCell(0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 2, end: 3 }); - viewModel.deleteCell(0, true); + runDeleteAction(editor, viewModel.cellAt(0)!); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 2 }); - viewModel.createCell(3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 3 }); - viewModel.deleteCell(3, true); + runDeleteAction(editor, viewModel.cellAt(3)!); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 2 }); - viewModel.deleteCell(1, true); + runDeleteAction(editor, viewModel.cellAt(1)!); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 0, @@ -241,14 +206,14 @@ suite('NotebookViewModel Decorations', () => { end: 3 }); - viewModel.createCell(5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 3 }); - viewModel.createCell(4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, @@ -258,25 +223,6 @@ suite('NotebookViewModel Decorations', () => { ); }); - test('reduce range', async function () { - assert.deepStrictEqual(reduceCellRanges([ - { start: 0, end: 1 }, - { start: 1, end: 2 }, - { start: 4, end: 6 } - ]), [ - { start: 0, end: 2 }, - { start: 4, end: 6 } - ]); - - assert.deepStrictEqual(reduceCellRanges([ - { start: 0, end: 1 }, - { start: 1, end: 2 }, - { start: 3, end: 4 } - ]), [ - { start: 0, end: 4 } - ]); - }); - test('diff hidden ranges', async function () { assert.deepStrictEqual(getVisibleCells([1, 2, 3, 4, 5], []), [1, 2, 3, 4, 5]); @@ -369,48 +315,18 @@ suite('NotebookViewModel API', () => { ['# header b', 'markdown', CellKind.Markup, [], {}] ], (editor, viewModel) => { - assert.strictEqual(viewModel.getCells().length, 3); - assert.deepStrictEqual(viewModel.getCells({ start: 0, end: 1 }).map(cell => cell.getText()), ['# header a']); - assert.deepStrictEqual(viewModel.getCells({ start: 0, end: 2 }).map(cell => cell.getText()), ['# header a', 'var b = 1;']); - assert.deepStrictEqual(viewModel.getCells({ start: 0, end: 3 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']); - assert.deepStrictEqual(viewModel.getCells({ start: 0, end: 4 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']); - assert.deepStrictEqual(viewModel.getCells({ start: 1, end: 4 }).map(cell => cell.getText()), ['var b = 1;', '# header b']); - assert.deepStrictEqual(viewModel.getCells({ start: 2, end: 4 }).map(cell => cell.getText()), ['# header b']); - assert.deepStrictEqual(viewModel.getCells({ start: 3, end: 4 }).map(cell => cell.getText()), []); + assert.strictEqual(viewModel.getCellsInRange().length, 3); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 1 }).map(cell => cell.getText()), ['# header a']); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 2 }).map(cell => cell.getText()), ['# header a', 'var b = 1;']); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 3 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: 0, end: 4 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: 1, end: 4 }).map(cell => cell.getText()), ['var b = 1;', '# header b']); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: 2, end: 4 }).map(cell => cell.getText()), ['# header b']); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: 3, end: 4 }).map(cell => cell.getText()), []); // no one should use an invalid range but `getCells` should be able to handle that. - assert.deepStrictEqual(viewModel.getCells({ start: -1, end: 1 }).map(cell => cell.getText()), ['# header a']); - assert.deepStrictEqual(viewModel.getCells({ start: 3, end: 0 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']); - } - ); - }); - - test('split cell', async function () { - await withTestNotebook( - [ - ['var b = 1;', 'javascript', CellKind.Code, [], {}] - ], - (editor, viewModel) => { - assert.deepStrictEqual(viewModel.computeCellLinesContents(viewModel.cellAt(0)!, [{ lineNumber: 1, column: 4 }]), [ - 'var', - ' b = 1;' - ]); - - assert.deepStrictEqual(viewModel.computeCellLinesContents(viewModel.cellAt(0)!, [{ lineNumber: 1, column: 4 }, { lineNumber: 1, column: 6 }]), [ - 'var', - ' b', - ' = 1;' - ]); - - assert.deepStrictEqual(viewModel.computeCellLinesContents(viewModel.cellAt(0)!, [{ lineNumber: 1, column: 1 }]), [ - '', - 'var b = 1;' - ]); - - assert.deepStrictEqual(viewModel.computeCellLinesContents(viewModel.cellAt(0)!, [{ lineNumber: 1, column: 11 }]), [ - 'var b = 1;', - '', - ]); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: -1, end: 1 }).map(cell => cell.getText()), ['# header a']); + assert.deepStrictEqual(viewModel.getCellsInRange({ start: 3, end: 0 }).map(cell => cell.getText()), ['# header a', 'var b = 1;', '# header b']); } ); }); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index bfa7a260ab..64a57b5756 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -5,49 +5,55 @@ import * as DOM from 'vs/base/browser/dom'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { VSBuffer } from 'vs/base/common/buffer'; import { NotImplementedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Mimes } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; +import { EditorFontLigatures } from 'vs/editor/common/config/editorOptions'; +import { FontInfo } from 'vs/editor/common/config/fontInfo'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { BrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { NullCommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { IEditorInput } from 'vs/workbench/common/editor'; -import { EditorModel } from 'vs/workbench/common/editor/editorModel'; -import { ICellViewModel, IActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellUri, INotebookDiffEditorModel, INotebookEditorModel, IOutputDto, IResolvedNotebookEditorModel, NotebookCellMetadata, SelectionStateType, } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; -import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { mock } from 'vs/base/test/common/mock'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { BrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; -import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; -import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { IActiveNotebookEditorDelegate, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { Mimes } from 'vs/base/common/mime'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellKind, CellUri, INotebookDiffEditorModel, INotebookEditorModel, IOutputDto, IResolvedNotebookEditorModel, NotebookCellMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; export class TestCell extends NotebookCellTextModel { constructor( @@ -141,7 +147,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi return false; } - saveAs(): Promise { + saveAs(): Promise { throw new NotImplementedError(); } @@ -150,12 +156,13 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi } } -export function setupInstantiationService() { +export function setupInstantiationService(disposables = new DisposableStore()) { const instantiationService = new TestInstantiationService(); - instantiationService.stub(IModeService, new ModeServiceImpl()); + instantiationService.stub(IModeService, disposables.add(new ModeServiceImpl())); instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IThemeService, new TestThemeService()); + instantiationService.stub(ILanguageConfigurationService, new TestLanguageConfigurationService()); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); instantiationService.stub(IContextKeyService, instantiationService.createInstance(ContextKeyService)); @@ -168,7 +175,7 @@ export function setupInstantiationService() { return instantiationService; } -function _createTestNotebookEditor(instantiationService: TestInstantiationService, cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][]): { editor: IActiveNotebookEditor, viewModel: NotebookViewModel; } { +function _createTestNotebookEditor(instantiationService: TestInstantiationService, cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][]): { editor: IActiveNotebookEditorDelegate, viewModel: NotebookViewModel; } { const viewType = 'notebook'; const notebook = instantiationService.createInstance(NotebookTextModel, viewType, URI.parse('test'), cells.map(cell => { @@ -190,17 +197,20 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic cellList.attachViewModel(viewModel); const listViewInfoAccessor = new ListViewInfoAccessor(cellList); - const notebookEditor: IActiveNotebookEditor = new class extends mock() { + const notebookEditor: IActiveNotebookEditorDelegate = new class extends mock() { override dispose() { viewModel.dispose(); } override notebookOptions = notebookOptions; override onDidChangeModel: Event = new Emitter().event; - override get viewModel() { return viewModel; } - override get textModel() { return viewModel.notebookDocument; } - override hasModel(): this is IActiveNotebookEditor { - return !!this.viewModel; + override _getViewModel(): NotebookViewModel { + return viewModel; } + override textModel = viewModel.notebookDocument; + override hasModel(): this is IActiveNotebookEditorDelegate { + return !!viewModel; + } + override getLength() { return viewModel.length; } override getFocus() { return viewModel.getFocus(); } override getSelections() { return viewModel.getSelections(); } override setFocus(focus: ICellRange) { @@ -217,7 +227,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic selections: selections }); } - override getViewIndex(cell: ICellViewModel) { return listViewInfoAccessor.getViewIndex(cell); } + override getViewIndexByModelIndex(index: number) { return listViewInfoAccessor.getViewIndex(viewModel.viewCells[index]); } override getCellRangeFromViewRange(startIndex: number, endIndex: number) { return listViewInfoAccessor.getCellRangeFromViewRange(startIndex, endIndex); } override revealCellRangeInView() { } override setHiddenAreas(_ranges: ICellRange[]): boolean { @@ -239,22 +249,61 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic override focusElement() { } override setCellEditorSelection() { } override async revealRangeInCenterIfOutsideViewportAsync() { } - override getOutputRenderer() { return new OutputRenderer(notebookEditor, instantiationService); } + override getOutputRenderer() { + return new OutputRenderer({ + creationOptions: notebookEditor.creationOptions, + getCellOutputLayoutInfo() { + return { + height: 100, + width: 100, + fontInfo: new FontInfo({ + zoomLevel: 0, + pixelRatio: 1, + fontFamily: 'mockFont', + fontWeight: 'normal', + fontSize: 14, + fontFeatureSettings: EditorFontLigatures.OFF, + lineHeight: 19, + letterSpacing: 1.5, + isMonospace: true, + typicalHalfwidthCharacterWidth: 10, + typicalFullwidthCharacterWidth: 20, + canUseHalfwidthRightwardsArrow: true, + spaceWidth: 10, + middotWidth: 10, + wsmiddotWidth: 10, + maxDigitWidth: 10, + }, true) + }; + } + }, instantiationService, NullCommandService); + } override async layoutNotebookCell() { } override async removeInset() { } + override async focusNotebookCell() { } + override cellAt(index: number) { return viewModel.cellAt(index)!; } + override getCellIndex(cell: ICellViewModel) { return viewModel.getCellIndex(cell); } + override getCellsInRange(range?: ICellRange) { return viewModel.getCellsInRange(range); } + override getNextVisibleCellIndex(index: number) { return viewModel.getNextVisibleCellIndex(index); } + getControl() { return this; } + override get onDidChangeSelection() { return viewModel.onDidChangeSelection as Event; } + override get onDidChangeOptions() { return viewModel.onDidChangeOptions; } + override get onDidChangeViewCells() { return viewModel.onDidChangeViewCells; } + }; return { editor: notebookEditor, viewModel }; } -export function createTestNotebookEditor(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][]): { editor: IActiveNotebookEditor, viewModel: NotebookViewModel; } { - return _createTestNotebookEditor(setupInstantiationService(), cells); +export function createTestNotebookEditor(instantiationService: TestInstantiationService, cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][]): { editor: INotebookEditorDelegate, viewModel: NotebookViewModel; } { + return _createTestNotebookEditor(instantiationService, cells); } export async function withTestNotebookDiffModel(originalCells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], modifiedCells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (diffModel: INotebookDiffEditorModel, accessor: TestInstantiationService) => Promise | R): Promise { - const instantiationService = setupInstantiationService(); - const originalNotebook = createTestNotebookEditor(originalCells); - const modifiedNotebook = createTestNotebookEditor(modifiedCells); + const disposables = new DisposableStore(); + const instantiationService = setupInstantiationService(disposables); + const originalNotebook = createTestNotebookEditor(instantiationService, originalCells); + const modifiedNotebook = createTestNotebookEditor(instantiationService, modifiedCells); const originalResource = new class extends mock() { override get notebook() { return originalNotebook.viewModel.notebookDocument; @@ -283,18 +332,21 @@ export async function withTestNotebookDiffModel(originalCells: [source: originalNotebook.viewModel.dispose(); modifiedNotebook.editor.dispose(); modifiedNotebook.viewModel.dispose(); + disposables.dispose(); }); } else { originalNotebook.editor.dispose(); originalNotebook.viewModel.dispose(); modifiedNotebook.editor.dispose(); modifiedNotebook.viewModel.dispose(); + disposables.dispose(); } return res; } -export async function withTestNotebook(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (editor: IActiveNotebookEditor, viewModel: NotebookViewModel, accessor: TestInstantiationService) => Promise | R, accessor?: TestInstantiationService): Promise { - const instantiationService = accessor ?? setupInstantiationService(); +export async function withTestNotebook(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (editor: IActiveNotebookEditorDelegate, viewModel: NotebookViewModel, accessor: TestInstantiationService) => Promise | R, accessor?: TestInstantiationService): Promise { + const disposables = new DisposableStore(); + const instantiationService = accessor ?? setupInstantiationService(disposables); const notebookEditor = _createTestNotebookEditor(instantiationService, cells); const res = await callback(notebookEditor.editor, notebookEditor.viewModel, instantiationService); @@ -302,10 +354,12 @@ export async function withTestNotebook(cells: [source: string, lang: st res.finally(() => { notebookEditor.editor.dispose(); notebookEditor.viewModel.dispose(); + disposables.dispose(); }); } else { notebookEditor.editor.dispose(); notebookEditor.viewModel.dispose(); + disposables.dispose(); } return res; } diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 794a61bec0..0ec0ed9003 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -11,7 +11,7 @@ import { IDisposable, toDisposable, DisposableStore, MutableDisposable } from 'v import { LRUCache } from 'vs/base/common/map'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyEqualsExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -66,7 +66,8 @@ export class OutlinePane extends ViewPane { private readonly _disposables = new DisposableStore(); - private readonly _editorDisposables = new DisposableStore(); + private readonly _editorControlDisposables = new DisposableStore(); + private readonly _editorPaneDisposables = new DisposableStore(); private readonly _outlineViewState = new OutlineViewState(); private readonly _editorListener = new MutableDisposable(); @@ -120,7 +121,8 @@ export class OutlinePane extends ViewPane { override dispose(): void { this._disposables.dispose(); - this._editorDisposables.dispose(); + this._editorPaneDisposables.dispose(); + this._editorControlDisposables.dispose(); this._editorListener.dispose(); super.dispose(); } @@ -148,7 +150,8 @@ export class OutlinePane extends ViewPane { if (!visible) { // stop everything when not visible this._editorListener.clear(); - this._editorDisposables.clear(); + this._editorPaneDisposables.clear(); + this._editorControlDisposables.clear(); } else if (!this._editorListener.value) { const event = Event.any(this._editorService.onDidActiveEditorChange, this._outlineService.onDidChange); @@ -189,13 +192,26 @@ export class OutlinePane extends ViewPane { return false; } - private async _handleEditorChanged(pane: IEditorPane | undefined): Promise { + private _handleEditorChanged(pane: IEditorPane | undefined): void { + this._editorPaneDisposables.clear(); + + if (pane) { + // react to control changes from within pane (https://github.com/microsoft/vscode/issues/134008) + this._editorPaneDisposables.add(pane.onDidChangeControl(() => { + this._handleEditorControlChanged(pane); + })); + } + + this._handleEditorControlChanged(pane); + } + + private async _handleEditorControlChanged(pane: IEditorPane | undefined): Promise { // persist state const resource = EditorResourceAccessor.getOriginalUri(pane?.input); const didCapture = this._captureViewState(resource); - this._editorDisposables.clear(); + this._editorControlDisposables.clear(); if (!pane || !this._outlineService.canCreateOutline(pane) || !resource) { return this._showMessage(localize('no-editor', "The active editor cannot provide outline information.")); @@ -211,7 +227,7 @@ export class OutlinePane extends ViewPane { this._progressBar.infinite().show(500); const cts = new CancellationTokenSource(); - this._editorDisposables.add(toDisposable(() => cts.dispose(true))); + this._editorControlDisposables.add(toDisposable(() => cts.dispose(true))); const newOutline = await this._outlineService.createOutline(pane, OutlineTarget.OutlinePane, cts.token); loadingMessage?.dispose(); @@ -225,7 +241,7 @@ export class OutlinePane extends ViewPane { return; } - this._editorDisposables.add(newOutline); + this._editorControlDisposables.add(newOutline); this._progressBar.stop().hide(); const sorter = new OutlineTreeSorter(newOutline.config.comparator, this._outlineViewState.sortBy); @@ -270,21 +286,21 @@ export class OutlinePane extends ViewPane { } }; updateTree(); - this._editorDisposables.add(newOutline.onDidChange(updateTree)); + this._editorControlDisposables.add(newOutline.onDidChange(updateTree)); // feature: apply panel background to tree - this._editorDisposables.add(this.viewDescriptorService.onDidChangeLocation(({ views }) => { + this._editorControlDisposables.add(this.viewDescriptorService.onDidChangeLocation(({ views }) => { if (views.some(v => v.id === this.id)) { tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); } })); // feature: filter on type - keep tree and menu in sync - this._editorDisposables.add(tree.onDidUpdateOptions(e => this._outlineViewState.filterOnType = Boolean(e.filterOnType))); + this._editorControlDisposables.add(tree.onDidUpdateOptions(e => this._outlineViewState.filterOnType = Boolean(e.filterOnType))); // feature: reveal outline selection in editor // on change -> reveal/select defining range - this._editorDisposables.add(tree.onDidOpen(e => newOutline.reveal(e.element, e.editorOptions, e.sideBySide))); + this._editorControlDisposables.add(tree.onDidOpen(e => newOutline.reveal(e.element, e.editorOptions, e.sideBySide))); // feature: reveal editor selection in outline const revealActiveElement = () => { if (!this._outlineViewState.followCursor || !newOutline.activeElement) { @@ -307,10 +323,10 @@ export class OutlinePane extends ViewPane { } }; revealActiveElement(); - this._editorDisposables.add(newOutline.onDidChange(revealActiveElement)); + this._editorControlDisposables.add(newOutline.onDidChange(revealActiveElement)); // feature: update view when user state changes - this._editorDisposables.add(this._outlineViewState.onDidChange((e: { followCursor?: boolean, sortBy?: boolean, filterOnType?: boolean }) => { + this._editorControlDisposables.add(this._outlineViewState.onDidChange((e: { followCursor?: boolean, sortBy?: boolean, filterOnType?: boolean }) => { this._outlineViewState.persist(this._storageService); if (e.filterOnType) { tree.updateOptions({ filterOnType: this._outlineViewState.filterOnType }); @@ -326,7 +342,7 @@ export class OutlinePane extends ViewPane { // feature: expand all nodes when filtering (not when finding) let viewState: IDataTreeViewState | undefined; - this._editorDisposables.add(tree.onDidChangeTypeFilterPattern(pattern => { + this._editorControlDisposables.add(tree.onDidChangeTypeFilterPattern(pattern => { if (!tree.options.filterOnType) { return; } @@ -342,7 +358,7 @@ export class OutlinePane extends ViewPane { // last: set tree property tree.layout(this._treeDimensions?.height, this._treeDimensions?.width); this._tree = tree; - this._editorDisposables.add(toDisposable(() => { + this._editorControlDisposables.add(toDisposable(() => { tree.dispose(); this._tree = undefined; })); @@ -363,7 +379,7 @@ registerAction2(class Collapse extends ViewAction { menu: { id: MenuId.ViewTitle, group: 'navigation', - when: ContextKeyEqualsExpr.create('view', OutlinePane.Id) + when: ContextKeyExpr.equals('view', OutlinePane.Id) } }); } @@ -384,7 +400,7 @@ registerAction2(class FollowCursor extends ViewAction { id: MenuId.ViewTitle, group: 'config', order: 1, - when: ContextKeyEqualsExpr.create('view', OutlinePane.Id) + when: ContextKeyExpr.equals('view', OutlinePane.Id) } }); } @@ -405,7 +421,7 @@ registerAction2(class FilterOnType extends ViewAction { id: MenuId.ViewTitle, group: 'config', order: 2, - when: ContextKeyEqualsExpr.create('view', OutlinePane.Id) + when: ContextKeyExpr.equals('view', OutlinePane.Id) } }); } @@ -427,7 +443,7 @@ registerAction2(class SortByPosition extends ViewAction { id: MenuId.ViewTitle, group: 'sort', order: 1, - when: ContextKeyEqualsExpr.create('view', OutlinePane.Id) + when: ContextKeyExpr.equals('view', OutlinePane.Id) } }); } @@ -448,7 +464,7 @@ registerAction2(class SortByName extends ViewAction { id: MenuId.ViewTitle, group: 'sort', order: 2, - when: ContextKeyEqualsExpr.create('view', OutlinePane.Id) + when: ContextKeyExpr.equals('view', OutlinePane.Id) } }); } @@ -469,7 +485,7 @@ registerAction2(class SortByKind extends ViewAction { id: MenuId.ViewTitle, group: 'sort', order: 3, - when: ContextKeyEqualsExpr.create('view', OutlinePane.Id) + when: ContextKeyExpr.equals('view', OutlinePane.Id) } }); } diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index 5c344c62cc..6d650233e3 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -22,6 +22,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; export class LogViewerInput extends TextResourceEditorInput { @@ -37,7 +38,8 @@ export class LogViewerInput extends TextResourceEditorInput { @ITextFileService textFileService: ITextFileService, @IEditorService editorService: IEditorService, @IFileService fileService: IFileService, - @ILabelService labelService: ILabelService + @ILabelService labelService: ILabelService, + @IEditorResolverService editorResolverService: IEditorResolverService ) { super( URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), @@ -49,7 +51,8 @@ export class LogViewerInput extends TextResourceEditorInput { textFileService, editorService, fileService, - labelService + labelService, + editorResolverService ); } } diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 657d37a1f7..ad04f1d229 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -28,7 +28,7 @@ import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/commo import { IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { assertIsDefined } from 'vs/base/common/types'; -import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { CATEGORIES } from 'vs/workbench/common/actions'; @@ -74,9 +74,9 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews id: 'workbench.action.output.toggleOutput', mnemonicTitle: nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output"), keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_U, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyU, linux: { - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_H) // On Ubuntu Ctrl+Shift+U is taken by some global OS command + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyH) // On Ubuntu Ctrl+Shift+U is taken by some global OS command } }, order: 1, @@ -112,7 +112,7 @@ registerAction2(class extends Action2 { title: nls.localize('switchToOutput.label', "Switch to Output"), menu: { id: MenuId.ViewTitle, - when: ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), group: 'navigation', order: 1 }, @@ -133,7 +133,7 @@ registerAction2(class extends Action2 { category: CATEGORIES.View, menu: [{ id: MenuId.ViewTitle, - when: ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), group: 'navigation', order: 2 }, { @@ -162,7 +162,7 @@ registerAction2(class extends Action2 { tooltip: nls.localize('outputScrollOff', "Turn Auto Scrolling Off"), menu: { id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', OUTPUT_VIEW_ID)), group: 'navigation', order: 3, }, @@ -186,7 +186,7 @@ registerAction2(class extends Action2 { title: { value: nls.localize('openActiveLogOutputFile', "Open Log Output File"), original: 'Open Log Output File' }, menu: [{ id: MenuId.ViewTitle, - when: ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), + when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID), group: 'navigation', order: 4 }, { diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index f2e2d915aa..b30f725719 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -8,7 +8,7 @@ import * as resources from 'vs/base/common/resources'; import { ITextModel } from 'vs/editor/common/model'; import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { RunOnceScheduler, ThrottledDelayer } from 'vs/base/common/async'; +import { Promises, RunOnceScheduler, ThrottledDelayer } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -17,7 +17,7 @@ import { isNumber } from 'vs/base/common/types'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { VSBuffer } from 'vs/base/common/buffer'; -import { ILogger, ILoggerService } from 'vs/platform/log/common/log'; +import { ILogger, ILoggerService, ILogService } from 'vs/platform/log/common/log'; export interface IOutputChannelModel extends IDisposable { readonly onDidAppendedContent: Event; @@ -151,7 +151,8 @@ class OutputFileListener extends Disposable { constructor( private readonly file: URI, - private readonly fileService: IFileService + private readonly fileService: IFileService, + private readonly logService: ILogService ) { super(); this.syncDelayer = new ThrottledDelayer(500); @@ -161,6 +162,7 @@ class OutputFileListener extends Disposable { if (!this.watching) { this.etag = eTag; this.poll(); + this.logService.trace('Started polling', this.file.toString()); this.watching = true; } } @@ -170,20 +172,19 @@ class OutputFileListener extends Disposable { this.syncDelayer.trigger(loop); } - private doWatch(): Promise { - return this.fileService.resolve(this.file, { resolveMetadata: true }) - .then(stat => { - if (stat.etag !== this.etag) { - this.etag = stat.etag; - this._onDidContentChange.fire(stat.size); - } - }); + private async doWatch(): Promise { + const stat = await this.fileService.resolve(this.file, { resolveMetadata: true }); + if (stat.etag !== this.etag) { + this.etag = stat.etag; + this._onDidContentChange.fire(stat.size); + } } unwatch(): void { if (this.watching) { this.syncDelayer.cancel(); this.watching = false; + this.logService.trace('Stopped polling', this.file.toString()); } } @@ -210,17 +211,18 @@ class FileOutputChannelModel extends AbstractFileOutputChannelModel implements I file: URI, @IFileService fileService: IFileService, @IModelService modelService: IModelService, - @IModeService modeService: IModeService + @IModeService modeService: IModeService, + @ILogService logService: ILogService ) { super(modelUri, mimeType, file, fileService, modelService, modeService); - this.fileHandler = this._register(new OutputFileListener(this.file, this.fileService)); + this.fileHandler = this._register(new OutputFileListener(this.file, this.fileService, logService)); this._register(this.fileHandler.onDidContentChange(size => this.update(size))); this._register(toDisposable(() => this.fileHandler.unwatch())); } loadModel(): Promise { - this.loadModelPromise = new Promise(async (c, e) => { + this.loadModelPromise = Promises.withAsyncBody(async (c, e) => { try { let content = ''; if (await this.fileService.exists(this.file)) { diff --git a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index 959d0fdd42..0197c08700 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -24,6 +24,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ByteSize, IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { isWeb } from 'vs/base/common/platform'; +import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; export class PerfviewContrib { @@ -55,7 +56,8 @@ export class PerfviewInput extends TextResourceEditorInput { @ITextFileService textFileService: ITextFileService, @IEditorService editorService: IEditorService, @IFileService fileService: IFileService, - @ILabelService labelService: ILabelService + @ILabelService labelService: ILabelService, + @IEditorResolverService editorResolverService: IEditorResolverService ) { super( PerfviewInput.Uri, @@ -67,7 +69,8 @@ export class PerfviewInput extends TextResourceEditorInput { textFileService, editorService, fileService, - labelService + labelService, + editorResolverService ); } } diff --git a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts index 619297dba6..359babdcb5 100644 --- a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts +++ b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts @@ -15,15 +15,15 @@ import { IUpdateService } from 'vs/platform/update/common/update'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import * as files from 'vs/workbench/contrib/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { didUseCachedData } from 'vs/workbench/services/timer/electron-sandbox/timerService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { VSBuffer } from 'vs/base/common/buffer'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; export class StartupTimings implements IWorkbenchContribution { @@ -32,8 +32,7 @@ export class StartupTimings implements IWorkbenchContribution { @ITimerService private readonly _timerService: ITimerService, @INativeHostService private readonly _nativeHostService: INativeHostService, @IEditorService private readonly _editorService: IEditorService, - @IViewletService private readonly _viewletService: IViewletService, - @IPanelService private readonly _panelService: IPanelService, + @IPaneCompositePartService private readonly _paneCompositeService: IPaneCompositePartService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IUpdateService private readonly _updateService: IUpdateService, @@ -96,7 +95,7 @@ export class StartupTimings implements IWorkbenchContribution { if (windowCount !== 1) { return 'Expected window count : 1, Actual : ' + windowCount; } - const activeViewlet = this._viewletService.getActiveViewlet(); + const activeViewlet = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar); if (!activeViewlet || activeViewlet.getId() !== files.VIEWLET_ID) { return 'Explorer viewlet not visible'; } @@ -107,9 +106,9 @@ export class StartupTimings implements IWorkbenchContribution { if (!isCodeEditor(visibleEditorPanes[0].getControl())) { return 'Active editor is not a text editor'; } - const activePanel = this._panelService.getActivePanel(); + const activePanel = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel); if (activePanel) { - return 'Current active panel : ' + this._panelService.getPanel(activePanel.getId())?.name; + return 'Current active panel : ' + this._paneCompositeService.getPaneComposite(activePanel.getId(), ViewContainerLocation.Panel)?.name; } const noCachedData = this._environmentService.args['no-cached-data']; if (!noCachedData && !didUseCachedData(this._productService, this._storageService, this._environmentService)) { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index cecbad387e..d01b792154 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -10,7 +10,8 @@ import { Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecy import { Event, Emitter } from 'vs/base/common/event'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { Widget } from 'vs/base/browser/ui/widget'; -import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; @@ -24,7 +25,7 @@ import { editorWidgetBackground, editorWidgetForeground, widgetShadow } from 'vs import { ScrollType } from 'vs/editor/common/editorCommon'; import { SearchWidget, SearchOptions } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { timeout } from 'vs/base/common/async'; +import { Promises, timeout } from 'vs/base/common/async'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export interface KeybindingsSearchOptions extends SearchOptions { @@ -54,12 +55,12 @@ export class KeybindingsSearchWidget extends SearchWidget { constructor(parent: HTMLElement, options: KeybindingsSearchOptions, @IContextViewService contextViewService: IContextViewService, - @IKeybindingService private readonly keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, ) { - super(parent, options, contextViewService, instantiationService, themeService, contextKeyService); + super(parent, options, contextViewService, instantiationService, themeService, contextKeyService, keybindingService); this._register(attachInputBoxStyler(this.inputBox, themeService)); this._register(toDisposable(() => this.stopRecordingKeys())); this._firstPart = null; @@ -225,7 +226,7 @@ export class DefineKeybindingWidget extends Widget { define(): Promise { this._keybindingInputWidget.clear(); - return new Promise(async (c) => { + return Promises.withAsyncBody(async (c) => { if (!this._isVisible) { this._isVisible = true; this._domNode.setDisplay('block'); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index e888df69c3..f6d46a3db3 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -25,11 +25,11 @@ import { DefineKeybindingWidget, KeybindingsSearchWidget } from 'vs/workbench/co import { CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE } from 'vs/workbench/contrib/preferences/common/preferences'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; -import { IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { listHighlightForeground, badgeBackground, contrastBorder, badgeForeground, listActiveSelectionForeground, listInactiveSelectionForeground, listHoverForeground, listFocusForeground, editorBackground, foreground, listActiveSelectionBackground, listInactiveSelectionBackground, listFocusBackground, listHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { listHighlightForeground, badgeBackground, contrastBorder, badgeForeground, listActiveSelectionForeground, listInactiveSelectionForeground, listHoverForeground, listFocusForeground, editorBackground, foreground, listActiveSelectionBackground, listInactiveSelectionBackground, listFocusBackground, listHoverBackground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; @@ -41,7 +41,6 @@ import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { Emitter, Event } from 'vs/base/common/event'; import { MenuRegistry, MenuId, isIMenuItem } from 'vs/platform/actions/common/actions'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { Color, RGBA } from 'vs/base/common/color'; import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IKeybindingItemEntry, IKeybindingsEditorPane } from 'vs/workbench/services/preferences/common/preferences'; @@ -51,9 +50,12 @@ import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browse import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -const $ = DOM.$; +type KeybindingEditorActionClassification = { + action: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; + command: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +}; -const evenRowBackgroundColor = new Color(new RGBA(130, 130, 130, 0.04)); +const $ = DOM.$; class ThemableCheckboxActionViewItem extends CheckboxActionViewItem { @@ -186,7 +188,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP try { const key = await this.defineKeybindingWidget.define(); if (key) { - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_DEFINE, keybindingEntry.keybindingItem.command, key); + this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_DEFINE, keybindingEntry.keybindingItem.command); await this.updateKeybinding(keybindingEntry, key, keybindingEntry.keybindingItem.when, add); } } catch (error) { @@ -221,7 +223,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP async removeKeybinding(keybindingEntry: IKeybindingItemEntry): Promise { this.selectEntry(keybindingEntry); if (keybindingEntry.keybindingItem.keybinding) { // This should be a pre-condition - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_REMOVE, keybindingEntry.keybindingItem.command, keybindingEntry.keybindingItem.keybinding); + this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_REMOVE, keybindingEntry.keybindingItem.command); try { await this.keybindingEditingService.removeKeybinding(keybindingEntry.keybindingItem.keybindingItem); this.focus(); @@ -234,7 +236,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP async resetKeybinding(keybindingEntry: IKeybindingItemEntry): Promise { this.selectEntry(keybindingEntry); - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_RESET, keybindingEntry.keybindingItem.command, keybindingEntry.keybindingItem.keybinding); + this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_RESET, keybindingEntry.keybindingItem.command); try { await this.keybindingEditingService.resetKeybinding(keybindingEntry.keybindingItem.keybindingItem); if (!keybindingEntry.keybindingItem.keybinding) { // reveal only if keybinding was added to unassinged. Because the entry will be placed in different position after rendering @@ -249,7 +251,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP async copyKeybinding(keybinding: IKeybindingItemEntry): Promise { this.selectEntry(keybinding); - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY, keybinding.keybindingItem.command, keybinding.keybindingItem.keybinding); + this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY, keybinding.keybindingItem.command); const userFriendlyKeybinding: IUserFriendlyKeybinding = { key: keybinding.keybindingItem.keybinding ? keybinding.keybindingItem.keybinding.getUserSettingsLabel() || '' : '', command: keybinding.keybindingItem.command @@ -262,13 +264,13 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP async copyKeybindingCommand(keybinding: IKeybindingItemEntry): Promise { this.selectEntry(keybinding); - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, keybinding.keybindingItem.command, keybinding.keybindingItem.keybinding); + this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, keybinding.keybindingItem.command); await this.clipboardService.writeText(keybinding.keybindingItem.command); } - private async copyKeybindingCommandTitle(keybinding: IKeybindingItemEntry): Promise { + async copyKeybindingCommandTitle(keybinding: IKeybindingItemEntry): Promise { this.selectEntry(keybinding); - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, keybinding.keybindingItem.command, keybinding.keybindingItem.keybinding); + this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, keybinding.keybindingItem.command); await this.clipboardService.writeText(keybinding.keybindingItem.commandLabel); } @@ -489,9 +491,10 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP )) as WorkbenchTable; this._register(this.keybindingsTable.onContextMenu(e => this.onContextMenu(e))); - this._register(this.keybindingsTable.onDidChangeFocus(e => this.onFocusChange(e))); + this._register(this.keybindingsTable.onDidChangeFocus(e => this.onFocusChange())); this._register(this.keybindingsTable.onDidFocus(() => { this.keybindingsTable.getHTMLElement().classList.add('focused'); + this.onFocusChange(); })); this._register(this.keybindingsTable.onDidBlur(() => { this.keybindingsTable.getHTMLElement().classList.remove('focused'); @@ -624,7 +627,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP private selectEntry(keybindingItemEntry: IKeybindingItemEntry | number, focus: boolean = true): void { const index = typeof keybindingItemEntry === 'number' ? keybindingItemEntry : this.getIndexOf(keybindingItemEntry); - if (index !== -1) { + if (index !== -1 && index < this.keybindingsTable.length) { if (focus) { this.keybindingsTable.domFocus(); this.keybindingsTable.setFocus([index]); @@ -680,9 +683,9 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP } } - private onFocusChange(e: IListEvent): void { + private onFocusChange(): void { this.keybindingFocusContextKey.reset(); - const element = e.elements[0]; + const element = this.keybindingsTable.getFocusedElements()[0]; if (!element) { return; } @@ -798,9 +801,8 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP return this.latestEmptyFilters.filter(filterText => (cumulativeSize += filterText.length) <= 8192); } - private reportKeybindingAction(action: string, command: string, keybinding: ResolvedKeybinding | string): void { - // __GDPR__TODO__ Need to move off dynamic event names and properties as they cannot be registered statically - this.telemetryService.publicLog(action, { command, keybinding: keybinding ? (typeof keybinding === 'string' ? keybinding : keybinding.getUserSettingsLabel()) : '' }); + private reportKeybindingAction(action: string, command: string): void { + this.telemetryService.publicLog2<{ action: string, command: string }, KeybindingEditorActionClassification>('keybindingsEditor.action', { command, action }); } private onKeybindingEditingError(error: any): void { @@ -1163,18 +1165,27 @@ class AccessibilityProvider implements IListAccessibilityProvider { - collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-table-th { background-color: ${evenRowBackgroundColor}; }`); - collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list-row:nth-child(even):not(.focused):not(.selected):not(:hover) .monaco-table-tr { background-color: ${evenRowBackgroundColor}; }`); - collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list:not(:focus) .monaco-list-row:nth-child(even).focused:not(.selected):not(:hover) .monaco-table-tr { background-color: ${evenRowBackgroundColor}; }`); - collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list:not(.focused) .monaco-list-row:nth-child(even).focused:not(.selected):not(:hover) .monaco-table-tr { background-color: ${evenRowBackgroundColor}; }`); + + const tableHeader = theme.getColor(keybindingTableHeader); + if (tableHeader) { + collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-table-th { background-color: ${tableHeader}; }`); + } + + const tableRows = theme.getColor(keybindingTableRows); + if (tableRows) { + collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list-row[data-parity=odd]:not(.focused):not(.selected):not(:hover) .monaco-table-tr { background-color: ${tableRows}; }`); + collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list:not(:focus) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr { background-color: ${tableRows}; }`); + collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list:not(.focused) .monaco-list-row[data-parity=odd].focused:not(.selected):not(:hover) .monaco-table-tr { background-color: ${tableRows}; }`); + } const foregroundColor = theme.getColor(foreground); if (foregroundColor) { const whenForegroundColor = foregroundColor.transparent(.8).makeOpaque(WORKBENCH_BACKGROUND(theme)); collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-table-tr .monaco-table-td .code { color: ${whenForegroundColor}; }`); - const whenForegroundColorForEvenRow = foregroundColor.transparent(.8).makeOpaque(evenRowBackgroundColor); - collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list-row:nth-child(even) .monaco-table-tr .monaco-table-td .code { color: ${whenForegroundColorForEvenRow}; }`); } const listActiveSelectionForegroundColor = theme.getColor(listActiveSelectionForeground); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts index 0f729ed6c9..d2524e9eb5 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts @@ -6,7 +6,8 @@ import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { KeyCode, KeyMod, KeyChord, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; +import { SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings'; import { Disposable } from 'vs/base/common/lifecycle'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,7 +21,6 @@ import { SmartSnippetInserter } from 'vs/workbench/contrib/preferences/common/sm import { DefineKeybindingOverlayWidget } from 'vs/workbench/contrib/preferences/browser/keybindingWidgets'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { parseTree, Node } from 'vs/base/common/json'; -import { ScanCodeBinding } from 'vs/base/common/scanCode'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { WindowsNativeResolvedKeybinding } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper'; import { themeColorFromId, ThemeColor } from 'vs/platform/theme/common/themeService'; @@ -358,7 +358,7 @@ class DefineKeybindingCommand extends EditorCommand { precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.languageId.isEqualTo('jsonc')), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyK), weight: KeybindingWeight.EditorContrib } }); diff --git a/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts b/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts index 03479dae00..5633022242 100644 --- a/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts +++ b/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/browser/statusbar'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { parseKeyboardLayoutDescription, areKeyboardLayoutsEqual, getKeyboardLayoutId, IKeyboardLayoutService, IKeyboardLayoutInfo } from 'vs/platform/keyboardLayout/common/keyboardLayout'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index f428236903..8146026656 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -124,6 +124,11 @@ padding-right: 8px; } +.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget > .monaco-action-bar .action-item .action-label:not(.checked):not(:focus) { + /* Add an extra pixel due to it not getting the outline */ + padding-bottom: 8px; +} + .settings-editor > .settings-body { position: relative; margin-top: 14px; @@ -288,6 +293,9 @@ padding-right: 24px; overflow: visible; } +.settings-editor > .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents.group-title { + max-width: min(100%, 1000px); /* Cut off title if too long for window */ +} .settings-editor > .settings-body > .settings-tree-container .settings-group-title-label, .settings-editor > .settings-body > .settings-tree-container .setting-item-contents { @@ -301,7 +309,6 @@ } .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-title { - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; /* size to contents for hover to show context button */ @@ -374,7 +381,11 @@ } .settings-editor > .settings-body > .settings-tree-container .setting-item-contents.is-deprecated .setting-item-deprecation-message { - display: block; + display: flex; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents.is-deprecated .setting-item-deprecation-message .codicon { + margin-right: 4px; } .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description { @@ -559,15 +570,17 @@ padding-left: 15px; width: 100%; position: relative; + overflow: hidden; + text-overflow: ellipsis; } - -.settings-editor > .settings-body > .settings-tree-container .settings-group-level-1 { - font-size: 24px; +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label.settings-group-level-1 { + font-size: 26px; } - -.settings-editor > .settings-body > .settings-tree-container .settings-group-level-2 { - padding-top: 32px; - font-size: 20px; +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label.settings-group-level-2 { + font-size: 22px; +} +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label.settings-group-level-3 { + font-size: 18px; } .settings-editor.search-mode > .settings-body .settings-toc-container .monaco-list-row .settings-toc-count { diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 603788023d..22ca468abf 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -35,7 +35,7 @@ import { ConfigureLanguageBasedSettingsAction } from 'vs/workbench/contrib/prefe import { SettingsEditorContribution } from 'vs/workbench/contrib/preferences/browser/preferencesEditor'; import { preferencesOpenSettingsIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { SettingsEditor2, SettingsFocusContext } from 'vs/workbench/contrib/preferences/browser/settingsEditor2'; -import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { PreferencesContribution } from 'vs/workbench/contrib/preferences/common/preferencesContribution'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -166,7 +166,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: null, - primary: KeyMod.CtrlCmd | KeyCode.US_COMMA, + primary: KeyMod.CtrlCmd | KeyCode.Comma, }, menu: { id: MenuId.GlobalActivity, @@ -198,8 +198,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon f1: true, }); } - run(accessor: ServicesAccessor) { - return accessor.get(IPreferencesService).openSettings({ jsonEditor: false }); + run(accessor: ServicesAccessor, args: IOpenSettingsActionOptions) { + args = sanitizeOpenSettingsArgs(args); + return accessor.get(IPreferencesService).openSettings({ jsonEditor: false, ...args }); } }); registerAction2(class extends Action2 { @@ -211,8 +212,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon f1: true, }); } - run(accessor: ServicesAccessor) { - return accessor.get(IPreferencesService).openSettings({ jsonEditor: true }); + run(accessor: ServicesAccessor, args: IOpenSettingsActionOptions) { + args = sanitizeOpenSettingsArgs(args); + return accessor.get(IPreferencesService).openSettings({ jsonEditor: true, ...args }); } }); registerAction2(class extends Action2 { @@ -526,7 +528,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } run(accessor: ServicesAccessor, args?: IOpenSettingsActionOptions) { args = sanitizeOpenSettingsArgs(args); - return accessor.get(IPreferencesService).openRemoteSettings(args); + return accessor.get(IPreferencesService).openRemoteSettings({ jsonEditor: true, ...args }); } }); }); @@ -554,7 +556,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon id: SETTINGS_EDITOR_COMMAND_SEARCH, precondition: CONTEXT_SETTINGS_EDITOR, keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + primary: KeyMod.CtrlCmd | KeyCode.KeyF, weight: KeybindingWeight.EditorContrib, when: null }, @@ -775,7 +777,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon keybinding: { when: null, weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_S) + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyS) }, menu: [ { id: MenuId.CommandPalette }, @@ -947,7 +949,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon id: KEYBINDINGS_EDITOR_COMMAND_ADD, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_A), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyA), handler: (accessor, args: any) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { @@ -960,7 +962,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon id: KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_E), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyE), handler: (accessor, args: any) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor && editorPane.activeKeybindingEntry!.keybindingItem.keybinding) { @@ -1002,7 +1004,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon id: KEYBINDINGS_EDITOR_COMMAND_SEARCH, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR), - primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + primary: KeyMod.CtrlCmd | KeyCode.KeyF, handler: (accessor, args: any) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { @@ -1015,8 +1017,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon id: KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS), - primary: KeyMod.Alt | KeyCode.KEY_K, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_K }, + primary: KeyMod.Alt | KeyCode.KeyK, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyK }, handler: (accessor, args: any) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { @@ -1029,8 +1031,8 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon id: KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR), - primary: KeyMod.Alt | KeyCode.KEY_P, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_P }, + primary: KeyMod.Alt | KeyCode.KeyP, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyP }, handler: (accessor, args: any) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { @@ -1056,7 +1058,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon id: KEYBINDINGS_EDITOR_COMMAND_COPY, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), - primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + primary: KeyMod.CtrlCmd | KeyCode.KeyC, handler: async (accessor, args: any) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { @@ -1078,6 +1080,19 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), + primary: 0, + handler: async (accessor, args: any) => { + const editorPane = accessor.get(IEditorService).activeEditorPane; + if (editorPane instanceof KeybindingsEditor) { + await editorPane.copyKeybindingCommandTitle(editorPane.activeKeybindingEntry!); + } + } + }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts index 0a206987f1..234f4884bc 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts @@ -53,9 +53,9 @@ export class ConfigureLanguageBasedSettingsAction extends Action { await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language") }) .then(pick => { if (pick) { - const modeId = this.modeService.getModeIdForLanguageName(pick.label.toLowerCase()); - if (typeof modeId === 'string') { - return this.preferencesService.openUserSettings({ jsonEditor: true, revealSetting: { key: `[${modeId}]`, edit: true } }); + const languageId = this.modeService.getModeIdForLanguageName(pick.label.toLowerCase()); + if (typeof languageId === 'string') { + return this.preferencesService.openUserSettings({ jsonEditor: true, revealSetting: { key: `[${languageId}]`, edit: true } }); } } return undefined; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 150b41759a..b4807f5237 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -22,10 +22,12 @@ import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/brows import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget'; +import { showHistoryKeybindingHint } from 'vs/platform/browser/historyWidgetKeybindingHint'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IContextKey, 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 { ILabelService } from 'vs/platform/label/common/label'; import { activeContrastBorder, badgeBackground, badgeForeground, contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; @@ -243,6 +245,7 @@ export class SettingsTargetsWidget extends Widget { const settingsTabsWidget = DOM.append(parent, DOM.$('.settings-tabs-widget')); this.settingsSwitcherBar = this._register(new ActionBar(settingsTabsWidget, { orientation: ActionsOrientation.HORIZONTAL, + focusOnlyEnabledItems: true, ariaLabel: localize('settingsSwitcherBarAriaLabel', "Settings Switcher"), animated: false, actionViewItemProvider: (action: IAction) => action.id === 'folderSettings' ? this.folderSettings : undefined @@ -370,7 +373,8 @@ export class SearchWidget extends Widget { @IContextViewService private readonly contextViewService: IContextViewService, @IInstantiationService protected instantiationService: IInstantiationService, @IThemeService private readonly themeService: IThemeService, - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IKeybindingService protected readonly keybindingService: IKeybindingService ) { super(); this.create(parent); @@ -420,7 +424,8 @@ export class SearchWidget extends Widget { } protected createInputBox(parent: HTMLElement): HistoryInputBox { - const box = this._register(new ContextScopedHistoryInputBox(parent, this.contextViewService, this.options, this.contextKeyService)); + const showHistoryHint = () => showHistoryKeybindingHint(this.keybindingService); + const box = this._register(new ContextScopedHistoryInputBox(parent, this.contextViewService, { ...this.options, showHistoryHint }, this.contextKeyService)); this._register(attachInputBoxStyler(box, this.themeService)); return box; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index c2aa27748d..ed67061a34 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -55,6 +55,7 @@ import { preferencesClearInputIcon } from 'vs/workbench/contrib/preferences/brow import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export const enum SettingsFocusContext { Search, @@ -200,7 +201,8 @@ export class SettingsEditor2 extends EditorPane { @IEditorGroupsService protected editorGroupService: IEditorGroupsService, @IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService, @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IExtensionService private readonly extensionService: IExtensionService ) { super(SettingsEditor2.ID, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); @@ -240,7 +242,7 @@ export class SettingsEditor2 extends EditorPane { })); this._register(configurationService.onDidChangeRestrictedSettings(e => { - if (e.default.length) { + if (e.default.length && this.currentSettingsModel) { this.updateElementsByKey([...e.default]); } })); @@ -308,7 +310,7 @@ export class SettingsEditor2 extends EditorPane { this.modelDisposables.clear(); this.modelDisposables.add(model.onDidChangeGroups(() => { this.updatedConfigSchemaDelayer.trigger(() => { - this.onConfigUpdate(undefined, undefined, true); + this.onConfigUpdate(undefined, false, true); }); })); this.defaultSettingsEditorModel = model; @@ -670,7 +672,7 @@ export class SettingsEditor2 extends EditorPane { private addCtrlAInterceptor(container: HTMLElement): void { this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => { if ( - e.keyCode === KeyCode.KEY_A && + e.keyCode === KeyCode.KeyA && (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'INPUT' @@ -1030,7 +1032,7 @@ export class SettingsEditor2 extends EditorPane { const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core, this.logService); resolvedSettingsRoot.children!.unshift(commonlyUsed.tree); - resolvedSettingsRoot.children!.push(resolveExtensionsSettings(dividedGroups.extension || [])); + resolvedSettingsRoot.children!.push(await resolveExtensionsSettings(this.extensionService, dividedGroups.extension || [])); if (!this.workspaceTrustManagementService.isWorkspaceTrusted() && (this.viewState.settingsTarget instanceof URI || this.viewState.settingsTarget === ConfigurationTarget.WORKSPACE)) { const configuredUntrustedWorkspaceSettings = resolveConfiguredUntrustedSettings(groups, this.viewState.settingsTarget, this.configurationService); @@ -1375,6 +1377,7 @@ export class SettingsEditor2 extends EditorPane { this.tocTree.expandAll(); } + this.settingsTree.scrollTop = 0; this.refreshTOCTree(); this.renderTree(undefined, true); return result; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 97e6442f61..f549983517 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -7,6 +7,7 @@ import { localize } from 'vs/nls'; export interface ITOCEntry { id: string; label: string; + order?: number; children?: ITOCEntry[]; settings?: Array; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index c45d387ea3..b07e1fe0ea 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -60,6 +60,7 @@ import { settingsMoreActionIcon } from 'vs/workbench/contrib/preferences/browser import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const $ = DOM.$; @@ -358,27 +359,69 @@ export function resolveConfiguredUntrustedSettings(groups: ISettingsGroup[], tar return [...allSettings].filter(setting => setting.restricted && inspectSetting(setting.key, target, configurationService).isConfigured); } -export function resolveExtensionsSettings(groups: ISettingsGroup[]): ITOCEntry { - const settingsGroupToEntry = (group: ISettingsGroup) => { +export async function resolveExtensionsSettings(extensionService: IExtensionService, groups: ISettingsGroup[]): Promise> { + const extGroupTree = new Map>(); + const addEntryToTree = (extensionId: string, extensionName: string, childEntry: ITOCEntry) => { + if (!extGroupTree.has(extensionId)) { + const rootEntry = { + id: extensionId, + label: extensionName, + children: [] + }; + extGroupTree.set(extensionId, rootEntry); + } + extGroupTree.get(extensionId)!.children!.push(childEntry); + }; + const processGroupEntry = async (group: ISettingsGroup) => { const flatSettings = arrays.flatten( group.sections.map(section => section.settings)); - return { + const extensionId = group.extensionInfo!.id; + const extension = await extensionService.getExtension(extensionId); + const extensionName = extension!.displayName ?? extension!.name; + + const childEntry = { id: group.id, label: group.title, + order: group.order, settings: flatSettings }; + addEntryToTree(extensionId, extensionName, childEntry); }; - const extGroups = groups + const processPromises = groups .sort((a, b) => a.title.localeCompare(b.title)) - .map(g => settingsGroupToEntry(g)); + .map(g => processGroupEntry(g)); - return { - id: 'extensions', - label: localize('extensions', "Extensions"), - children: extGroups - }; + return Promise.all(processPromises).then(() => { + const extGroups: ITOCEntry[] = []; + for (const value of extGroupTree.values()) { + if (value.children!.length === 1) { + // push a flattened setting + extGroups.push({ + id: value.id, + label: value.children![0].label, + settings: value.children![0].settings + }); + } else { + value.children!.sort((a, b) => { + if (a.order !== undefined && b.order !== undefined) { + return a.order - b.order; + } else { + // leave things as-is + return 0; + } + }); + extGroups.push(value); + } + } + + return { + id: 'extensions', + label: localize('extensions', "Extensions"), + children: extGroups + }; + }); } function _resolveSettingsTree(tocData: ITOCEntry, allSettings: Set, logService: ILogService): ITOCEntry { @@ -776,7 +819,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.descriptionElement.innerText = ''; if (element.setting.descriptionIsMarkdown) { const disposables = new DisposableStore(); - template.toDispose.add(disposables); + template.elementDisposables.add(disposables); const renderedDescription = this.renderSettingMarkdown(element, template.containerElement, element.description, disposables); template.descriptionElement.appendChild(renderedDescription); } else { @@ -825,6 +868,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } else { template.deprecationWarningElement.innerText = deprecationText; } + template.deprecationWarningElement.prepend($('.codicon.codicon-error')); template.containerElement.classList.toggle('is-deprecated', !!deprecationText); this.renderValue(element, template, onChange); @@ -1353,13 +1397,11 @@ export class SettingExcludeRenderer extends AbstractSettingRenderer implements I const newValue = { ...template.context.scopeValue }; // first delete the existing entry, if present - if (e.originalItem.value.data) { - if (e.originalItem.value.data.toString() in template.context.defaultValue) { - // delete a default by overriding it - newValue[e.originalItem.value.data.toString()] = false; - } else { - delete newValue[e.originalItem.value.data.toString()]; - } + if (e.originalItem.value.data.toString() in template.context.defaultValue) { + // delete a default by overriding it + newValue[e.originalItem.value.data.toString()] = false; + } else { + delete newValue[e.originalItem.value.data.toString()]; } // then add the new or updated entry, if present @@ -1452,6 +1494,7 @@ abstract class AbstractSettingTextRenderer extends AbstractSettingRenderer imple protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void): void { template.onChange = undefined; template.inputBox.value = dataElement.value; + template.inputBox.setAriaLabel(dataElement.setting.key); template.onChange = value => { if (!renderValidations(dataElement, template, false)) { onChange(value); @@ -1569,17 +1612,17 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const disposables = new DisposableStore(); template.toDispose.add(disposables); - const defaultOrEmptyString = dataElement.defaultValue ?? ''; - let createdDefault = false; - if (!settingEnum.includes(defaultOrEmptyString)) { + if (!settingEnum.includes(dataElement.defaultValue)) { // Add a new potentially blank default setting - settingEnum.unshift(defaultOrEmptyString); + settingEnum.unshift(dataElement.defaultValue); enumDescriptions.unshift(''); enumItemLabels.unshift(''); createdDefault = true; } + // Use String constructor in case of null or undefined values + const stringifiedDefaultValue = escapeInvisibleChars(String(dataElement.defaultValue)); const displayOptions = settingEnum .map(String) .map(escapeInvisibleChars) @@ -1596,15 +1639,16 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre }, disposables: disposables }, - decoratorRight: (data === dataElement.defaultValue || createdDefault && index === 0 ? localize('settings.Default', "default") : '') + decoratorRight: (((data === stringifiedDefaultValue) || (createdDefault && index === 0)) ? localize('settings.Default', "default") : '') }; }); template.selectBox.setOptions(displayOptions); + template.selectBox.setAriaLabel(dataElement.setting.key); let idx = settingEnum.indexOf(dataElement.value); if (idx === -1) { - idx = settingEnum.indexOf(defaultOrEmptyString); + idx = 0; } template.onChange = undefined; @@ -1669,6 +1713,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT template.onChange = undefined; template.inputBox.value = dataElement.value; + template.inputBox.setAriaLabel(dataElement.setting.key); template.onChange = value => { if (!renderValidations(dataElement, template, false)) { onChange(nullNumParseFn(value)); @@ -1764,6 +1809,7 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingBoolItemTemplate, onChange: (value: boolean) => void): void { template.onChange = undefined; template.checkbox.checked = dataElement.value; + template.checkbox.setTitle(dataElement.setting.key); template.onChange = onChange; } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index e08e6592bf..629855da81 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -429,9 +429,9 @@ export class SettingsTreeModel { } private createSettingsTreeGroupElement(tocEntry: ITOCEntry, parent?: SettingsTreeGroupElement): SettingsTreeGroupElement { - const depth = parent ? this.getDepth(parent) + 1 : 0; const element = new SettingsTreeGroupElement(tocEntry.id, undefined, tocEntry.label, depth, false); + element.parent = parent; const children: SettingsTreeGroupChild[] = []; if (tocEntry.settings) { @@ -596,14 +596,18 @@ function isObjectSetting({ return false; } - // object additional properties allow it to have any shape - if (objectAdditionalProperties === true || objectAdditionalProperties === undefined) { + // objectAdditionalProperties allow the setting to have any shape, + // but if there's a pattern property that handles everything, then every + // property will match that patternProperty, so we don't need to look at + // the value of objectAdditionalProperties in that case. + if ((objectAdditionalProperties === true || objectAdditionalProperties === undefined) + && !Object.keys(objectPatternProperties ?? {}).includes('.*')) { return false; } const schemas = [...Object.values(objectProperties ?? {}), ...Object.values(objectPatternProperties ?? {})]; - if (typeof objectAdditionalProperties === 'object') { + if (objectAdditionalProperties && typeof objectAdditionalProperties === 'object') { schemas.push(objectAdditionalProperties); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index c5eb31a03f..0ef2bb15cc 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -1368,11 +1368,12 @@ export class ObjectSettingCheckboxWidget extends AbstractListSettingWidget void ) { const checkbox = new Checkbox({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: value, - title: '' + title: checkboxDescription }); this.listDisposables.add(checkbox); diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index e5a4924e0f..c7ea9125f9 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -19,10 +19,10 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IEditorInputWithOptions } from 'vs/workbench/common/editor'; +import { EditorInputWithOptions } from 'vs/workbench/common/editor'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { RegisteredEditorPriority, IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IPreferencesService, USE_SPLIT_JSON_SETTING } from 'vs/workbench/services/preferences/common/preferences'; const schemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -40,7 +40,7 @@ export class PreferencesContribution implements IWorkbenchContribution { @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - @IEditorService private readonly editorService: IEditorService, + @ITextEditorService private readonly textEditorService: ITextEditorService ) { this.settingsListener = this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(USE_SPLIT_JSON_SETTING) || e.affectsConfiguration(DEFAULT_SETTINGS_EDITOR_SETTING)) { @@ -69,7 +69,7 @@ export class PreferencesContribution implements IWorkbenchContribution { { canHandleDiff: false, }, - ({ resource, options }): IEditorInputWithOptions => { + ({ resource, options }): EditorInputWithOptions => { // Global User Settings File if (isEqual(resource, this.environmentService.settingsResource)) { return { editor: this.preferencesService.createSplitJsonEditorInput(ConfigurationTarget.USER_LOCAL, resource), options }; @@ -94,7 +94,7 @@ export class PreferencesContribution implements IWorkbenchContribution { } } - return { editor: this.editorService.createEditorInput({ resource }), options }; + return { editor: this.textEditorService.createTextEditor({ resource }), options }; } ); } diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 3b4e37279b..10963d5735 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -164,7 +164,7 @@ export class ShowAllCommandsAction extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: undefined, - primary: !isFirefox ? (KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_P) : undefined, + primary: !isFirefox ? (KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyP) : undefined, secondary: [KeyCode.F1] } }); diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts index 89e2d3a5a8..50d2b1e8ca 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -57,6 +57,15 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { order: 1 }); +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: ShowAllCommandsAction.ID, + title: localize({ key: 'miShowAllCommands', comment: ['&& denotes a mnemonic'] }, "Show All Commands") + }, + order: 2 +}); + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { group: '1_open', command: { @@ -66,15 +75,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { order: 2 }); -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '4_symbol_nav', - command: { - id: 'workbench.action.gotoSymbol', - title: localize({ key: 'miGotoSymbolInEditor', comment: ['&& denotes a mnemonic'] }, "Go to &&Symbol in Editor...") - }, - order: 1 -}); - MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { group: '5_infile_nav', command: { diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index d03de44c60..7e7a1f5f9e 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -6,13 +6,11 @@ import { localize } from 'vs/nls'; import { IQuickPickSeparator, IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IPickerQuickAccessItem, PickerQuickAccessProvider } from 'vs/platform/quickinput/browser/pickerQuickAccess'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IViewDescriptorService, IViewsService, ViewContainer } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewsService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IPanelService, IPanelIdentifier } from 'vs/workbench/services/panel/common/panelService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; +import { PaneCompositeDescriptor } from 'vs/workbench/browser/panecomposite'; import { matchesFuzzy } from 'vs/base/common/filters'; import { fuzzyContains } from 'vs/base/common/strings'; import { withNullAsUndefined } from 'vs/base/common/types'; @@ -22,6 +20,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CATEGORIES } from 'vs/workbench/common/actions'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; interface IViewQuickPickItem extends IPickerQuickAccessItem { containerLabel: string; @@ -32,13 +31,12 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider { const viewEntries: Array = []; - const getViewEntriesForViewlet = (viewlet: ViewletDescriptor, viewContainer: ViewContainer): IViewQuickPickItem[] => { + const getViewEntriesForPaneComposite = (paneComposite: PaneCompositeDescriptor, viewContainer: ViewContainer): IViewQuickPickItem[] => { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); const result: IViewQuickPickItem[] = []; for (const view of viewContainerModel.allViewDescriptors) { if (this.contextKeyService.contextMatchesRules(view.when)) { result.push({ label: view.name, - containerLabel: viewlet.name, + containerLabel: viewContainerModel.title, accept: () => this.viewsService.openView(view.id, true) }); } @@ -115,37 +113,39 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider this.viewletService.openViewlet(viewlet.id, true) - }); + const addPaneComposites = (location: ViewContainerLocation, containerLabel: string) => { + const paneComposites = this.paneCompositeService.getPaneComposites(location); + for (const paneComposite of paneComposites) { + if (this.includeViewContainer(paneComposite)) { + const viewContainer = this.viewDescriptorService.getViewContainerById(paneComposite.id); + if (viewContainer) { + viewEntries.push({ + label: this.viewDescriptorService.getViewContainerModel(viewContainer).title, + containerLabel, + accept: () => this.paneCompositeService.openPaneComposite(paneComposite.id, location, true) + }); + } + } } - } + }; - // Panels - const panels = this.panelService.getPanels(); - for (const panel of panels) { - if (this.includeViewContainer(panel)) { - viewEntries.push({ - label: panel.name, - containerLabel: localize('panels', "Panel"), - accept: () => this.panelService.openPanel(panel.id, true) - }); - } - } + // Viewlets / Panels + addPaneComposites(ViewContainerLocation.Sidebar, localize('views', "Side Bar")); + addPaneComposites(ViewContainerLocation.Panel, localize('panels', "Panel")); - // Viewlet Views - for (const viewlet of viewlets) { - const viewContainer = this.viewDescriptorService.getViewContainerById(viewlet.id); - if (viewContainer) { - viewEntries.push(...getViewEntriesForViewlet(viewlet, viewContainer)); + const addPaneCompositeViews = (location: ViewContainerLocation) => { + const paneComposites = this.paneCompositeService.getPaneComposites(location); + for (const paneComposite of paneComposites) { + const viewContainer = this.viewDescriptorService.getViewContainerById(paneComposite.id); + if (viewContainer) { + viewEntries.push(...getViewEntriesForPaneComposite(paneComposite, viewContainer)); + } } - } + }; + + // Side Bar / Panel Views + addPaneCompositeViews(ViewContainerLocation.Sidebar); + addPaneCompositeViews(ViewContainerLocation.Panel); // Terminals this.terminalGroupService.groups.forEach((group, groupIndex) => { @@ -176,7 +176,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider 0; @@ -211,8 +211,8 @@ export class QuickAccessViewPickerAction extends Action2 { static readonly ID = 'workbench.action.quickOpenView'; static readonly KEYBINDING = { - primary: KeyMod.CtrlCmd | KeyCode.KEY_Q, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_Q }, + primary: KeyMod.CtrlCmd | KeyCode.KeyQ, + mac: { primary: KeyMod.WinCtrl | KeyCode.KeyQ }, linux: { primary: 0 } }; diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index fec403c418..f8c7b9269d 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -22,10 +22,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IProductService } from 'vs/platform/product/common/productService'; interface IConfiguration extends IWindowsConfiguration { - update: { mode: string; }; - debug: { console: { wordWrap: boolean } }; - editor: { accessibilitySupport: 'on' | 'off' | 'auto' }; - security: { workspace: { trust: { enabled: boolean } } } + update?: { mode?: string; }; + debug?: { console?: { wordWrap?: boolean } }; + editor?: { accessibilitySupport?: 'on' | 'off' | 'auto' }; + security?: { workspace?: { trust?: { enabled?: boolean } } }; + files?: { legacyWatcher?: string, experimentalSandboxedFileService?: boolean }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { @@ -37,6 +38,8 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private updateMode: string | undefined; private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; private workspaceTrustEnabled: boolean | undefined; + private legacyFileWatcher: string | undefined = undefined; + private experimentalSandboxedFileService: boolean | undefined = undefined; constructor( @IHostService private readonly hostService: IHostService, @@ -94,10 +97,22 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo } // Workspace trust - if (typeof config.security?.workspace.trust.enabled === 'boolean' && config.security?.workspace.trust.enabled !== this.workspaceTrustEnabled) { + if (typeof config?.security?.workspace?.trust?.enabled === 'boolean' && config.security?.workspace.trust.enabled !== this.workspaceTrustEnabled) { this.workspaceTrustEnabled = config.security.workspace.trust.enabled; changed = true; } + + // Legacy File Watcher + if (typeof config.files?.legacyWatcher === 'string' && config.files.legacyWatcher !== this.legacyFileWatcher) { + this.legacyFileWatcher = config.files.legacyWatcher; + changed = true; + } + + // Experimental Sandboxed File Service + if (typeof config.files?.experimentalSandboxedFileService === 'boolean' && config.files.experimentalSandboxedFileService !== this.experimentalSandboxedFileService) { + this.experimentalSandboxedFileService = config.files.experimentalSandboxedFileService; + changed = true; + } } // Notify only when changed and we are the focused window (avoids notification spam across windows) diff --git a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts index 9c18f20b92..765575ff3c 100644 --- a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts +++ b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts @@ -14,7 +14,7 @@ import { IViewDescriptor } from 'vs/workbench/common/views'; import { isStringArray } from 'vs/base/common/types'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Action2, MenuId } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -109,7 +109,7 @@ export class SwitchRemoteAction extends Action2 { title: SwitchRemoteAction.LABEL, menu: [{ id: MenuId.ViewContainerTitle, - when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + when: ContextKeyExpr.equals('viewContainer', VIEWLET_ID), group: 'navigation', order: 1 }], diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 9c21f59bdc..9dcdb22e00 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -265,25 +265,27 @@ class HelpItemValue { constructor(private commandService: ICommandService, public extensionDescription: IExtensionDescription, public remoteAuthority: string[] | undefined, private urlOrCommand?: string) { } get url(): Promise { - return new Promise(async (resolve) => { - if (this._url === undefined) { - if (this.urlOrCommand) { - let url = URI.parse(this.urlOrCommand); - if (url.authority) { - this._url = this.urlOrCommand; - } else { - const urlCommand: Promise = this.commandService.executeCommand(this.urlOrCommand); - // We must be defensive. The command may never return, meaning that no help at all is ever shown! - const emptyString: Promise = new Promise(resolve => setTimeout(() => resolve(''), 500)); - this._url = await Promise.race([urlCommand, emptyString]); - } + return this.getUrl(); + } + + private async getUrl(): Promise { + if (this._url === undefined) { + if (this.urlOrCommand) { + let url = URI.parse(this.urlOrCommand); + if (url.authority) { + this._url = this.urlOrCommand; + } else { + const urlCommand: Promise = this.commandService.executeCommand(this.urlOrCommand); + // We must be defensive. The command may never return, meaning that no help at all is ever shown! + const emptyString: Promise = new Promise(resolve => setTimeout(() => resolve(''), 500)); + this._url = await Promise.race([urlCommand, emptyString]); } } - if (this._url === undefined) { - this._url = ''; - } - resolve(this._url); - }); + } + if (this._url === undefined) { + this._url = ''; + } + return this._url; } } diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 72e57d5ca3..473e3b3b59 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -11,7 +11,7 @@ import { forwardedPortsViewEnabled, ForwardPortAction, OpenPortInBrowserAction, import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; import { UrlFinder } from 'vs/workbench/contrib/remote/browser/urlFinder'; import Severity from 'vs/base/common/severity'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -43,6 +43,7 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu @IContextKeyService private readonly contextKeyService: IContextKeyService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, + @ITunnelService private readonly tunnelService: ITunnelService, @IActivityService private readonly activityService: IActivityService, @IStatusbarService private readonly statusbarService: IStatusbarService, ) { @@ -76,7 +77,7 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu if (this.environmentService.remoteAuthority && viewEnabled) { const viewContainer = await this.getViewContainer(); - const tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService), this.environmentService); + const tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService, this.tunnelService), this.environmentService); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); if (viewContainer) { this.remoteExplorerService.enablePortsFeatures(); @@ -241,7 +242,8 @@ class OnAutoForwardedAction extends Disposable { const tunnel = await this.portNumberHeuristicDelay(); this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) Heuristic chose ${tunnel?.tunnelRemotePort}`); if (tunnel) { - const attributes = (await this.remoteExplorerService.tunnelModel.getAttributes([tunnel.tunnelRemotePort]))?.get(tunnel.tunnelRemotePort)?.onAutoForward; + const allAttributes = await this.remoteExplorerService.tunnelModel.getAttributes([{ port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost }]); + const attributes = allAttributes?.get(tunnel.tunnelRemotePort)?.onAutoForward; this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) onAutoForward action is ${attributes}`); switch (attributes) { case OnPortForward.OpenBrowserOnce: { @@ -459,7 +461,7 @@ class OutputAutomaticPortForwarding extends Disposable { if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, localUrl.host, localUrl.port)) { return; } - const attributes = (await this.remoteExplorerService.tunnelModel.getAttributes([localUrl.port]))?.get(localUrl.port); + const attributes = (await this.remoteExplorerService.tunnelModel.getAttributes([localUrl]))?.get(localUrl.port); if (attributes?.onAutoForward === OnPortForward.Ignore) { return; } @@ -587,7 +589,7 @@ class ProcAutomaticPortForwarding extends Disposable { } if (!attributes) { - attributes = await this.remoteExplorerService.tunnelModel.getAttributes(this.remoteExplorerService.tunnelModel.candidates.map(candidate => candidate.port)); + attributes = await this.remoteExplorerService.tunnelModel.getAttributes(this.remoteExplorerService.tunnelModel.candidates); } const portAttributes = attributes?.get(value.port); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIcons.ts b/src/vs/workbench/contrib/remote/browser/remoteIcons.ts index ec64928607..ea7c972d93 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIcons.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIcons.ts @@ -18,7 +18,6 @@ export const remoteExplorerViewIcon = registerIcon('remote-explorer-view-icon', export const portsViewIcon = registerIcon('ports-view-icon', Codicon.plug, nls.localize('portsViewIcon', 'View icon of the remote ports view.')); export const portIcon = registerIcon('ports-view-icon', Codicon.plug, nls.localize('portIcon', 'Icon representing a remote port.')); export const privatePortIcon = registerIcon('private-ports-view-icon', Codicon.lock, nls.localize('privatePortIcon', 'Icon representing a private remote port.')); -export const publicPortIcon = registerIcon('public-ports-view-icon', Codicon.eye, nls.localize('publicPortIcon', 'Icon representing a public remote port.')); export const forwardPortIcon = registerIcon('ports-forward-icon', Codicon.plus, nls.localize('forwardPortIcon', 'Icon for the forward action.')); export const stopForwardIcon = registerIcon('ports-stop-forward-icon', Codicon.x, nls.localize('stopForwardIcon', 'Icon for the stop forwarding action.')); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 7f08396b82..2073318388 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -10,7 +10,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { Disposable, dispose } from 'vs/base/common/lifecycle'; import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar'; import { ILabelService } from 'vs/platform/label/common/label'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -30,11 +30,12 @@ import { getCodiconAriaLabel } from 'vs/base/common/codicons'; import { ILogService } from 'vs/platform/log/common/log'; import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IExtensionsViewPaneContainer, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { RemoteNameContext, VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; type ActionGroup = [string, Array]; @@ -150,8 +151,8 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr }); } run = (accessor: ServicesAccessor, input: string) => { - const viewletService = accessor.get(IViewletService); - return viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { + const paneCompositeService = accessor.get(IPaneCompositePartService); + return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true).then(viewlet => { if (viewlet) { (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(`tag:"remote-menu"`); viewlet.focus(); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index cd9aff137c..9b58984dfc 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -23,7 +23,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IMenuService, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy, TunnelEditId, mapHasAddressLocalhostOrAllInterfaces, Attributes, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelEditId, mapHasAddressLocalhostOrAllInterfaces, Attributes, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -34,12 +34,12 @@ import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platfor import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { URI } from 'vs/base/common/uri'; -import { isPortPrivileged, ITunnelService, RemoteTunnel, TunnelProtocol } from 'vs/platform/remote/common/tunnel'; +import { isAllInterfaces, isLocalhost, isPortPrivileged, ITunnelService, RemoteTunnel, TunnelPrivacy, TunnelPrivacyId, TunnelProtocol } from 'vs/platform/remote/common/tunnel'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { copyAddressIcon, forwardedPortWithoutProcessIcon, forwardedPortWithProcessIcon, forwardPortIcon, labelPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, privatePortIcon, publicPortIcon, stopForwardIcon } from 'vs/workbench/contrib/remote/browser/remoteIcons'; +import { copyAddressIcon, forwardedPortWithoutProcessIcon, forwardedPortWithProcessIcon, forwardPortIcon, labelPortIcon, openBrowserIcon, openPreviewIcon, portsViewIcon, privatePortIcon, stopForwardIcon } from 'vs/workbench/contrib/remote/browser/remoteIcons'; import { IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; @@ -51,6 +51,7 @@ import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { STATUS_BAR_HOST_NAME_BACKGROUND } from 'vs/workbench/common/theme'; +import { Codicon } from 'vs/base/common/codicons'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false, nls.localize('tunnel.forwardedPortsViewEnabled', "Whether the Ports view is enabled.")); @@ -93,11 +94,17 @@ export class TunnelViewModel implements ITunnelViewModel { originTooltip: '', privacyTooltip: '', source: { source: TunnelSource.User, description: '' }, - protocol: TunnelProtocol.Http + protocol: TunnelProtocol.Http, + privacy: { + id: TunnelPrivacyId.Private, + themeIcon: privatePortIcon.id, + label: nls.localize('tunnelPrivacy.private', "Private") + } }; constructor( - @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, + @ITunnelService private readonly tunnelService: ITunnelService ) { this.model = remoteExplorerService.tunnelModel; this.onForwardedPortsChanged = Event.any(this.model.onForwardPort, this.model.onClosePort, this.model.onPortName, this.model.onCandidatesChanged); @@ -129,7 +136,7 @@ export class TunnelViewModel implements ITunnelViewModel { private get forwarded(): TunnelItem[] { const forwarded = Array.from(this.model.forwarded.values()).map(tunnel => { - const tunnelItem = TunnelItem.createFromTunnel(this.remoteExplorerService, tunnel); + const tunnelItem = TunnelItem.createFromTunnel(this.remoteExplorerService, this.tunnelService, tunnel); this.addProcessInfoFromCandidate(tunnelItem); return tunnelItem; }).sort((a: TunnelItem, b: TunnelItem) => { @@ -144,7 +151,7 @@ export class TunnelViewModel implements ITunnelViewModel { private get detected(): TunnelItem[] { return Array.from(this.model.detected.values()).map(tunnel => { - const tunnelItem = TunnelItem.createFromTunnel(this.remoteExplorerService, tunnel, TunnelType.Detected, false); + const tunnelItem = TunnelItem.createFromTunnel(this.remoteExplorerService, this.tunnelService, tunnel, TunnelType.Detected, false); this.addProcessInfoFromCandidate(tunnelItem); return tunnelItem; }); @@ -288,7 +295,7 @@ class OriginColumn implements ITableColumn { } class PrivacyColumn implements ITableColumn { - readonly label: string = nls.localize('tunnel.privacyColumn.label', "Privacy"); + readonly label: string = nls.localize('tunnel.privacyColumn.label', "Visibility"); readonly tooltip: string = nls.localize('tunnel.privacyColumn.tooltip', "The availability of the forwarded port."); readonly weight: number = 1; readonly templateId: string = 'actionbar'; @@ -297,12 +304,12 @@ class PrivacyColumn implements ITableColumn { return emptyCell(row); } - const label = row.privacy === TunnelPrivacy.Public ? nls.localize('tunnel.privacyPublic', "Public") : nls.localize('tunnel.privacyPrivate', "Private"); + const label = row.privacy?.label; let tooltip: string = ''; if (row instanceof TunnelItem) { - tooltip = `${row.privacyTooltip} ${row.tooltipPostfix}`; + tooltip = `${row.privacy.label} ${row.tooltipPostfix}`; } - return { label, tunnel: row, icon: row.icon, editId: TunnelEditId.None, tooltip }; + return { label, tunnel: row, icon: { id: row.privacy.themeIcon }, editId: TunnelEditId.None, tooltip }; } } @@ -353,9 +360,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer { - return this.hoverService.showHover(options); - }, + showHover: (options: IHoverDelegateOptions) => this.hoverService.showHover(options), delay: this.configurationService.getValue('workbench.hover.delay') } }); @@ -423,7 +428,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer element.id === this._privacy) ?? + { + id: '', + themeIcon: Codicon.question.id, + label: nls.localize('tunnelPrivacy.unknown', "Unknown") + }; + } else { + return { + id: TunnelPrivacyId.Private, + themeIcon: privatePortIcon.id, + label: nls.localize('tunnelPrivacy.private', "Private") + }; + } } } export const TunnelTypeContextKey = new RawContextKey('tunnelType', TunnelType.Add, true); export const TunnelCloseableContextKey = new RawContextKey('tunnelCloseable', false, true); -const TunnelPrivacyContextKey = new RawContextKey('tunnelPrivacy', undefined, true); +const TunnelPrivacyContextKey = new RawContextKey('tunnelPrivacy', undefined, true); +const TunnelPrivacyEnabledContextKey = new RawContextKey('tunnelPrivacyEnabled', false, true); const TunnelProtocolContextKey = new RawContextKey('tunnelProtocol', TunnelProtocol.Http, true); const TunnelViewFocusContextKey = new RawContextKey('tunnelViewFocus', false, nls.localize('tunnel.focusContext', "Whether the Ports view has focus.")); const TunnelViewSelectionKeyName = 'tunnelViewSelection'; @@ -684,7 +696,8 @@ export class TunnelPanel extends ViewPane { private table!: WorkbenchTable; private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; - private tunnelPrivacyContext: IContextKey; + private tunnelPrivacyContext: IContextKey; + private tunnelPrivacyEnabledContext: IContextKey; private tunnelProtocolContext: IContextKey; private tunnelViewFocusContext: IContextKey; private tunnelViewSelectionContext: IContextKey; @@ -719,6 +732,8 @@ export class TunnelPanel extends ViewPane { this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService); this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService); this.tunnelPrivacyContext = TunnelPrivacyContextKey.bindTo(contextKeyService); + this.tunnelPrivacyEnabledContext = TunnelPrivacyEnabledContextKey.bindTo(contextKeyService); + this.tunnelPrivacyEnabledContext.set(tunnelService.privacyOptions.length !== 0); this.tunnelProtocolContext = TunnelProtocolContextKey.bindTo(contextKeyService); this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService); this.tunnelViewSelectionContext = TunnelViewSelectionContextKey.bindTo(contextKeyService); @@ -739,6 +754,23 @@ export class TunnelPanel extends ViewPane { this._register(toDisposable(() => { this.titleActions = []; })); + + this.registerPrivacyActions(); + } + + private registerPrivacyActions() { + for (const privacyOption of this.tunnelService.privacyOptions) { + const optionId = `remote.tunnel.privacy${privacyOption.id}`; + CommandsRegistry.registerCommand(optionId, ChangeTunnelPrivacyAction.handler(privacyOption.id)); + MenuRegistry.appendMenuItem(MenuId.TunnelPrivacy, ({ + order: 0, + command: { + id: optionId, + title: privacyOption.label, + toggled: TunnelPrivacyContextKey.isEqualTo(privacyOption.id) + } + })); + } } get portCount(): number { @@ -757,7 +789,7 @@ export class TunnelPanel extends ViewPane { this.menuService, this.contextViewService, this.themeService, this.remoteExplorerService, this.commandService, this.configurationService, this.hoverService); const columns = [new IconColumn(), new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()]; - if (this.tunnelService.canMakePublic) { + if (this.tunnelService.canChangePrivacy) { columns.push(new PrivacyColumn()); } columns.push(new OriginColumn()); @@ -778,7 +810,7 @@ export class TunnelPanel extends ViewPane { accessibilityProvider: { getAriaLabel: (item: ITunnelItem) => { if (item instanceof TunnelItem) { - return `${item.tooltipPostfix} ${item.portTooltip} ${item.iconTooltip} ${item.processTooltip} ${item.originTooltip} ${this.tunnelService.canMakePublic ? item.privacyTooltip : ''}`; + return `${item.tooltipPostfix} ${item.portTooltip} ${item.iconTooltip} ${item.processTooltip} ${item.originTooltip} ${this.tunnelService.canChangePrivacy ? item.privacy.label : ''}`; } else { return item.label; } @@ -875,7 +907,7 @@ export class TunnelPanel extends ViewPane { this.tunnelViewSelectionContext.set(item); this.tunnelTypeContext.set(item.tunnelType); this.tunnelCloseableContext.set(!!item.closeable); - this.tunnelPrivacyContext.set(item.privacy); + this.tunnelPrivacyContext.set(item.privacy.id); this.tunnelProtocolContext.set(item.protocol === TunnelProtocol.Https ? TunnelProtocol.Https : TunnelProtocol.Https); this.portChangableContextKey.set(!!item.localPort); } else { @@ -927,7 +959,7 @@ export class TunnelPanel extends ViewPane { this.table.setFocus([this.table.indexOf(node)]); this.tunnelTypeContext.set(node.tunnelType); this.tunnelCloseableContext.set(!!node.closeable); - this.tunnelPrivacyContext.set(node.privacy); + this.tunnelPrivacyContext.set(node.privacy.id); this.tunnelProtocolContext.set(node.protocol); this.portChangableContextKey.set(!!node.localPort); } else { @@ -1033,7 +1065,7 @@ namespace LabelTunnelAction { } } -const invalidPortString: string = nls.localize('remote.tunnelsView.portNumberValid', "Forwarded port is invalid."); +const invalidPortString: string = nls.localize('remote.tunnelsView.portNumberValid', "Forwarded port should be a number or a host:port."); const maxPortNumber: number = 65536; const invalidPortNumberString: string = nls.localize('remote.tunnelsView.portNumberToHigh', "Port number must be \u2265 0 and < {0}.", maxPortNumber); const requiresSudoString: string = nls.localize('remote.tunnelView.inlineElevationMessage', "May Require Sudo"); @@ -1115,9 +1147,9 @@ interface QuickPickTunnel extends IQuickPickItem { tunnel?: ITunnelItem } -function makeTunnelPicks(tunnels: Tunnel[], remoteExplorerService: IRemoteExplorerService): QuickPickInput[] { +function makeTunnelPicks(tunnels: Tunnel[], remoteExplorerService: IRemoteExplorerService, tunnelService: ITunnelService): QuickPickInput[] { const picks: QuickPickInput[] = tunnels.map(forwarded => { - const item = TunnelItem.createFromTunnel(remoteExplorerService, forwarded); + const item = TunnelItem.createFromTunnel(remoteExplorerService, tunnelService, forwarded); return { label: item.label, description: item.processDescription, @@ -1160,9 +1192,10 @@ namespace ClosePortAction { return async (accessor) => { const quickInputService = accessor.get(IQuickInputService); const remoteExplorerService = accessor.get(IRemoteExplorerService); + const tunnelService = accessor.get(ITunnelService); const commandService = accessor.get(ICommandService); - const picks: QuickPickInput[] = makeTunnelPicks(Array.from(remoteExplorerService.tunnelModel.forwarded.values()).filter(tunnel => tunnel.closeable), remoteExplorerService); + const picks: QuickPickInput[] = makeTunnelPicks(Array.from(remoteExplorerService.tunnelModel.forwarded.values()).filter(tunnel => tunnel.closeable), remoteExplorerService, tunnelService); const result = await quickInputService.pick(picks, { placeHolder: nls.localize('remote.tunnel.closePlaceholder', "Choose a port to stop forwarding") }); if (result && result.tunnel) { await remoteExplorerService.close({ host: result.tunnel.remoteHost, port: result.tunnel.remotePort }); @@ -1248,12 +1281,13 @@ namespace OpenPortInBrowserCommandPaletteAction { export function handler(): ICommandHandler { return async (accessor, arg) => { const remoteExplorerService = accessor.get(IRemoteExplorerService); + const tunnelService = accessor.get(ITunnelService); const model = remoteExplorerService.tunnelModel; const quickPickService = accessor.get(IQuickInputService); const openerService = accessor.get(IOpenerService); const commandService = accessor.get(ICommandService); const options: QuickPickTunnel[] = [...model.forwarded, ...model.detected].map(value => { - const tunnelItem = TunnelItem.createFromTunnel(remoteExplorerService, value[1]); + const tunnelItem = TunnelItem.createFromTunnel(remoteExplorerService, tunnelService, value[1]); return { label: tunnelItem.label, description: tunnelItem.processDescription, @@ -1305,11 +1339,12 @@ namespace CopyAddressAction { return async (accessor, arg) => { const quickInputService = accessor.get(IQuickInputService); const remoteExplorerService = accessor.get(IRemoteExplorerService); + const tunnelService = accessor.get(ITunnelService); const commandService = accessor.get(ICommandService); const clipboardService = accessor.get(IClipboardService); const tunnels = Array.from(remoteExplorerService.tunnelModel.forwarded.values()).concat(Array.from(remoteExplorerService.tunnelModel.detected.values())); - const result = await quickInputService.pick(makeTunnelPicks(tunnels, remoteExplorerService), { placeHolder: nls.localize('remote.tunnel.copyAddressPlaceholdter', "Choose a forwarded port") }); + const result = await quickInputService.pick(makeTunnelPicks(tunnels, remoteExplorerService, tunnelService), { placeHolder: nls.localize('remote.tunnel.copyAddressPlaceholdter', "Choose a forwarded port") }); if (result && result.tunnel) { await copyAddress(remoteExplorerService, clipboardService, result.tunnel); } else if (result) { @@ -1367,11 +1402,8 @@ namespace ChangeLocalPortAction { } } -namespace MakePortPublicAction { - export const ID = 'remote.tunnel.makePublic'; - export const LABEL = nls.localize('remote.tunnel.makePublic', "Make Public"); - - export function handler(): ICommandHandler { +namespace ChangeTunnelPrivacyAction { + export function handler(privacyId: string): ICommandHandler { return async (accessor, arg) => { if (arg instanceof TunnelItem) { const remoteExplorerService = accessor.get(IRemoteExplorerService); @@ -1381,29 +1413,7 @@ namespace MakePortPublicAction { local: arg.localPort, name: arg.name, elevateIfNeeded: true, - isPublic: true, - source: arg.source - }); - } - }; - } -} - -namespace MakePortPrivateAction { - export const ID = 'remote.tunnel.makePrivate'; - export const LABEL = nls.localize('remote.tunnel.makePrivate', "Make Private"); - - export function handler(): ICommandHandler { - return async (accessor, arg) => { - if (arg instanceof TunnelItem) { - const remoteExplorerService = accessor.get(IRemoteExplorerService); - await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort }); - return remoteExplorerService.forward({ - remote: { host: arg.remoteHost, port: arg.remotePort }, - local: arg.localPort, - name: arg.name, - elevateIfNeeded: true, - isPublic: false, + privacy: privacyId, source: arg.source }); } @@ -1444,6 +1454,7 @@ const tunnelViewCommandsWeightBonus = 10; // give our commands a little bit more const isForwardedExpr = TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded); const isForwardedOrDetectedExpr = ContextKeyExpr.or(isForwardedExpr, TunnelTypeContextKey.isEqualTo(TunnelType.Detected)); const isNotMultiSelectionExpr = TunnelViewMultiSelectionContextKey.isEqualTo(undefined); +const isNotPrivateExpr = ContextKeyExpr.and(TunnelPrivacyContextKey.notEqualsTo(TunnelPrivacyId.Private), TunnelPrivacyContextKey.notEqualsTo(TunnelPrivacyId.ConstantPrivate)); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: LabelTunnelAction.ID, @@ -1477,13 +1488,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CopyAddressAction.INLINE_ID, weight: KeybindingWeight.WorkbenchContrib + tunnelViewCommandsWeightBonus, when: ContextKeyExpr.and(TunnelViewFocusContextKey, isForwardedOrDetectedExpr, isNotMultiSelectionExpr), - primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + primary: KeyMod.CtrlCmd | KeyCode.KeyC, handler: CopyAddressAction.inlineHandler() }); CommandsRegistry.registerCommand(CopyAddressAction.COMMANDPALETTE_ID, CopyAddressAction.commandPaletteHandler()); CommandsRegistry.registerCommand(ChangeLocalPortAction.ID, ChangeLocalPortAction.handler()); -CommandsRegistry.registerCommand(MakePortPublicAction.ID, MakePortPublicAction.handler()); -CommandsRegistry.registerCommand(MakePortPrivateAction.ID, MakePortPrivateAction.handler()); CommandsRegistry.registerCommand(SetTunnelProtocolAction.ID_HTTP, SetTunnelProtocolAction.handlerHttp()); CommandsRegistry.registerCommand(SetTunnelProtocolAction.ID_HTTPS, SetTunnelProtocolAction.handlerHttps()); @@ -1533,7 +1542,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ title: OpenPortInPreviewAction.LABEL, }, when: ContextKeyExpr.and( - ContextKeyExpr.or(WebContextKey.negate(), TunnelPrivacyContextKey.isEqualTo(TunnelPrivacy.Public)), + ContextKeyExpr.or(WebContextKey.negate(), isNotPrivateExpr), isForwardedOrDetectedExpr, isNotMultiSelectionExpr) })); @@ -1569,20 +1578,9 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '2_localaddress', order: 2, - command: { - id: MakePortPublicAction.ID, - title: MakePortPublicAction.LABEL, - }, - when: ContextKeyExpr.and(TunnelPrivacyContextKey.isEqualTo(TunnelPrivacy.Private), isNotMultiSelectionExpr) -})); -MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ - group: '2_localaddress', - order: 2, - command: { - id: MakePortPrivateAction.ID, - title: MakePortPrivateAction.LABEL, - }, - when: ContextKeyExpr.and(TunnelPrivacyContextKey.isEqualTo(TunnelPrivacy.Public), isNotMultiSelectionExpr) + submenu: MenuId.TunnelPrivacy, + title: nls.localize('tunnelContext.privacyMenu', "Port Visibility"), + when: ContextKeyExpr.and(isForwardedExpr, TunnelPrivacyEnabledContextKey) })); MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '2_localaddress', @@ -1684,7 +1682,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ icon: openPreviewIcon }, when: ContextKeyExpr.and( - ContextKeyExpr.or(WebContextKey.negate(), TunnelPrivacyContextKey.isEqualTo(TunnelPrivacy.Public)), + ContextKeyExpr.or(WebContextKey.negate(), isNotPrivateExpr), isForwardedOrDetectedExpr) })); diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 4d6753c91d..457dcea434 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -20,6 +20,11 @@ import { TunnelFactoryContribution } from 'vs/workbench/contrib/remote/common/tu import { ShowCandidateContribution } from 'vs/workbench/contrib/remote/common/showCandidate'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { firstOrDefault } from 'vs/base/common/arrays'; export class LabelContribution implements IWorkbenchContribution { constructor( @@ -86,9 +91,72 @@ class RemoteLogOutputChannels implements IWorkbenchContribution { } } +class RemoteInvalidWorkspaceDetector extends Disposable implements IWorkbenchContribution { + + constructor( + @IFileService private readonly fileService: IFileService, + @IDialogService private readonly dialogService: IDialogService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService + ) { + super(); + + // When connected to a remote workspace, we currently cannot + // validate that the workspace exists before actually opening + // it. As such, we need to check on that after startup and guide + // the user to a valid workspace. + // (see https://github.com/microsoft/vscode/issues/133872) + if (this.environmentService.remoteAuthority) { + remoteAgentService.getEnvironment().then(remoteEnv => { + if (remoteEnv) { + // we use the presence of `remoteEnv` to figure out + // if we got a healthy remote connection + // (see https://github.com/microsoft/vscode/issues/135331) + this.validateRemoteWorkspace(); + } + }); + } + } + + private async validateRemoteWorkspace(): Promise { + const workspace = this.contextService.getWorkspace(); + const workspaceUriToStat = workspace.configuration ?? firstOrDefault(workspace.folders)?.uri; + if (!workspaceUriToStat) { + return; // only when in workspace + } + + const exists = await this.fileService.exists(workspaceUriToStat); + if (exists) { + return; // all good! + } + + const res = await this.dialogService.confirm({ + type: 'warning', + message: localize('invalidWorkspaceMessage', "Workspace does not exist"), + detail: localize('invalidWorkspaceDetail', "The workspace does not exist. Please select another workspace to open."), + primaryButton: localize('invalidWorkspacePrimary', "&&Open Workspace..."), + secondaryButton: localize('invalidWorkspaceCancel', "&&Cancel") + }); + + if (res.confirmed) { + + // Pick Workspace + if (workspace.configuration) { + return this.fileDialogService.pickWorkspaceAndOpen({}); + } + + // Pick Folder + return this.fileDialogService.pickFolderAndOpen({}); + } + } +} + const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(LabelContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteChannelsContribution, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(RemoteInvalidWorkspaceDetector, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteLogOutputChannels, LifecyclePhase.Restored); workbenchContributionsRegistry.registerWorkbenchContribution(TunnelFactoryContribution, LifecyclePhase.Ready); workbenchContributionsRegistry.registerWorkbenchContribution(ShowCandidateContribution, LifecyclePhase.Ready); @@ -97,13 +165,11 @@ const extensionKindSchema: IJSONSchema = { type: 'string', enum: [ 'ui', - 'workspace', - 'web' + 'workspace' ], enumDescriptions: [ localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."), - localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote."), - localize('web', "Web worker extension kind. Such an extension can execute in a web worker extension host.") + localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.") ], }; @@ -117,7 +183,7 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'object', markdownDescription: localize('remote.extensionKind', "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely."), patternProperties: { - '([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$': { + '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': { oneOf: [{ type: 'array', items: extensionKindSchema }, extensionKindSchema], default: ['ui'], }, @@ -154,7 +220,7 @@ Registry.as(ConfigurationExtensions.Configuration) patternProperties: { '(^\\d+(\\-\\d+)?$)|(.+)': { type: 'object', - description: localize('remote.portsAttributes.port', "A port, range of ports (ex. \"40000-55000\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression."), + description: localize('remote.portsAttributes.port', "A port, range of ports (ex. \"40000-55000\"), host and port (ex. \"db:1234\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression."), properties: { 'onAutoForward': { type: 'string', @@ -200,7 +266,15 @@ Registry.as(ConfigurationExtensions.Configuration) markdownDescription: localize('remote.portsAttributes', "Set properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Application\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n},\n\".+\\\\/server.js\": {\n \"onAutoForward\": \"openPreview\"\n}\n```"), defaultSnippets: [{ body: { '${1:3000}': { label: '${2:Application}', onAutoForward: 'openPreview' } } }], errorMessage: localize('remote.portsAttributes.patternError', "Must be a port number, range of port numbers, or regular expression."), - additionalProperties: false + additionalProperties: false, + default: { + '443': { + 'protocol': 'https' + }, + '8443': { + 'protocol': 'https' + } + } }, 'remote.otherPortsAttributes': { type: 'object', diff --git a/src/vs/workbench/contrib/remote/common/tunnelFactory.ts b/src/vs/workbench/contrib/remote/common/tunnelFactory.ts index fca16f1b02..479cbaf123 100644 --- a/src/vs/workbench/contrib/remote/common/tunnelFactory.ts +++ b/src/vs/workbench/contrib/remote/common/tunnelFactory.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITunnelService, TunnelOptions, RemoteTunnel, TunnelCreationOptions, ITunnel, TunnelProtocol } from 'vs/platform/remote/common/tunnel'; +import * as nls from 'vs/nls'; +import { ITunnelService, TunnelOptions, RemoteTunnel, TunnelCreationOptions, ITunnel, TunnelProtocol, TunnelPrivacyId } from 'vs/platform/remote/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -24,8 +25,25 @@ export class TunnelFactoryContribution extends Disposable implements IWorkbenchC super(); const tunnelFactory = environmentService.options?.tunnelProvider?.tunnelFactory; if (tunnelFactory) { + let privacyOptions = environmentService.options?.tunnelProvider?.features?.privacyOptions ?? []; + if (environmentService.options?.tunnelProvider?.features?.public + && (privacyOptions.length === 0)) { + privacyOptions = [ + { + id: 'private', + label: nls.localize('tunnelPrivacy.private', "Private"), + themeIcon: 'lock' + }, + { + id: 'public', + label: nls.localize('tunnelPrivacy.public', "Public"), + themeIcon: 'eye' + } + ]; + } + this._register(tunnelService.setTunnelProvider({ - forwardPort: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined => { + forwardPort: async (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise => { let tunnelPromise: Promise | undefined; try { tunnelPromise = tunnelFactory(tunnelOptions, tunnelCreationOptions); @@ -33,34 +51,34 @@ export class TunnelFactoryContribution extends Disposable implements IWorkbenchC logService.trace('tunnelFactory: tunnel provider error'); } - return new Promise(async (resolve) => { - if (!tunnelPromise) { - resolve(undefined); - return; - } - let tunnel: ITunnel; - try { - tunnel = await tunnelPromise; - } catch (e) { - logService.trace('tunnelFactory: tunnel provider promise error'); - resolve(undefined); - return; - } - const localAddress = tunnel.localAddress.startsWith('http') ? tunnel.localAddress : `http://${tunnel.localAddress}`; - const remoteTunnel: RemoteTunnel = { - tunnelRemotePort: tunnel.remoteAddress.port, - tunnelRemoteHost: tunnel.remoteAddress.host, - // The tunnel factory may give us an inaccessible local address. - // To make sure this doesn't happen, resolve the uri immediately. - localAddress: await this.resolveExternalUri(localAddress), - public: !!tunnel.public, - protocol: tunnel.protocol ?? TunnelProtocol.Http, - dispose: async () => { await tunnel.dispose(); } - }; - resolve(remoteTunnel); - }); + if (!tunnelPromise) { + return undefined; + } + let tunnel: ITunnel; + try { + tunnel = await tunnelPromise; + } catch (e) { + logService.trace('tunnelFactory: tunnel provider promise error'); + return undefined; + } + const localAddress = tunnel.localAddress.startsWith('http') ? tunnel.localAddress : `http://${tunnel.localAddress}`; + const remoteTunnel: RemoteTunnel = { + tunnelRemotePort: tunnel.remoteAddress.port, + tunnelRemoteHost: tunnel.remoteAddress.host, + // The tunnel factory may give us an inaccessible local address. + // To make sure this doesn't happen, resolve the uri immediately. + localAddress: await this.resolveExternalUri(localAddress), + privacy: tunnel.privacy ?? (tunnel.public ? TunnelPrivacyId.Public : TunnelPrivacyId.Private), + protocol: tunnel.protocol ?? TunnelProtocol.Http, + dispose: async () => { await tunnel.dispose(); } + }; + return remoteTunnel; } - }, environmentService.options?.tunnelProvider?.features ?? { elevation: false, public: false })); + }, { + elevation: !!environmentService.options?.tunnelProvider?.features?.elevation, + public: !!environmentService.options?.tunnelProvider?.features?.public, + privacyOptions + })); remoteExplorerService.setTunnelInformation(undefined); } } diff --git a/src/vs/workbench/contrib/remote/electron-sandbox/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-sandbox/remote.contribution.ts index 268fc4b4d9..7d431be614 100644 --- a/src/vs/workbench/contrib/remote/electron-sandbox/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-sandbox/remote.contribution.ts @@ -29,6 +29,8 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot import { IDownloadService } from 'vs/platform/download/common/download'; import { OpenLocalFileFolderCommand, OpenLocalFileCommand, OpenLocalFolderCommand, SaveLocalFileCommand, RemoteFileDialogContext } from 'vs/workbench/services/dialogs/browser/simpleFileDialog'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { TelemetryLevel, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; +import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils'; class RemoteChannelsContribution implements IWorkbenchContribution { @@ -104,14 +106,14 @@ class RemoteTelemetryEnablementUpdater extends Disposable implements IWorkbenchC this.updateRemoteTelemetryEnablement(); this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('telemetry.enableTelemetry')) { + if (e.affectsConfiguration(TELEMETRY_SETTING_ID)) { this.updateRemoteTelemetryEnablement(); } })); } private updateRemoteTelemetryEnablement(): Promise { - if (!this.configurationService.getValue('telemetry.enableTelemetry')) { + if (getTelemetryLevel(this.configurationService) === TelemetryLevel.NONE) { return this.remoteAgentService.disableTelemetry(); } @@ -178,7 +180,7 @@ if (isMacintosh) { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: OpenLocalFileFolderCommand.ID, weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KEY_O, + primary: KeyMod.CtrlCmd | KeyCode.KeyO, when: RemoteFileDialogContext, description: { description: OpenLocalFileFolderCommand.LABEL, args: [] }, handler: OpenLocalFileFolderCommand.handler() @@ -187,7 +189,7 @@ if (isMacintosh) { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: OpenLocalFileCommand.ID, weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KEY_O, + primary: KeyMod.CtrlCmd | KeyCode.KeyO, when: RemoteFileDialogContext, description: { description: OpenLocalFileCommand.LABEL, args: [] }, handler: OpenLocalFileCommand.handler() @@ -195,7 +197,7 @@ if (isMacintosh) { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: OpenLocalFolderCommand.ID, weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_O), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyO), when: RemoteFileDialogContext, description: { description: OpenLocalFolderCommand.LABEL, args: [] }, handler: OpenLocalFolderCommand.handler() @@ -205,7 +207,7 @@ if (isMacintosh) { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: SaveLocalFileCommand.ID, weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyS, when: RemoteFileDialogContext, description: { description: SaveLocalFileCommand.LABEL, args: [] }, handler: SaveLocalFileCommand.handler() diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index bfd14d23f7..e47f2d0047 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -11,7 +11,7 @@ import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/w import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; +import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorResourceAccessor } from 'vs/workbench/common/editor'; diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index b52b338c5a..b33441eb16 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -50,6 +50,7 @@ import { EncodingMode, ITextFileEditorModel, IResolvedTextFileEditorModel, IText import { gotoNextLocation, gotoPreviousLocation } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { TextCompareEditorActiveContext } from 'vs/workbench/common/editor'; class DiffActionRunner extends ActionRunner { @@ -293,7 +294,7 @@ class DirtyDiffWidget extends PeekViewWidget { minimap: { enabled: false }, renderSideBySide: false, readOnly: false, - ignoreTrimWhitespace: false + renderIndicators: false }; this.diffEditor = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, container, options, this.editor); @@ -363,7 +364,7 @@ export class ShowPreviousChangeAction extends EditorAction { id: 'editor.action.dirtydiff.previous', label: nls.localize('show previous change', "Show Previous Change"), alias: 'Show Previous Change', - precondition: undefined, + precondition: TextCompareEditorActiveContext.toNegated(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } }); } @@ -397,7 +398,7 @@ export class ShowNextChangeAction extends EditorAction { id: 'editor.action.dirtydiff.next', label: nls.localize('show next change', "Show Next Change"), alias: 'Show Next Change', - precondition: undefined, + precondition: TextCompareEditorActiveContext.toNegated(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } }); } @@ -443,14 +444,14 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { order: 2 }); -export class MoveToPreviousChangeAction extends EditorAction { +export class GotoPreviousChangeAction extends EditorAction { constructor() { super({ id: 'workbench.action.editor.previousChange', - label: nls.localize('move to previous change', "Move to Previous Change"), - alias: 'Move to Previous Change', - precondition: undefined, + label: nls.localize('move to previous change', "Go to Previous Change"), + alias: 'Go to Previous Change', + precondition: TextCompareEditorActiveContext.toNegated(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } }); } @@ -483,16 +484,16 @@ export class MoveToPreviousChangeAction extends EditorAction { outerEditor.revealPositionInCenter(position); } } -registerEditorAction(MoveToPreviousChangeAction); +registerEditorAction(GotoPreviousChangeAction); -export class MoveToNextChangeAction extends EditorAction { +export class GotoNextChangeAction extends EditorAction { constructor() { super({ id: 'workbench.action.editor.nextChange', - label: nls.localize('move to next change', "Move to Next Change"), - alias: 'Move to Next Change', - precondition: undefined, + label: nls.localize('move to next change', "Go to Next Change"), + alias: 'Go to Next Change', + precondition: TextCompareEditorActiveContext.toNegated(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } }); } @@ -525,7 +526,7 @@ export class MoveToNextChangeAction extends EditorAction { outerEditor.revealPositionInCenter(position); } } -registerEditorAction(MoveToNextChangeAction); +registerEditorAction(GotoNextChangeAction); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'closeDirtyDiff', diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index e5da6a242b..8d36c04c50 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -182,7 +182,7 @@ .scm-view .scm-input { height: 100%; - padding-left: 8px; + padding-left: 11px; } .scm-view .scm-editor { @@ -194,6 +194,23 @@ justify-content: center; } +.scm-view .button-container { + margin-left: 8px; + height: 100%; + display: flex; + align-items: center; +} + +.scm-view .button-container .codicon { + margin: 0 0.4em; +} + +.scm-view .button-container .codicon.codicon-arrow-up, +.scm-view .button-container .codicon.codicon-arrow-down { + font-size: small !important; + margin: 0 0.2em 0 0; +} + .scm-view .scm-editor.hidden { display: none; } diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 248160c3e0..48dadd86d2 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -74,9 +74,9 @@ viewsRegistry.registerViews([{ mnemonicTitle: localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "S&&CM"), keybindings: { primary: 0, - win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_G }, + win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG }, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyG }, }, order: 2, } @@ -207,6 +207,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'number', description: localize('providersVisible', "Controls how many repositories are visible in the Source Control Repositories section. Set to `0` to be able to manually resize the view."), default: 10 + }, + 'scm.showActionButton': { + type: 'boolean', + markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the SCM view."), + default: true } } }); diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts index 01224a31b5..4fbb7a4fb3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/scm'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { append, $ } from 'vs/base/browser/dom'; import { ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -110,7 +110,18 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer disposed = true)); + disposables.add(repository.provider.onDidChange(() => { + if (disposed) { + return; + } + + onDidChangeProvider(); + })); + onDidChangeProvider(); const menus = this.scmViewService.menus.getRepositoryMenus(repository.provider); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index e7ad14e2bc..4a115ad003 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -6,11 +6,11 @@ import 'vs/css!./media/scm'; import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { append, $, Dimension, asCSSUrl, trackFocus } from 'vs/base/browser/dom'; +import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -23,8 +23,8 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, import { IAction, ActionRunner } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, registerThemingParticipant, IFileIconTheme, ThemeIcon, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider } from './util'; -import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton } from './util'; +import { attachBadgeStyler, attachButtonStyler } from 'vs/platform/theme/common/styler'; import { WorkbenchCompressibleObjectTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer } from 'vs/base/common/async'; @@ -82,8 +82,11 @@ import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/ import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Command } from 'vs/editor/common/modes'; +import { INotificationService } from 'vs/platform/notification/common/notification'; -type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode | ISCMResource; +type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode | ISCMResource; interface ISCMLayout { height: number | undefined; @@ -91,6 +94,51 @@ interface ISCMLayout { readonly onDidChange: Event; } +interface ActionButtonTemplate { + readonly actionButton: ScmActionButton; + disposable: IDisposable; + readonly templateDisposable: IDisposable; +} + +class ActionButtonRenderer implements ICompressibleTreeRenderer { + static readonly DEFAULT_HEIGHT = 30; + + static readonly TEMPLATE_ID = 'actionButton'; + get templateId(): string { return ActionButtonRenderer.TEMPLATE_ID; } + + constructor( + @ICommandService private commandService: ICommandService, + @IThemeService private themeService: IThemeService, + @INotificationService private notificationService: INotificationService, + ) { } + + renderTemplate(container: HTMLElement): ActionButtonTemplate { + const buttonContainer = append(container, $('.button-container')); + const actionButton = new ScmActionButton(buttonContainer, this.commandService, this.themeService, this.notificationService); + + return { actionButton, disposable: Disposable.None, templateDisposable: actionButton }; + } + + renderElement(node: ITreeNode, index: number, templateData: ActionButtonTemplate, height: number | undefined): void { + templateData.disposable.dispose(); + + templateData.actionButton.setButton(node.element.button); + } + + renderCompressedElements(): void { + throw new Error('Should never happen since node is incompressible'); + } + + disposeElement(node: ITreeNode, index: number, template: ActionButtonTemplate): void { + template.disposable.dispose(); + } + + disposeTemplate(templateData: ActionButtonTemplate): void { + templateData.disposable.dispose(); + templateData.templateDisposable.dispose(); + } +} + interface InputTemplate { readonly inputWidget: SCMInputWidget; disposable: IDisposable; @@ -529,6 +577,8 @@ class ListDelegate implements IListVirtualDelegate { getHeight(element: TreeElement) { if (isSCMInput(element)) { return this.inputRenderer.getHeight(element); + } else if (isSCMActionButton(element)) { + return ActionButtonRenderer.DEFAULT_HEIGHT + 10; } else { return 22; } @@ -539,6 +589,8 @@ class ListDelegate implements IListVirtualDelegate { return RepositoryRenderer.TEMPLATE_ID; } else if (isSCMInput(element)) { return InputRenderer.TEMPLATE_ID; + } else if (isSCMActionButton(element)) { + return ActionButtonRenderer.TEMPLATE_ID; } else if (ResourceTree.isResourceNode(element) || isSCMResource(element)) { return ResourceRenderer.TEMPLATE_ID; } else { @@ -582,6 +634,12 @@ export class SCMTreeSorter implements ITreeSorter { return 1; } + if (isSCMActionButton(one)) { + return -1; + } else if (isSCMActionButton(other)) { + return 1; + } + if (isSCMResourceGroup(one)) { if (!isSCMResourceGroup(other)) { throw new Error('Invalid comparison'); @@ -642,9 +700,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | { toString(): string; }[] | undefined { if (ResourceTree.isResourceNode(element)) { return element.name; - } else if (isSCMRepository(element)) { - return undefined; - } else if (isSCMInput(element)) { + } else if (isSCMRepository(element) || isSCMInput(element) || isSCMActionButton(element)) { return undefined; } else if (isSCMResourceGroup(element)) { return element.label; @@ -682,6 +738,9 @@ function getSCMResourceId(element: TreeElement): string { } else if (isSCMInput(element)) { const provider = element.repository.provider; return `input:${provider.id}`; + } else if (isSCMActionButton(element)) { + const provider = element.repository.provider; + return `actionButton:${provider.id}`; } else if (isSCMResource(element)) { const group = element.resourceGroup; const provider = group.provider; @@ -727,6 +786,8 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider('scm.alwaysShowRepositories'); + this.showActionButton = this.configurationService.getValue('scm.showActionButton'); this.refresh(); } } @@ -1043,7 +1106,12 @@ class ViewModel { for (const repository of added) { const disposable = combinedDisposable( repository.provider.groups.onDidSplice(splice => this._onDidSpliceGroups(item, splice)), - repository.input.onDidChangeVisibility(() => this.refresh(item)) + repository.input.onDidChangeVisibility(() => this.refresh(item)), + repository.provider.onDidChange(() => { + if (this.showActionButton) { + this.refresh(item); + } + }) ); const groupItems = repository.provider.groups.elements.map(group => this.createGroupItem(group)); const item: IRepositoryItem = { @@ -1154,6 +1222,8 @@ class ViewModel { this.scmProviderHasRootUriContextKey.set(false); } + const focusedInput = this.inputRenderer.getFocusedInput(); + if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { const item = Iterable.first(this.items.values())!; this.tree.setChildren(null, this.render(item, this.treeViewState).children); @@ -1164,6 +1234,10 @@ class ViewModel { this.tree.setChildren(null, items.map(item => this.render(item, this.treeViewState))); } + if (focusedInput) { + this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); + } + this.updateRepositoryCollapseAllContextKeys(); } @@ -1176,10 +1250,23 @@ class ViewModel { children.push({ element: item.element.input, incompressible: true, collapsible: false }); } - if (this.items.size === 1 || hasSomeChanges) { + if (hasSomeChanges || (this.items.size === 1 && (!this.showActionButton || !item.element.provider.actionButton))) { children.push(...item.groupItems.map(i => this.render(i, treeViewState))); } + if (this.showActionButton && item.element.provider.actionButton) { + const button: ICompressedTreeElement = { + element: { + type: 'actionButton', + repository: item.element, + button: item.element.provider.actionButton, + }, + incompressible: true, + collapsible: false + }; + children.push(button); + } + const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; @@ -1538,7 +1625,7 @@ class SCMInputWidget extends Disposable { this.repositoryDisposables.add(this.inputEditor.onDidChangeCursorPosition(triggerValidation)); // Adaptive indentation rules - const opts = this.modelService.getCreationOptions(textModel.getLanguageIdentifier().language, textModel.uri, textModel.isForSimpleWidget); + const opts = this.modelService.getCreationOptions(textModel.getLanguageId(), textModel.uri, textModel.isForSimpleWidget); const onEnter = Event.filter(this.inputEditor.onKeyDown, e => e.keyCode === KeyCode.Enter); this.repositoryDisposables.add(onEnter(() => textModel.detectIndentation(opts.insertSpaces, opts.tabSize))); @@ -1969,6 +2056,7 @@ export class SCMViewPane extends ViewPane { const renderers: ICompressibleTreeRenderer[] = [ this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), this.inputRenderer, + this.instantiationService.createInstance(ActionButtonRenderer), this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(ResourceRenderer, () => this._viewModel, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner) ]; @@ -2118,6 +2206,10 @@ export class SCMViewPane extends ViewPane { } } + return; + } else if (isSCMActionButton(e.element)) { + this.scmViewService.focus(e.element.repository); + return; } @@ -2170,7 +2262,7 @@ export class SCMViewPane extends ViewPane { const menu = menus.repositoryMenu; context = element.provider; [actions, disposable] = collectContextMenuActions(menu); - } else if (isSCMInput(element)) { + } else if (isSCMInput(element) || isSCMActionButton(element)) { // noop } else if (isSCMResourceGroup(element)) { const menus = this.scmViewService.menus.getRepositoryMenus(element.provider); @@ -2310,3 +2402,48 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.scm-view .scm-provider > .status > .monaco-action-bar > .actions-container { border-color: ${repositoryStatusActionsBorderColor}; }`); } }); + +export class ScmActionButton implements IDisposable { + private button: Button | undefined; + private readonly disposables = new MutableDisposable(); + + constructor( + private readonly container: HTMLElement, + private readonly commandService: ICommandService, + private readonly themeService: IThemeService, + private readonly notificationService: INotificationService + ) { + } + + dispose(): void { + this.disposables?.dispose(); + } + + + setButton(button: Command | undefined): void { + // Clear old button + this.clear(); + if (!button) { + return; + } + + this.button = new Button(this.container, { title: button.tooltip, supportIcons: true }); + this.button.label = button.title; + this.button.onDidClick(async () => { + try { + await this.commandService.executeCommand(button!.id, ...(button!.arguments || [])); + } catch (ex) { + this.notificationService.error(ex); + } + }, null, this.disposables.value); + + this.disposables.value!.add(this.button); + this.disposables.value!.add(attachButtonStyler(this.button, this.themeService)); + } + + private clear(): void { + this.disposables.value = new DisposableStore(); + this.button = undefined; + clearNode(this.container); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index b3a5fea6b0..61226f0cd8 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IDisposable, Disposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -25,6 +25,10 @@ export function isSCMInput(element: any): element is ISCMInput { return !!(element as ISCMInput).validateInput && typeof (element as ISCMInput).value === 'string'; } +export function isSCMActionButton(element: any): element is ISCMActionButton { + return (element as ISCMActionButton).type === 'actionButton'; +} + export function isSCMResourceGroup(element: any): element is ISCMResourceGroup { return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements; } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index c1fc860ad6..20d3534fde 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -65,6 +65,7 @@ export interface ISCMProvider extends IDisposable { readonly onDidChangeCommitTemplate: Event; readonly onDidChangeStatusBarCommands?: Event; readonly acceptInputCommand?: Command; + readonly actionButton?: Command; readonly statusBarCommands?: Command[]; readonly onDidChange: Event; @@ -96,6 +97,12 @@ export interface ISCMInputChangeEvent { readonly reason?: SCMInputChangeReason; } +export interface ISCMActionButton { + readonly type: 'actionButton'; + readonly repository: ISCMRepository; + readonly button?: Command; +} + export interface ISCMInput { readonly repository: ISCMRepository; diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index f714e9956b..38c470cb3b 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -27,14 +27,15 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkbenchEditorConfiguration, IEditorInput, EditorResourceAccessor, isEditorInput } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration, EditorResourceAccessor, isEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ThrottledDelayer } from 'vs/base/common/async'; import { top } from 'vs/base/common/arrays'; import { FileQueryCacheState } from 'vs/workbench/contrib/search/common/cacheState'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { IResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorOptions, IResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { Schemas } from 'vs/base/common/network'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ResourceMap } from 'vs/base/common/map'; @@ -45,7 +46,7 @@ import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/b import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ScrollType, IEditor, ICodeEditorViewState, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; import { once } from 'vs/base/common/functional'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { getIEditor } from 'vs/editor/browser/editorBrowser'; import { withNullAsUndefined } from 'vs/base/common/types'; import { Codicon } from 'vs/base/common/codicons'; @@ -82,7 +83,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | undefined = undefined; editorViewState: { - editor: IEditorInput, + editor: EditorInput, group: IEditorGroup, state: ICodeEditorViewState | IDiffEditorViewState | undefined } | undefined = undefined; @@ -143,7 +144,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { if (this.editorViewState) { - const options: ITextEditorOptions = { + const options: IEditorOptions = { viewState: this.editorViewState.state, preserveFocus: true /* import to not close the picker as a result */ }; @@ -177,7 +178,8 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { @@ -942,7 +944,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + private async openAnything(resourceOrEditor: URI | EditorInput | IResourceEditorInput, options: { keyMods?: IKeyMods, preserveFocus?: boolean, range?: IRange, forceOpenSideBySide?: boolean, forcePinned?: boolean }): Promise { // Craft some editor options based on quick access usage const editorOptions: ITextEditorOptions = { @@ -960,7 +962,8 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider showHistoryKeybindingHint(this.keybindingService) }, this.contextKeyService); this._register(attachInputBoxStyler(this.inputBox, this.themeService)); this._register(this.inputBox.onDidChange(() => this._onSubmit.fire(true))); @@ -192,8 +196,9 @@ export class IncludePatternInputWidget extends PatternInputWidget { @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, ) { - super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService); + super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService, keybindingService); } private useSearchInEditorsBox!: Checkbox; @@ -209,6 +214,7 @@ export class IncludePatternInputWidget extends PatternInputWidget { setOnlySearchInOpenEditors(value: boolean) { this.useSearchInEditorsBox.checked = value; + this._onChangeSearchInEditorsBoxEmitter.fire(); } protected override getSubcontrolsWidth(): number { @@ -242,8 +248,9 @@ export class ExcludePatternInputWidget extends PatternInputWidget { @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, ) { - super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService); + super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService, keybindingService); } private useExcludesAndIgnoreFilesBox!: Checkbox; diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index 601e32f632..d559792b20 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -70,7 +70,7 @@ class ReplacePreviewModel extends Disposable { const fileMatch = this.searchWorkbenchService.searchModel.searchResult.matches().filter(match => match.resource.toString() === fileResource.toString())[0]; const ref = this._register(await this.textModelResolverService.createModelReference(fileResource)); const sourceModel = ref.object.textEditorModel; - const sourceModelModeId = sourceModel.getLanguageIdentifier().language; + const sourceModelModeId = sourceModel.getLanguageId(); const replacePreviewModel = this.modelService.createModel(createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()), this.modeService.create(sourceModelModeId), replacePreviewUri); this._register(fileMatch.onChange(({ forceUpdateModel }) => this.update(sourceModel, replacePreviewModel, fileMatch, forceUpdateModel))); this._register(this.searchWorkbenchService.searchModel.onReplaceTermChanged(() => this.update(sourceModel, replacePreviewModel, fileMatch))); diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index bfc9beb76a..f6eceabe74 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -17,7 +17,7 @@ import { Action2, ICommandAction, MenuId, MenuRegistry, registerAction2, SyncAct import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyRegexExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -53,8 +53,8 @@ import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browse import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ISearchConfiguration, SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, VIEW_ID } from 'vs/workbench/services/search/common/search'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); registerSingleton(ISearchHistoryService, SearchHistoryService, true); @@ -68,7 +68,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.search.toggleQueryDetails', weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.or(Constants.SearchViewFocusedKey, SearchEditorConstants.InSearchEditor), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_J, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyJ, handler: accessor => { const contextService = accessor.get(IContextKeyService).getContext(document.activeElement); if (contextService.getValue(SearchEditorConstants.InSearchEditor.serialize())) { @@ -149,7 +149,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceActionId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, handler: (accessor, args: any) => { const searchView = getSearchView(accessor.get(IViewsService)); if (searchView) { @@ -163,7 +163,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceAllInFileActionId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], handler: (accessor, args: any) => { const searchView = getSearchView(accessor.get(IViewsService)); @@ -178,7 +178,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.ReplaceAllInFolderActionId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_1, + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], handler: (accessor, args: any) => { const searchView = getSearchView(accessor.get(IViewsService)); @@ -267,7 +267,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.CopyMatchCommandId, weight: KeybindingWeight.WorkbenchContrib, when: Constants.FileMatchOrMatchFocusKey, - primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + primary: KeyMod.CtrlCmd | KeyCode.KeyC, handler: copyMatchCommand }); @@ -285,9 +285,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.CopyPathCommandId, weight: KeybindingWeight.WorkbenchContrib, when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC, win: { - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyC }, handler: copyPathCommand }); @@ -325,7 +325,7 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommand({ id: Constants.RevealInSideBarForSearchResults, handler: (accessor, args: any) => { - const viewletService = accessor.get(IViewletService); + const paneCompositeService = accessor.get(IPaneCompositePartService); const explorerService = accessor.get(IExplorerService); const contextService = accessor.get(IWorkspaceContextService); @@ -344,7 +344,7 @@ CommandsRegistry.registerCommand({ return; } - viewletService.openViewlet(VIEWLET_ID_FILES, false).then((viewlet) => { + paneCompositeService.openPaneComposite(VIEWLET_ID_FILES, ViewContainerLocation.Sidebar, false).then((viewlet) => { if (!viewlet) { return; } @@ -378,7 +378,7 @@ registerAction2(class CancelSearchAction extends Action2 { id: MenuId.ViewTitle, group: 'navigation', order: 0, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch)), }] }); } @@ -400,7 +400,7 @@ registerAction2(class RefreshAction extends Action2 { id: MenuId.ViewTitle, group: 'navigation', order: 0, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch).negate()), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch).negate()), }] }); } @@ -422,7 +422,7 @@ registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { id: MenuId.ViewTitle, group: 'navigation', order: 3, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', VIEW_ID), ContextKeyExpr.or(Constants.HasSearchResults.negate(), Constants.ViewHasSomeCollapsibleKey)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.HasSearchResults.negate(), Constants.ViewHasSomeCollapsibleKey)), }] }); } @@ -444,7 +444,7 @@ registerAction2(class ExpandAllAction extends Action2 { id: MenuId.ViewTitle, group: 'navigation', order: 3, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', VIEW_ID), Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), }] }); } @@ -466,7 +466,7 @@ registerAction2(class ClearSearchResultsAction extends Action2 { id: MenuId.ViewTitle, group: 'navigation', order: 1, - when: ContextKeyEqualsExpr.create('view', VIEW_ID), + when: ContextKeyExpr.equals('view', VIEW_ID), }] }); } @@ -547,7 +547,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: FIND_IN_FOLDER_ID, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerFolderContext), - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F, + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, handler: searchInFolderCommand }); @@ -635,9 +635,9 @@ const viewDescriptor: IViewDescriptor = { id: viewContainer.id, mnemonicTitle: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search"), keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF, // Yes, this is weird. See #116188, #115556, #115511, and now #124146, for examples of what can go wrong here. - when: ContextKeyRegexExpr.create('neverMatch', /doesNotMatch/) + when: ContextKeyExpr.regex('neverMatch', /doesNotMatch/) }, order: 1 } @@ -714,7 +714,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.FindInFilesActionId, weight: KeybindingWeight.WorkbenchContrib, when: null, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF, handler: FindInFilesCommand }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: Constants.FindInFilesActionId, title: { value: nls.localize('findInFiles', "Find in Files"), original: 'Find in Files' }, category } }); @@ -730,7 +730,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { registry.registerWorkbenchAction(SyncActionDescriptor.from(FocusNextSearchResultAction, { primary: KeyCode.F4 }), 'Search: Focus Next Search Result', category.value, ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor)); registry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPreviousSearchResultAction, { primary: KeyMod.Shift | KeyCode.F4 }), 'Search: Focus Previous Search Result', category.value, ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor)); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ReplaceInFilesAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_H }), 'Search: Replace in Files', category.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(ReplaceInFilesAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH }), 'Search: Replace in Files', category.value); MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { group: '4_find_global', command: { @@ -782,7 +782,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.AddCursorsAtSearchResults, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, handler: (accessor, args: any) => { const searchView = getSearchView(accessor.get(IViewsService)); if (searchView) { @@ -792,7 +792,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowAllSymbolsAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_T }), 'Go to Symbol in Workspace...'); +registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowAllSymbolsAction, { primary: KeyMod.CtrlCmd | KeyCode.KeyT }), 'Go to Symbol in Workspace...'); registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSearchOnTypeAction), 'Search: Toggle Search on Type', category.value); // Register Quick Access Handler @@ -877,7 +877,7 @@ configurationRegistry.registerConfiguration({ }, 'search.useGlobalIgnoreFiles': { type: 'boolean', - markdownDescription: nls.localize('useGlobalIgnoreFiles', "Controls whether to use global `.gitignore` and `.ignore` files when searching for files."), + markdownDescription: nls.localize('useGlobalIgnoreFiles', "Controls whether to use global `.gitignore` and `.ignore` files when searching for files. Requires `#search.useIgnoreFiles#` to be enabled."), default: false, scope: ConfigurationScope.RESOURCE }, @@ -1020,6 +1020,11 @@ configurationRegistry.registerConfiguration({ nls.localize('searchSortOrder.countAscending', "Results are sorted by count per file, in ascending order.") ], 'description': nls.localize('search.sortOrder', "Controls sorting order of search results.") + }, + 'search.forceSearchProcess': { + type: 'boolean', + default: false, + description: nls.localize('search.forceSearchProcess', "When enabled, search in a local window runs in a separate search process instead of the extension host.") } } }); diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 0feed9a1aa..dec034596d 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; import { Action } from 'vs/base/common/actions'; -import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keybindings'; import { isWindows, OS } from 'vs/base/common/platform'; import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; diff --git a/src/vs/workbench/contrib/search/browser/searchMessage.ts b/src/vs/workbench/contrib/search/browser/searchMessage.ts index 0561d2b4dc..c394f22b73 100644 --- a/src/vs/workbench/contrib/search/browser/searchMessage.ts +++ b/src/vs/workbench/contrib/search/browser/searchMessage.ts @@ -42,7 +42,7 @@ export const renderSearchMessage = ( if (typeof node === 'string') { dom.append(div, document.createTextNode(node)); } else { - const link = instantiationService.createInstance(Link, node, { + const link = instantiationService.createInstance(Link, div, node, { opener: async href => { if (!message.trusted) { return; } const parsed = URI.parse(href, true); @@ -62,7 +62,6 @@ export const renderSearchMessage = ( } } }); - dom.append(div, link.el); disposableStore.add(link); } } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 631e956bb4..7613d7d2d9 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -322,8 +322,8 @@ export class SearchView extends ViewPane { dom.append(folderIncludesList, $('h4', undefined, filesToIncludeTitle)); this.inputPatternIncludes = this._register(this.instantiationService.createInstance(IncludePatternInputWidget, folderIncludesList, this.contextViewService, { - ariaLabel: nls.localize('label.includes', 'Search Include Patterns'), - placeholder: nls.localize('placeholder.includes', "(e.g. *.ts, src/**/include)"), + ariaLabel: filesToIncludeTitle, + placeholder: nls.localize('placeholder.includes', "e.g. *.ts, src/**/include"), showPlaceholderOnFocus: true, history: patternIncludesHistory, })); @@ -341,8 +341,8 @@ export class SearchView extends ViewPane { const excludesTitle = nls.localize('searchScope.excludes', "files to exclude"); dom.append(excludesList, $('h4', undefined, excludesTitle)); this.inputPatternExcludes = this._register(this.instantiationService.createInstance(ExcludePatternInputWidget, excludesList, this.contextViewService, { - ariaLabel: nls.localize('label.excludes', 'Search Exclude Patterns'), - placeholder: nls.localize('placeholder.excludes', "(e.g. *.ts, src/**/exclude)"), + ariaLabel: excludesTitle, + placeholder: nls.localize('placeholder.excludes', "e.g. *.ts, src/**/exclude"), showPlaceholderOnFocus: true, history: patternExclusionsHistory, })); @@ -512,7 +512,7 @@ export class SearchView extends ViewPane { protected refreshAndUpdateCount(event?: IChangeEvent): void { // {{SQL CARBON EDIT}} this.searchWidget.setReplaceAllActionState(!this.viewModel.searchResult.isEmpty()); - this.updateSearchResultCount(this.viewModel.searchResult.query!.userDisabledExcludesAndIgnoreFiles); + this.updateSearchResultCount(this.viewModel.searchResult.query!.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors); return this.refreshTree(event); } @@ -1574,6 +1574,7 @@ export class SearchView extends ViewPane { this.searchWidget.setReplaceAllActionState(false); + this.tree.setSelection([]); return this.viewModel.search(query) .then(onComplete, onError); } @@ -1607,7 +1608,12 @@ export class SearchView extends ViewPane { this.searchExcludePattern.setUseExcludesAndIgnoreFiles(true); } - protected updateSearchResultCount(disregardExcludesAndIgnores?: boolean, showOpenInEditor: boolean = true): void { // {{SQL CARBON EDIT}} - Hide Open in Editor in Notebooks viewlet + private onDisableSearchInOpenEditors(): void { + this.toggleQueryDetails(false, true); + this.inputPatternIncludes.setOnlySearchInOpenEditors(false); + } + + protected updateSearchResultCount(disregardExcludesAndIgnores?: boolean, onlyOpenEditors: boolean = false, showOpenInEditor: boolean = true): void { // {{SQL CARBON EDIT}} - Hide Open in Editor in Notebooks viewlet const fileCount = this.viewModel.searchResult.fileCount(); this.hasSearchResultsKey.set(fileCount > 0); @@ -1625,6 +1631,12 @@ export class SearchView extends ViewPane { dom.append(messageEl, $('span', undefined, excludesDisabledMessage, '(', enableExcludesButton.element, ')')); } + if (onlyOpenEditors) { + const searchingInOpenMessage = ' - ' + nls.localize('onlyOpenEditors', "searching only in open files") + ' '; + const disableOpenEditorsButton = this.messageDisposables.add(new SearchLinkButton(nls.localize('openEditors.disable', "disable"), this.onDisableSearchInOpenEditors.bind(this), nls.localize('disableOpenEditors', "Search in entire workspace"))); + dom.append(messageEl, $('span', undefined, searchingInOpenMessage, '(', disableOpenEditorsButton.element, ')')); + } + if (showOpenInEditor) { // {{SQL CARBON EDIT}} - Hide Open in Editor in Notebooks viewlet dom.append(messageEl, ' - '); } else { diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index af06b36092..7be0ea3329 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -35,6 +35,7 @@ import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { IViewsService } from 'vs/workbench/common/views'; import { searchReplaceAllIcon, searchHideReplaceIcon, searchShowContextIcon, searchShowReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { ToggleSearchEditorContextLinesCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { showHistoryKeybindingHint } from 'vs/platform/browser/historyWidgetKeybindingHint'; /** Specified in searchview.css */ export const SingleLineInputHeight = 24; @@ -156,7 +157,7 @@ export class SearchWidget extends Widget { @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService private readonly themeService: IThemeService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IKeybindingService private readonly keyBindingService: IKeybindingService, + @IKeybindingService private readonly keybindingService: IKeybindingService, @IClipboardService private readonly clipboardServce: IClipboardService, @IConfigurationService private readonly configurationService: IConfigurationService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService @@ -235,6 +236,7 @@ export class SearchWidget extends Widget { clearHistory(): void { this.searchInput.inputBox.clearHistory(); + this.replaceInput.inputBox.clearHistory(); } showNextSearchTerm() { @@ -306,10 +308,11 @@ export class SearchWidget extends Widget { label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search'), validation: (value: string) => this.validateSearchInput(value), placeholder: nls.localize('search.placeHolder', "Search"), - appendCaseSensitiveLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleCaseSensitiveCommandId), this.keyBindingService), - appendWholeWordsLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleWholeWordCommandId), this.keyBindingService), - appendRegexLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.ToggleRegexCommandId), this.keyBindingService), + appendCaseSensitiveLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.ToggleCaseSensitiveCommandId), this.keybindingService), + appendWholeWordsLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.ToggleWholeWordCommandId), this.keybindingService), + appendRegexLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.ToggleRegexCommandId), this.keybindingService), history: options.searchHistory, + showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), flexibleHeight: true, flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT }; @@ -354,7 +357,7 @@ export class SearchWidget extends Widget { this.showContextCheckbox = new Checkbox({ isChecked: false, - title: appendKeyBindingLabel(nls.localize('showContext', "Toggle Context Lines"), this.keyBindingService.lookupKeybinding(ToggleSearchEditorContextLinesCommandId), this.keyBindingService), + title: appendKeyBindingLabel(nls.localize('showContext', "Toggle Context Lines"), this.keybindingService.lookupKeybinding(ToggleSearchEditorContextLinesCommandId), this.keybindingService), icon: searchShowContextIcon // {{SQL CARBON EDIT}} }); this._register(this.showContextCheckbox.onChange(() => this.onContextLinesChanged())); @@ -396,8 +399,9 @@ export class SearchWidget extends Widget { this.replaceInput = this._register(new ContextScopedReplaceInput(replaceBox, this.contextViewService, { label: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview'), placeholder: nls.localize('search.replace.placeHolder', "Replace"), - appendPreserveCaseLabel: appendKeyBindingLabel('', this.keyBindingService.lookupKeybinding(Constants.TogglePreserveCaseId), this.keyBindingService), + appendPreserveCaseLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.TogglePreserveCaseId), this.keybindingService), history: options.replaceHistory, + showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), flexibleHeight: true, flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT }, this.contextKeyService, true)); @@ -452,7 +456,7 @@ export class SearchWidget extends Widget { setReplaceAllActionState(enabled: boolean): void { if (this.replaceAllAction.enabled !== enabled) { this.replaceAllAction.enabled = enabled; - this.replaceAllAction.label = enabled ? SearchWidget.REPLACE_ALL_ENABLED_LABEL(this.keyBindingService) : SearchWidget.REPLACE_ALL_DISABLED_LABEL; + this.replaceAllAction.label = enabled ? SearchWidget.REPLACE_ALL_ENABLED_LABEL(this.keybindingService) : SearchWidget.REPLACE_ALL_DISABLED_LABEL; this.updateReplaceActiveState(); } } diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index 0ed6284b0e..5a6f5cf0ae 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -98,7 +98,7 @@ export function getOutOfWorkspaceEditorResources(accessor: ServicesAccessor): UR const resources = editorService.editors .map(editor => EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY })) - .filter(resource => !!resource && !contextService.isInsideWorkspace(resource) && fileService.canHandleResource(resource)); + .filter(resource => !!resource && !contextService.isInsideWorkspace(resource) && fileService.hasProvider(resource)); return resources as URI[]; } diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index a986fcda1f..2e19974344 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Keybinding } from 'vs/base/common/keyCodes'; +import { Keybinding } from 'vs/base/common/keybindings'; import { OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IModelService } from 'vs/editor/common/services/modelService'; diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index fd25c17583..0372bf6345 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -21,12 +21,15 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; +import { ILanguageConfigurationService } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('Search - Viewlet', () => { let instantiation: TestInstantiationService; setup(() => { instantiation = new TestInstantiationService(); + instantiation.stub(ILanguageConfigurationService, TestLanguageConfigurationService); instantiation.stub(IModelService, stubModelService(instantiation)); instantiation.set(IWorkspaceContextService, new TestContextService(TestWorkspace)); instantiation.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); diff --git a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts index 694cc2b058..2fe483c89b 100644 --- a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts @@ -17,7 +17,6 @@ import { IFileMatch, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchPro import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; -import * as process from 'vs/base/common/process'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -92,7 +91,7 @@ suite('SearchModel', () => { return { textSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise { return new Promise(resolve => { - process.nextTick(() => { + queueMicrotask(() => { results.forEach(onProgress!); resolve(complete!); }); @@ -119,7 +118,7 @@ suite('SearchModel', () => { } return new Promise(resolve => { - process.nextTick(() => { + queueMicrotask(() => { resolve({}); }); }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index 9393a378bc..97fd2c0ed1 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -18,3 +18,5 @@ export const SearchEditorID = 'workbench.editor.searchEditor'; export const OpenNewEditorCommandId = 'search.action.openNewEditor'; export const OpenEditorCommandId = 'search.action.openEditor'; export const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines'; + +export const SearchEditorInputTypeId = 'workbench.editorinputs.searchEditorInput'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 16bcb5680d..c1a047fded 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -12,7 +12,7 @@ import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKe import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ContextKeyEqualsExpr, ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -325,7 +325,7 @@ registerAction2(class extends Action2 { title: { value: localize('search.rerunSearchInEditor', "Search Again"), original: 'Search Again' }, category, keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyR, when: SearchEditorConstants.InSearchEditor, weight: KeybindingWeight.EditorContrib }, @@ -440,8 +440,8 @@ registerAction2(class extends Action2 { precondition: SearchEditorConstants.InSearchEditor, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Alt | KeyCode.KEY_L, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L } + primary: KeyMod.Alt | KeyCode.KeyL, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyL } } }); } @@ -460,7 +460,7 @@ registerAction2(class extends Action2 { precondition: SearchEditorConstants.InSearchEditor, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Alt | KeyCode.US_EQUAL + primary: KeyMod.Alt | KeyCode.Equal } }); } @@ -477,7 +477,7 @@ registerAction2(class extends Action2 { precondition: SearchEditorConstants.InSearchEditor, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Alt | KeyCode.US_MINUS + primary: KeyMod.Alt | KeyCode.Minus } }); } @@ -494,7 +494,7 @@ registerAction2(class extends Action2 { precondition: SearchEditorConstants.InSearchEditor, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, } }); } @@ -514,7 +514,7 @@ registerAction2(class OpenSearchEditorAction extends Action2 { id: MenuId.ViewTitle, group: 'navigation', order: 2, - when: ContextKeyEqualsExpr.create('view', VIEW_ID), + when: ContextKeyExpr.equals('view', VIEW_ID), }] }); } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 1b152c8098..0cdbca8d50 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -39,13 +39,14 @@ import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platfor import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { EditorInputCapabilities, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; import { SearchModel, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; -import { InSearchEditor, SearchEditorFindMatchClass, SearchEditorID } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { InSearchEditor, SearchEditorFindMatchClass, SearchEditorID, SearchEditorInputTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -61,12 +62,12 @@ import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchM import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { UnusualLineTerminatorsDetector } from 'vs/editor/contrib/unusualLineTerminators/unusualLineTerminators'; -const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/; +const RESULT_LINE_REGEX = /^(\s+)(\d+)(: | )(\s*)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; type SearchEditorViewState = ICodeEditorViewState & { focused: 'input' | 'editor' }; -export class SearchEditor extends BaseTextEditor { +export class SearchEditor extends BaseTextEditor { static readonly ID: string = SearchEditorID; static readonly SEARCH_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'searchEditorViewState'; @@ -251,8 +252,6 @@ export class SearchEditor extends BaseTextEditor { } }); - this._register(this.onDidBlur(() => this.saveViewState())); - this._register(this.searchResultEditor.onDidChangeModelContent(() => this.getInput()?.setDirty(true))); } @@ -261,7 +260,7 @@ export class SearchEditor extends BaseTextEditor { } override focus() { - const viewState = this.loadViewState(); + const viewState = this.loadEditorViewState(this.getInput()); if (viewState && viewState.focused === 'editor') { this.searchResultEditor.focus(); } else { @@ -539,7 +538,7 @@ export class SearchEditor extends BaseTextEditor { }); const searchOperation = await startInput.ongoingSearchOperation; - this.onSearchComplete(searchOperation, config, startInput); + await this.onSearchComplete(searchOperation, config, startInput); } private async onSearchComplete(searchOperation: ISearchComplete, startConfig: SearchConfiguration, startInput: SearchEditorInput) { @@ -633,8 +632,6 @@ export class SearchEditor extends BaseTextEditor { } override async setInput(newInput: SearchEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - this.saveViewState(); - await super.setInput(newInput, options, context, token); if (token.isCancellationRequested) { return; @@ -658,7 +655,7 @@ export class SearchEditor extends BaseTextEditor { } })); - this.restoreViewState(); + this.restoreViewState(context); if (!options?.preserveFocus) { this.focus(); @@ -691,40 +688,32 @@ export class SearchEditor extends BaseTextEditor { this.reLayout(); } - override saveState() { - this.saveViewState(); - super.saveState(); + protected override toEditorViewStateResource(input: EditorInput): URI | undefined { + if (input.typeId === SearchEditorInputTypeId) { + return (input as SearchEditorInput).modelUri; + } + + return undefined; } - private saveViewState() { - const resource = this.getInput()?.modelUri; - if (resource) { this.saveTextEditorViewState(resource); } - } - - protected override retrieveTextEditorViewState(resource: URI): SearchEditorViewState | null { + protected override computeEditorViewState(resource: URI): SearchEditorViewState | undefined { const control = this.getControl(); const editorViewState = control.saveViewState(); - if (!editorViewState) { return null; } - if (resource.toString() !== this.getInput()?.modelUri.toString()) { return null; } + if (!editorViewState) { return undefined; } + if (resource.toString() !== this.getInput()?.modelUri.toString()) { return undefined; } return { ...editorViewState, focused: this.searchResultEditor.hasWidgetFocus() ? 'editor' : 'input' }; } - private loadViewState() { - const resource = assertIsDefined(this.getInput()?.modelUri); - return this.loadTextEditorViewState(resource) as SearchEditorViewState; + protected tracksEditorViewState(input: EditorInput): boolean { + return input.typeId === SearchEditorInputTypeId; } - private restoreViewState() { - const viewState = this.loadViewState(); + private restoreViewState(context: IEditorOpenContext) { + const viewState = this.loadEditorViewState(this.getInput(), context); if (viewState) { this.searchResultEditor.restoreViewState(viewState); } } - override clearInput() { - this.saveViewState(); - super.clearInput(); - } - getAriaLabel() { return this.getInput()?.getName() ?? localize('searchEditor', "Search"); } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 12f2f1df6e..e90061e6a4 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -16,9 +16,9 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, EditorResourceAccessor, IMoveResult, EditorInputCapabilities, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IRevertOptions, ISaveOptions, EditorResourceAccessor, IMoveResult, EditorInputCapabilities, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { Memento } from 'vs/workbench/common/memento'; -import { SearchEditorFindMatchClass, SearchEditorScheme, SearchEditorWorkingCopyTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { SearchEditorFindMatchClass, SearchEditorInputTypeId, SearchEditorScheme, SearchEditorWorkingCopyTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchConfigurationModel, SearchEditorModel, searchEditorModelFactory } from 'vs/workbench/contrib/searchEditor/browser/searchEditorModel'; import { defaultSearchConfig, parseSavedSearchEditor, serializeSearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -49,7 +49,7 @@ export type SearchConfiguration = { export const SEARCH_EDITOR_EXT = '.code-search'; export class SearchEditorInput extends EditorInput { - static readonly ID: string = 'workbench.editorinputs.searchEditorInput'; + static readonly ID: string = SearchEditorInputTypeId; override get typeId(): string { return SearchEditorInput.ID; @@ -127,7 +127,7 @@ export class SearchEditorInput extends EditorInput { this._register(this.workingCopyService.registerWorkingCopy(workingCopyAdapter)); } - override async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + override async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { if (((await this.getModels()).resultsModel).isDisposed()) { return undefined; } // {{SQL CARBON EDIT}} strict-null-checks if (this.backingUri) { @@ -172,7 +172,7 @@ export class SearchEditorInput extends EditorInput { }); } - override async saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + override async saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { const path = await this.fileDialogService.pickFileToSave(await this.suggestFileName(), options?.availableFileSystems); if (path) { this.telemetryService.publicLog2('searchEditor/saveSearchResults'); @@ -214,7 +214,7 @@ export class SearchEditorInput extends EditorInput { return this.dirty; } - override rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { + override async rename(group: GroupIdentifier, target: URI): Promise { if (extname(target) === SEARCH_EDITOR_EXT) { return { editor: this.instantiationService.invokeFunction(getOrMakeSearchEditorInput, { from: 'existingFile', fileUri: target }) @@ -229,7 +229,7 @@ export class SearchEditorInput extends EditorInput { super.dispose(); } - override matches(other: IEditorInput | IUntypedEditorInput): boolean { + override matches(other: EditorInput | IUntypedEditorInput): boolean { if (super.matches(other)) { return true; } diff --git a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts index ca1e593c89..62567d16fb 100644 --- a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { registerEditorAction, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { LanguageId } from 'vs/editor/common/modes'; +import { NULL_MODE_ID } from 'vs/editor/common/modes/nullMode'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; @@ -90,7 +90,7 @@ class InsertSnippetAction extends EditorAction { const clipboardService = accessor.get(IClipboardService); const quickInputService = accessor.get(IQuickInputService); - const snippet = await new Promise(async (resolve) => { + const snippet = await new Promise((resolve, reject) => { const { lineNumber, column } = editor.getPosition(); let { snippet, name, langId } = Args.fromUser(arg); @@ -107,11 +107,11 @@ class InsertSnippetAction extends EditorAction { )); } - let languageId = LanguageId.Null; + let languageId = NULL_MODE_ID; if (langId) { - const otherLangId = modeService.getLanguageIdentifier(langId); + const otherLangId = modeService.validateLanguageId(langId); if (otherLangId) { - languageId = otherLangId.id; + languageId = otherLangId; } } else { editor.getModel().tokenizeIfCheap(lineNumber); @@ -120,21 +120,20 @@ class InsertSnippetAction extends EditorAction { // validate the `languageId` to ensure this is a user // facing language with a name and the chance to have // snippets, else fall back to the outer language - const otherLangId = modeService.getLanguageIdentifier(languageId); - if (otherLangId && !modeService.getLanguageName(otherLangId.language)) { - languageId = editor.getModel().getLanguageIdentifier().id; + if (!modeService.getLanguageName(languageId)) { + languageId = editor.getModel().getLanguageId(); } } if (name) { // take selected snippet - const snippet = (await snippetService.getSnippets(languageId, { includeNoPrefixSnippets: true })).find(snippet => snippet.name === name); - resolve(snippet); + snippetService.getSnippets(languageId, { includeNoPrefixSnippets: true }) + .then(snippets => snippets.find(snippet => snippet.name === name)) + .then(resolve, reject); } else { // let user pick a snippet - const snippet = await this._pickSnippet(snippetService, quickInputService, languageId); - resolve(snippet); + resolve(this._pickSnippet(snippetService, quickInputService, languageId)); } }); @@ -148,7 +147,7 @@ class InsertSnippetAction extends EditorAction { SnippetController2.get(editor).insert(snippet.codeSnippet, { clipboardText }); } - private async _pickSnippet(snippetService: ISnippetsService, quickInputService: IQuickInputService, languageId: LanguageId): Promise { + private async _pickSnippet(snippetService: ISnippetsService, quickInputService: IQuickInputService, languageId: string): Promise { interface ISnippetPick extends IQuickPickItem { snippet: Snippet; @@ -207,6 +206,7 @@ class InsertSnippetAction extends EditorAction { picker.placeholder = nls.localize('pick.placeholder', "Select a snippet"); picker.matchOnDetail = true; picker.ignoreFocusOut = false; + picker.keepScrollPosition = true; picker.onDidTriggerItemButton(ctx => { const isEnabled = snippetService.isEnabled(ctx.item.snippet); snippetService.updateEnablement(ctx.item.snippet, !isEnabled); diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 92ddfe2326..a089a770b8 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -8,7 +8,7 @@ import { compare, compareSubstring } from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, LanguageId, CompletionItemInsertTextRule, CompletionContext, CompletionTriggerKind, CompletionItemLabel } from 'vs/editor/common/modes'; +import { CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, CompletionItemInsertTextRule, CompletionContext, CompletionTriggerKind, CompletionItemLabel } from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; import { localize } from 'vs/nls'; @@ -72,81 +72,61 @@ export class SnippetCompletionProvider implements CompletionItemProvider { const sw = new StopWatch(true); const languageId = this._getLanguageIdAtPosition(model, position); - const snippets = await this._snippets.getSnippets(languageId); + const snippets = new Set(await this._snippets.getSnippets(languageId)); - let pos = { lineNumber: position.lineNumber, column: 1 }; - let lineOffsets: number[] = []; - const lineContent = model.getLineContent(position.lineNumber).toLowerCase(); - const endsInWhitespace = /\s/.test(lineContent[position.column - 2]); + const lineContentLow = model.getLineContent(position.lineNumber).toLowerCase(); - while (pos.column < position.column) { - let word = model.getWordAtPosition(pos); - if (word) { - // at a word - lineOffsets.push(word.startColumn - 1); - pos.column = word.endColumn + 1; - if (word.endColumn < position.column && !/\s/.test(lineContent[word.endColumn - 1])) { - lineOffsets.push(word.endColumn - 1); - } - } - else if (!/\s/.test(lineContent[pos.column - 1])) { - // at a none-whitespace character - lineOffsets.push(pos.column - 1); - pos.column += 1; - } - else { - // always advance! - pos.column += 1; - } - } - - const availableSnippets = new Set(snippets); const suggestions: SnippetCompletion[] = []; - const columnOffset = position.column - 1; - for (const start of lineOffsets) { - availableSnippets.forEach(snippet => { - if (isPatternInWord(lineContent, start, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) { - const prefixPos = position.column - (1 + start); - const prefixRestLen = snippet.prefixLow.length - prefixPos; - const endsWithPrefixRest = compareSubstring(lineContent, snippet.prefixLow, columnOffset, (columnOffset) + prefixRestLen, prefixPos, prefixPos + prefixRestLen); - const startPosition = position.delta(0, -prefixPos); - let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column; + for (const snippet of snippets) { - // First check if there is anything to the right of the cursor - if (columnOffset < lineContent.length) { - const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageId); - const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContent[columnOffset]); - // If the character to the right of the cursor is a closing character of an autoclosing pair - if (standardAutoClosingPairConditionals?.some(p => - // and the start position is the opening character of an autoclosing pair - p.open === lineContent[startPosition.column - 1] && - // and the snippet prefix contains the opening and closing pair at its edges - snippet.prefix.startsWith(p.open) && - snippet.prefix[snippet.prefix.length - 1] === p.close)) { - - // Eat the character that was likely inserted because of auto-closing pairs - endColumn++; - } - } - - const replace = Range.fromPositions(startPosition, { lineNumber: position.lineNumber, column: endColumn }); - const insert = replace.setEndPosition(position.lineNumber, position.column); - - suggestions.push(new SnippetCompletion(snippet, { replace, insert })); - availableSnippets.delete(snippet); + for (let pos = Math.max(0, columnOffset - snippet.prefixLow.length); pos < lineContentLow.length; pos++) { + if (!isPatternInWord(lineContentLow, pos, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) { + continue; } - }); - } - if (endsInWhitespace || lineOffsets.length === 0) { - // add remaing snippets when the current prefix ends in whitespace or when no - // interesting positions have been found - availableSnippets.forEach(snippet => { - const insert = Range.fromPositions(position); - const replace = lineContent.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert; + + const prefixRestLen = snippet.prefixLow.length - (columnOffset - pos); + const endsWithPrefixRest = compareSubstring(lineContentLow, snippet.prefixLow, columnOffset, columnOffset + prefixRestLen, columnOffset - pos); + const startPosition = position.with(undefined, pos + 1); + let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column; + + // First check if there is anything to the right of the cursor + if (columnOffset < lineContentLow.length) { + const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageId); + const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContentLow[columnOffset]); + // If the character to the right of the cursor is a closing character of an autoclosing pair + if (standardAutoClosingPairConditionals?.some(p => + // and the start position is the opening character of an autoclosing pair + p.open === lineContentLow[startPosition.column - 1] && + // and the snippet prefix contains the opening and closing pair at its edges + snippet.prefix.startsWith(p.open) && + snippet.prefix[snippet.prefix.length - 1] === p.close) + ) { + // Eat the character that was likely inserted because of auto-closing pairs + endColumn++; + } + } + + const replace = Range.fromPositions(startPosition, { lineNumber: position.lineNumber, column: endColumn }); + const insert = replace.setEndPosition(position.lineNumber, position.column); + suggestions.push(new SnippetCompletion(snippet, { replace, insert })); - }); + snippets.delete(snippet); + break; + } + } + + + const endsInWhitespace = /\s/.test(lineContentLow[position.column - 2]); + + if (endsInWhitespace || !lineContentLow /*empty line*/) { + // add remaing snippets when the current prefix ends in whitespace or when line is empty + for (let snippet of snippets) { + const insert = Range.fromPositions(position); + const replace = lineContentLow.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert; + suggestions.push(new SnippetCompletion(snippet, { replace, insert })); + } } @@ -174,15 +154,15 @@ export class SnippetCompletionProvider implements CompletionItemProvider { return (item instanceof SnippetCompletion) ? item.resolve() : item; } - private _getLanguageIdAtPosition(model: ITextModel, position: Position): LanguageId { + private _getLanguageIdAtPosition(model: ITextModel, position: Position): string { // validate the `languageId` to ensure this is a user // facing language with a name and the chance to have // snippets, else fall back to the outer language model.tokenizeIfCheap(position.lineNumber); - let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column); - const languageIdentifier = this._modeService.getLanguageIdentifier(languageId); - if (languageIdentifier && !this._modeService.getLanguageName(languageIdentifier.language)) { - languageId = model.getLanguageIdentifier().id; + let languageId: string | null = model.getLanguageIdAtPosition(position.lineNumber, position.column); + languageId = this._modeService.validateLanguageId(languageId); + if (!languageId || !this._modeService.getLanguageName(languageId)) { + languageId = model.getLanguageId(); } return languageId; } diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index 8aa3b664e0..fc595a484d 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -8,7 +8,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import * as nls from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { LanguageId } from 'vs/editor/common/modes'; import { SnippetFile, Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; export const ISnippetsService = createDecorator('snippetService'); @@ -28,9 +27,9 @@ export interface ISnippetsService { updateEnablement(snippet: Snippet, enabled: boolean): void; - getSnippets(languageId: LanguageId, opt?: ISnippetGetOptions): Promise; + getSnippets(languageId: string, opt?: ISnippetGetOptions): Promise; - getSnippetsSync(languageId: LanguageId, opt?: ISnippetGetOptions): Snippet[]; + getSnippetsSync(languageId: string, opt?: ISnippetGetOptions): Snippet[]; } const languageScopeSchemaId = 'vscode://schemas/snippets'; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 3ecc68801e..46bfff9f62 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -9,7 +9,6 @@ import * as resources from 'vs/base/common/resources'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; -import { LanguageId } from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest'; import { localize } from 'vs/nls'; @@ -222,15 +221,14 @@ class SnippetsService implements ISnippetsService { return this._files.values(); } - async getSnippets(languageId: LanguageId, opts?: ISnippetGetOptions): Promise { + async getSnippets(languageId: string, opts?: ISnippetGetOptions): Promise { await this._joinSnippets(); const result: Snippet[] = []; const promises: Promise[] = []; - const languageIdentifier = this._modeService.getLanguageIdentifier(languageId); - if (languageIdentifier) { - const langName = languageIdentifier.language; + const langName = this._modeService.validateLanguageId(languageId); + if (langName) { for (const file of this._files.values()) { promises.push(file.load() .then(file => file.select(langName, result)) @@ -242,11 +240,10 @@ class SnippetsService implements ISnippetsService { return this._filterSnippets(result, opts); } - getSnippetsSync(languageId: LanguageId, opts?: ISnippetGetOptions): Snippet[] { + getSnippetsSync(languageId: string, opts?: ISnippetGetOptions): Snippet[] { const result: Snippet[] = []; - const languageIdentifier = this._modeService.getLanguageIdentifier(languageId); - if (languageIdentifier) { - const langName = languageIdentifier.language; + const langName = this._modeService.validateLanguageId(languageId); + if (langName) { for (const file of this._files.values()) { // kick off loading (which is a noop in case it's already loaded) // and optimistically collect snippets diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 861badb706..2213bcf7bb 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -12,7 +12,7 @@ import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { CompletionContext, CompletionTriggerKind } from 'vs/editor/common/modes'; +import { CompletionContext, CompletionItemLabel, CompletionItemRanges, CompletionTriggerKind } from 'vs/editor/common/modes'; import { DisposableStore } from 'vs/base/common/lifecycle'; class SimpleSnippetService implements ISnippetsService { @@ -40,21 +40,23 @@ suite('SnippetsService', function () { const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke }; suiteSetup(function () { - ModesRegistry.registerLanguage({ + disposableStore.add(ModesRegistry.registerLanguage({ id: 'fooLang', extensions: ['.fooLang',] - }); + })); }); suiteTeardown(function () { disposableStore.dispose(); }); + let disposables: DisposableStore; let modeService: ModeServiceImpl; let snippetService: ISnippetsService; setup(function () { - modeService = new ModeServiceImpl(); + disposables = new DisposableStore(); + modeService = disposables.add(new ModeServiceImpl()); snippetService = new SimpleSnippetService([new Snippet( ['fooLang'], 'barTest', @@ -74,10 +76,14 @@ suite('SnippetsService', function () { )]); }); + teardown(() => { + disposables.dispose(); + }); + test('snippet completions - simple', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - const model = createTextModel('', undefined, modeService.getLanguageIdentifier('fooLang')); + const model = disposables.add(createTextModel('', undefined, 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -85,10 +91,21 @@ suite('SnippetsService', function () { }); }); + test('snippet completions - simple 2', function () { + + const provider = new SnippetCompletionProvider(modeService, snippetService); + const model = disposables.add(createTextModel('hello ', undefined, 'fooLang')); + + return provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => { + assert.strictEqual(result.incomplete, undefined); + assert.strictEqual(result.suggestions.length, 2); + }); + }); + test('snippet completions - with prefix', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - const model = createTextModel('bar', undefined, modeService.getLanguageIdentifier('fooLang')); + const model = disposables.add(createTextModel('bar', undefined, 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 4), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -123,7 +140,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(modeService, snippetService); - const model = createTextModel('bar-bar', undefined, modeService.getLanguageIdentifier('fooLang')); + const model = disposables.add(createTextModel('bar-bar', undefined, 'fooLang')); await provider.provideCompletionItems(model, new Position(1, 3), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -133,24 +150,34 @@ suite('SnippetsService', function () { description: 'barTest' }); assert.strictEqual(result.suggestions[0].insertText, 's1'); - assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 1); + assert.strictEqual((result.suggestions[0].range as CompletionItemRanges).insert.startColumn, 1); assert.deepStrictEqual(result.suggestions[1].label, { label: 'bar-bar', description: 'name' }); assert.strictEqual(result.suggestions[1].insertText, 's2'); - assert.strictEqual((result.suggestions[1].range as any).insert.startColumn, 1); + assert.strictEqual((result.suggestions[1].range as CompletionItemRanges).insert.startColumn, 1); }); await provider.provideCompletionItems(model, new Position(1, 5), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); - assert.strictEqual(result.suggestions.length, 1); - assert.deepStrictEqual(result.suggestions[0].label, { + assert.strictEqual(result.suggestions.length, 2); + + const [first, second] = result.suggestions; + + assert.deepStrictEqual(first.label, { + label: 'bar', + description: 'barTest' + }); + assert.strictEqual(first.insertText, 's1'); + assert.strictEqual((first.range as CompletionItemRanges).insert.startColumn, 5); + + assert.deepStrictEqual(second.label, { label: 'bar-bar', description: 'name' }); - assert.strictEqual(result.suggestions[0].insertText, 's2'); - assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 1); + assert.strictEqual(second.insertText, 's2'); + assert.strictEqual((second.range as CompletionItemRanges).insert.startColumn, 1); }); await provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => { @@ -184,19 +211,19 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('\t { assert.strictEqual(result.suggestions.length, 1); model.dispose(); - model = createTextModel('\t { assert.strictEqual(result.suggestions.length, 1); assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 2); model.dispose(); - model = createTextModel('a { assert.strictEqual(result.suggestions.length, 1); @@ -219,7 +246,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('\n\t\n>/head>', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel('\n\t\n>/head>', undefined, 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.suggestions.length, 1); return provider.provideCompletionItems(model, new Position(2, 2), context)!; @@ -249,7 +276,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel('', undefined, 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.suggestions.length, 2); let [first, second] = result.suggestions; @@ -276,7 +303,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('p-', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel('p-', undefined, 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -301,7 +328,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', undefined, 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -320,7 +347,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel(':', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel(':', undefined, 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 0); @@ -339,7 +366,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('template', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel('template', undefined, 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -362,14 +389,14 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', undefined, 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.strictEqual(result.suggestions.length, 1); }); test('issue #61296: VS code freezes when editing CSS file with emoji', async function () { - disposableStore.add(LanguageConfigurationRegistry.register(modeService.getLanguageIdentifier('fooLang')!, { + disposableStore.add(LanguageConfigurationRegistry.register('fooLang'!, { wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g })); @@ -385,7 +412,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('.🐷-a-b', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel('.🐷-a-b', undefined, 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -404,7 +431,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('a ', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = disposables.add(createTextModel('a ', undefined, 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -431,19 +458,21 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel(' <', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel(' <', undefined, 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); let [first] = result.suggestions; assert.strictEqual((first.range as any).insert.startColumn, 2); + model.dispose(); - model = createTextModel('1', undefined, modeService.getLanguageIdentifier('fooLang')); + model = createTextModel('1', undefined, 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 1); [first] = result.suggestions; assert.strictEqual((first.range as any).insert.startColumn, 1); + model.dispose(); }); test('Snippet replace range', async function () { @@ -459,29 +488,32 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('not wordFoo bar', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('not wordFoo bar', undefined, 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); let [first] = result.suggestions; assert.strictEqual((first.range as any).insert.endColumn, 3); assert.strictEqual((first.range as any).replace.endColumn, 9); + model.dispose(); - model = createTextModel('not woFoo bar', undefined, modeService.getLanguageIdentifier('fooLang')); + model = createTextModel('not woFoo bar', undefined, 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); [first] = result.suggestions; assert.strictEqual((first.range as any).insert.endColumn, 3); assert.strictEqual((first.range as any).replace.endColumn, 3); + model.dispose(); - model = createTextModel('not word', undefined, modeService.getLanguageIdentifier('fooLang')); + model = createTextModel('not word', undefined, 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 1), context)!; assert.strictEqual(result.suggestions.length, 1); [first] = result.suggestions; assert.strictEqual((first.range as any).insert.endColumn, 1); assert.strictEqual((first.range as any).replace.endColumn, 9); + model.dispose(); }); test('Snippet replace-range incorrect #108894', async function () { @@ -498,17 +530,18 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('filler e KEEP ng filler', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('filler e KEEP ng filler', undefined, 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.strictEqual(result.suggestions.length, 1); let [first] = result.suggestions; assert.strictEqual((first.range as any).insert.endColumn, 9); assert.strictEqual((first.range as any).replace.endColumn, 9); + model.dispose(); }); test('Snippet will replace auto-closing pair if specified in prefix', async function () { - disposableStore.add(LanguageConfigurationRegistry.register(modeService.getLanguageIdentifier('fooLang')!, { + disposableStore.add(LanguageConfigurationRegistry.register('fooLang'!, { brackets: [ ['{', '}'], ['[', ']'], @@ -528,7 +561,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = createTextModel('[psc]', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('[psc]', undefined, 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 5), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -536,5 +569,31 @@ suite('SnippetsService', function () { assert.strictEqual((first.range as any).insert.endColumn, 5); // This is 6 because it should eat the `]` at the end of the text even if cursor is before it assert.strictEqual((first.range as any).replace.endColumn, 6); + model.dispose(); + }); + + test('Leading whitespace in snippet prefix #123860', async function () { + + snippetService = new SimpleSnippetService([new Snippet( + ['fooLang'], + 'cite-name', + ' cite', + '', + '~\\cite{$CLIPBOARD}', + '', + SnippetSource.User + )]); + + const provider = new SnippetCompletionProvider(modeService, snippetService); + + let model = createTextModel(' ci', undefined, 'fooLang'); + let result = await provider.provideCompletionItems(model, new Position(1, 4), context)!; + + assert.strictEqual(result.suggestions.length, 1); + let [first] = result.suggestions; + assert.strictEqual((first.label).label, ' cite'); + assert.strictEqual((first.range).insert.startColumn, 1); + + model.dispose(); }); }); diff --git a/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts b/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts index b507db411f..2fffadf57b 100644 --- a/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; import { language } from 'vs/base/common/platform'; import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -39,7 +38,7 @@ class CESContribution extends Disposable implements IWorkbenchContribution { @ITelemetryService private readonly telemetryService: ITelemetryService, @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, - @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, + @ITASExperimentService tasExperimentService: ITASExperimentService, ) { super(); diff --git a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts index 6494bac566..5e9549d92c 100644 --- a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts @@ -93,9 +93,6 @@ class LanguageSurvey extends Disposable { return; } - // __GDPR__TODO__ Need to move away from dynamic event names as those cannot be registered statically - telemetryService.publicLog(`${data.surveyId}.survey/userAsked`); - notificationService.prompt( Severity.Info, localize('helpUs', "Help us improve our support for {0}", modeService.getLanguageName(data.languageId) ?? data.languageId), diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts index c7e18bfcd2..c3d8eb1074 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts @@ -7,7 +7,7 @@ import { sha1Hex } from 'vs/base/browser/hash'; import { onUnexpectedError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { IFileService, IFileStat } from 'vs/platform/files/common/files'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ITextFileService, } from 'vs/workbench/services/textfile/common/textfiles'; @@ -36,7 +36,7 @@ export class WorkspaceTags implements IWorkbenchContribution { @IProductService private readonly productService: IProductService, @INativeHostService private readonly nativeHostService: INativeHostService ) { - if (this.telemetryService.isOptedIn) { + if (this.telemetryService.telemetryLevel === TelemetryLevel.USAGE) { this.report(); } } @@ -80,6 +80,7 @@ export class WorkspaceTags implements IWorkbenchContribution { telemetryId, rendererSessionId: info.sessionId, folders: workspace.folders, + transient: workspace.transient, configuration: workspace.configuration }; }); diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index 9ea099d9b1..c95255d201 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -38,6 +38,11 @@ const ModulesToLookFor = [ 'hapi', 'socket.io', 'restify', + 'next', + 'nuxt', + '@nestjs/core', + 'strapi', + 'gatsby', // JS frameworks 'react', 'react-native', @@ -48,6 +53,7 @@ const ModulesToLookFor = [ '@ionic', 'vue', 'tns-core-modules', + '@nativescript/core', 'electron', // Other interesting packages 'aws-sdk', @@ -311,6 +317,11 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.npm.hapi" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.socket.io" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.restify" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.next" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.nuxt" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@nestjs/core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.strapi" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.gatsby" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.rnpm-plugin-windows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.react" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@angular/core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -696,9 +707,9 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { let dependencies = Object.keys(packageJsonContents['dependencies'] || {}).concat(Object.keys(packageJsonContents['devDependencies'] || {})); for (let dependency of dependencies) { - if ('react-native' === dependency) { + if (dependency.startsWith('react-native')) { tags['workspace.reactNative'] = true; - } else if ('tns-core-modules' === dependency) { + } else if ('tns-core-modules' === dependency || '@nativescript/core' === dependency) { tags['workspace.nativescript'] = true; } else if (ModulesToLookFor.indexOf(dependency) > -1) { tags['workspace.npm.' + dependency] = true; @@ -755,6 +766,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { // Ignore errors when resolving android }); }); + return Promise.all([...packageJsonPromises, ...requirementsTxtPromises, ...pipfilePromises, ...pomPromises, ...gradlePromises, ...androidPromises]).then(() => tags); }); } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 9935639e71..fc1219d413 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -35,7 +35,6 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -83,6 +82,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; import { Schemas } from 'vs/base/common/network'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; // {{SQL CARBON EDIT}} // integration with tasks view panel @@ -247,7 +247,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer @IConfigurationService private readonly configurationService: IConfigurationService, @IMarkerService protected readonly markerService: IMarkerService, @IOutputService protected readonly outputService: IOutputService, - @IPanelService private readonly panelService: IPanelService, + @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IViewsService private readonly viewsService: IViewsService, @ICommandService private readonly commandService: ICommandService, @IEditorService private readonly editorService: IEditorService, @@ -935,15 +935,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer throw new TaskError(Severity.Info, nls.localize('TaskServer.noTask', 'Task to execute is undefined'), TaskErrors.TaskNotFound); } - return new Promise(async (resolve) => { + return new Promise((resolve) => { let resolver = this.createResolver(); if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(task) && !InMemoryTask.is(task)) { - const toExecute = await this.attachProblemMatcher(task); - if (toExecute) { - resolve(this.executeTask(toExecute, resolver, runSource)); - } else { - resolve(undefined); - } + this.attachProblemMatcher(task).then(toExecute => { + if (toExecute) { + resolve(this.executeTask(toExecute, resolver, runSource)); + } else { + resolve(undefined); + } + }); } else { resolve(this.executeTask(task, resolver, runSource)); } @@ -1673,7 +1674,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected createTerminalTaskSystem(): ITaskSystem { return new TerminalTaskSystem( - this.terminalService, this.terminalGroupService, this.outputService, this.panelService, this.viewsService, this.markerService, + this.terminalService, this.terminalGroupService, this.outputService, this.paneCompositeService, this.viewsService, this.markerService, this.modelService, this.configurationResolverService, this.telemetryService, this.contextService, this.environmentService, AbstractTaskService.OutputChannelId, this.fileService, this.terminalProfileResolverService, @@ -2400,9 +2401,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } }); - const timeout: boolean = await Promise.race([new Promise(async (resolve) => { - await _createEntries; - resolve(false); + const timeout: boolean = await Promise.race([new Promise((resolve) => { + _createEntries.then(() => resolve(false)); }), new Promise((resolve) => { const timer = setTimeout(() => { clearTimeout(timer); @@ -3081,9 +3081,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); }); - const timeout: boolean = await Promise.race([new Promise(async (resolve) => { - await entries; - resolve(false); + const timeout: boolean = await Promise.race([new Promise((resolve) => { + entries.then(() => resolve(false)); }), new Promise((resolve) => { const timer = setTimeout(() => { clearTimeout(timer); diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index c4f3ade9a4..baee3b78eb 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -20,6 +20,7 @@ import { ConfigurationTarget } from 'vs/platform/configuration/common/configurat import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; +import { ILogService } from 'vs/platform/log/common/log'; const ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE = 'tasks.run.allowAutomatic'; @@ -27,22 +28,29 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut constructor( @ITaskService private readonly taskService: ITaskService, @IStorageService private readonly storageService: IStorageService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService) { + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @ILogService private readonly logService: ILogService) { super(); this.tryRunTasks(); } private async tryRunTasks() { + this.logService.trace('RunAutomaticTasks: Trying to run tasks.'); // Wait until we have task system info (the extension host and workspace folders are available). if (!this.taskService.hasTaskSystemInfo) { + this.logService.trace('RunAutomaticTasks: Awaiting task system info.'); await Event.toPromise(Event.once(this.taskService.onDidChangeTaskSystemInfo)); } + + this.logService.trace('RunAutomaticTasks: Checking if automatic tasks should run.'); const isFolderAutomaticAllowed = this.storageService.getBoolean(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, StorageScope.WORKSPACE, undefined); const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted(); // Only run if allowed. Prompting for permission occurs when a user first tries to run a task. if (isFolderAutomaticAllowed && isWorkspaceTrusted) { this.taskService.getWorkspaceTasks(TaskRunSource.FolderOpen).then(workspaceTaskResult => { let { tasks } = RunAutomaticTasks.findAutoTasks(this.taskService, workspaceTaskResult); + this.logService.trace(`RunAutomaticTasks: Found ${tasks.length} automatic tasks tasks`); + if (tasks.length > 0) { RunAutomaticTasks.runTasks(this.taskService, tasks); } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index c11cb83083..49cacc4c2d 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -16,7 +16,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/services/output/common/output'; @@ -360,7 +360,7 @@ KeybindingsRegistry.registerKeybindingRule({ id: 'workbench.action.tasks.build', weight: KeybindingWeight.WorkbenchContrib, when: undefined, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_B + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyB }); // Tasks Output channel. Register it before using it in Task Service. diff --git a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts index 2e3d04da5d..bd358b72ee 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts @@ -12,6 +12,7 @@ import { TaskEvent, TaskEventKind } from 'vs/workbench/contrib/tasks/common/task import { ITaskService, Task } from 'vs/workbench/contrib/tasks/common/taskService'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalStatus } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; interface TerminalData { terminal: ITerminalInstance; @@ -25,6 +26,10 @@ const SUCCEEDED_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, ic const SUCCEEDED_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.check, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.succeededInactive', "Task succeeded and waiting...") }; const FAILED_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.error, severity: Severity.Error, tooltip: nls.localize('taskTerminalStatus.errors', "Task has errors") }; const FAILED_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.error, severity: Severity.Error, tooltip: nls.localize('taskTerminalStatus.errorsInactive', "Task has errors and is waiting...") }; +const WARNING_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.warning, severity: Severity.Warning, tooltip: nls.localize('taskTerminalStatus.warnings', "Task has warnings") }; +const WARNING_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.warning, severity: Severity.Warning, tooltip: nls.localize('taskTerminalStatus.warningsInactive', "Task has warnings and is waiting...") }; +const INFO_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.info, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.infos', "Task has infos") }; +const INFO_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.info, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.infosInactive', "Task has infos and is waiting...") }; export class TaskTerminalStatus extends Disposable { private terminalMap: Map = new Map(); @@ -66,8 +71,12 @@ export class TaskTerminalStatus extends Disposable { terminalData.terminal.statusList.remove(terminalData.status); if ((event.exitCode === 0) && (terminalData.problemMatcher.numberOfMatches === 0)) { terminalData.terminal.statusList.add(SUCCEEDED_TASK_STATUS); - } else { + } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Error) { terminalData.terminal.statusList.add(FAILED_TASK_STATUS); + } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Warning) { + terminalData.terminal.statusList.add(WARNING_TASK_STATUS); + } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Info) { + terminalData.terminal.statusList.add(INFO_TASK_STATUS); } } @@ -79,8 +88,12 @@ export class TaskTerminalStatus extends Disposable { terminalData.terminal.statusList.remove(terminalData.status); if (terminalData.problemMatcher.numberOfMatches === 0) { terminalData.terminal.statusList.add(SUCCEEDED_INACTIVE_TASK_STATUS); - } else { + } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Error) { terminalData.terminal.statusList.add(FAILED_INACTIVE_TASK_STATUS); + } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Warning) { + terminalData.terminal.statusList.add(WARNING_INACTIVE_TASK_STATUS); + } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Info) { + terminalData.terminal.statusList.add(INFO_INACTIVE_TASK_STATUS); } } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index bdbd89a39c..6fb548201f 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -42,7 +42,6 @@ import { import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Schemas } from 'vs/base/common/network'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { ILogService } from 'vs/platform/log/common/log'; @@ -51,6 +50,7 @@ import { IShellLaunchConfig, TerminalSettingId } from 'vs/platform/terminal/comm import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy'; import { TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTerminalStatus'; import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; interface TerminalData { terminal: ITerminalInstance; @@ -207,7 +207,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { private terminalService: ITerminalService, private terminalGroupService: ITerminalGroupService, private outputService: IOutputService, - private panelService: IPanelService, + private paneCompositeService: IPaneCompositePartService, private viewsService: IViewsService, private markerService: IMarkerService, private modelService: IModelService, private configurationResolverService: IConfigurationResolverService, @@ -328,15 +328,15 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (this.previousTerminalInstance) { this.terminalService.setActiveInstance(this.previousTerminalInstance); } - this.panelService.openPanel(this.previousPanelId); + this.paneCompositeService.openPaneComposite(this.previousPanelId, ViewContainerLocation.Panel); } else { - this.panelService.hideActivePanel(); + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); } this.previousPanelId = undefined; this.previousTerminalInstance = undefined; } else { if (isTerminalInPanel) { - this.previousPanelId = this.panelService.getActivePanel()?.getId(); + this.previousPanelId = this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)?.getId(); if (this.previousPanelId === TERMINAL_VIEW_ID) { this.previousTerminalInstance = this.terminalService.activeInstance ?? undefined; } @@ -1059,11 +1059,16 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { os, remoteAuthority: this.environmentService.remoteAuthority }); - const defaultConfig = { - shell: defaultProfile.path, - args: defaultProfile.args + shellLaunchConfig = { + name: terminalName, + description, + executable: defaultProfile.path, + args: defaultProfile.args, + icon: defaultProfile.icon, + env: { ...defaultProfile.env }, + color: defaultProfile.color, + waitOnExit }; - shellLaunchConfig = { name: terminalName, description, executable: defaultConfig.shell, args: defaultConfig.args, waitOnExit }; let shellSpecified: boolean = false; let shellOptions: ShellConfiguration | undefined = task.command.options && task.command.options.shell; if (shellOptions) { @@ -1189,7 +1194,11 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { shellLaunchConfig.cwd = isUNC(cwd) ? cwd : resources.toLocalResource(URI.from({ scheme: Schemas.file, path: cwd }), this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); } if (options.env) { - shellLaunchConfig.env = options.env; + if (shellLaunchConfig.env) { + shellLaunchConfig.env = { ...shellLaunchConfig.env, ...options.env }; + } else { + shellLaunchConfig.env = options.env; + } } shellLaunchConfig.isFeatureTerminal = true; shellLaunchConfig.useShellEnvironment = true; @@ -1239,14 +1248,11 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { args = resolvedResult.args; commandExecutable = CommandString.value(command); - this.currentTask.shellLaunchConfig = launchConfigs = (this.isRerun && this.lastTask) ? this.lastTask.getVerifiedTask().shellLaunchConfig : await this.createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit); + this.currentTask.shellLaunchConfig = launchConfigs = await this.createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit); if (launchConfigs === undefined) { return [undefined, undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; } } - if (this.currentTask.shellLaunchConfig) { - this.currentTask.shellLaunchConfig.icon = { id: 'tools' }; - } let prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated; let allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared; diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index 45157aeb60..6b3d0f083b 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -37,13 +37,13 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITaskService as ISqlTaskService } from 'sql/workbench/services/tasks/common/tasksService'; // {{SQL CARBON EDIT}} integration with tasks view panel import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; interface WorkspaceFolderConfigurationResult { workspaceFolder: IWorkspaceFolder; @@ -55,7 +55,7 @@ export class TaskService extends AbstractTaskService { constructor(@IConfigurationService configurationService: IConfigurationService, @IMarkerService markerService: IMarkerService, @IOutputService outputService: IOutputService, - @IPanelService panelService: IPanelService, + @IPaneCompositePartService paneCompositeService: IPaneCompositePartService, @IViewsService viewsService: IViewsService, @ICommandService commandService: ICommandService, @IEditorService editorService: IEditorService, @@ -89,7 +89,7 @@ export class TaskService extends AbstractTaskService { super(configurationService, markerService, outputService, - panelService, + paneCompositeService, viewsService, commandService, editorService, diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 6d66a0ba8b..3a5dae29de 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -8,7 +8,6 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWo import { LifecyclePhase, ILifecycleService, StartupKind } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -18,13 +17,14 @@ import { Disposable } from 'vs/base/common/lifecycle'; import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; import { configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITextFileService, ITextFileSaveEvent, ITextFileResolveEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { extname, basename, isEqual, isEqualOrParent } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { guessMimeTypes } from 'vs/base/common/mime'; import { hash } from 'vs/base/common/hash'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; type TelemetryData = { mimeType: string; @@ -50,20 +50,19 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr constructor( @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IActivityBarService activityBarService: IActivityBarService, @ILifecycleService lifecycleService: ILifecycleService, @IEditorService editorService: IEditorService, @IKeybindingService keybindingsService: IKeybindingService, @IWorkbenchThemeService themeService: IWorkbenchThemeService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IConfigurationService configurationService: IConfigurationService, - @IViewletService viewletService: IViewletService, + @IPaneCompositePartService paneCompositeService: IPaneCompositePartService, @ITextFileService textFileService: ITextFileService ) { super(); const { filesToOpenOrCreate, filesToDiff } = environmentService.configuration; - const activeViewlet = viewletService.getActiveViewlet(); + const activeViewlet = paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar); type WindowSizeFragment = { innerHeight: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -111,7 +110,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr customKeybindingsCount: keybindingsService.customKeybindingsCount(), theme: themeService.getColorTheme().id, language, - pinnedViewlets: activityBarService.getPinnedViewContainerIds(), + pinnedViewlets: paneCompositeService.getPinnedPaneCompositeIds(ViewContainerLocation.Sidebar), restoredViewlet: activeViewlet ? activeViewlet.getId() : undefined, restoredEditors: editorService.visibleEditors.length, startupKind: lifecycleService.startupKind @@ -194,7 +193,10 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr } private getTelemetryData(resource: URI, reason?: number): TelemetryData { - const ext = extname(resource); + let ext = extname(resource); + // Remove query parameters from the resource extension + const queryStringLocation = ext.indexOf('?'); + ext = queryStringLocation !== -1 ? ext.substr(0, queryStringLocation) : ext; const fileName = basename(resource); const path = resource.scheme === Schemas.file ? resource.fsPath : resource.path; const telemetryData = { diff --git a/src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts b/src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.ts new file mode 100644 index 0000000000..ac28175afb --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon.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 { Emitter } from 'vs/base/common/event'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { OperatingSystem } from 'vs/base/common/platform'; +import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm'; + +/** + * Provides extensions to the xterm object in a modular, testable way. + */ +export class LineDataEventAddon extends Disposable implements ITerminalAddon { + + private _xterm?: XTermTerminal; + private _isOsSet = false; + + private readonly _onLineData = this._register(new Emitter()); + readonly onLineData = this._onLineData.event; + + activate(xterm: XTermTerminal) { + this._xterm = xterm; + // Fire onLineData when a line feed occurs, taking into account wrapped lines + xterm.onLineFeed(() => { + const buffer = xterm.buffer; + const newLine = buffer.active.getLine(buffer.active.baseY + buffer.active.cursorY); + if (newLine && !newLine.isWrapped) { + this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY - 1); + } + }); + + // Fire onLineData when disposing object to flush last line + this._register(toDisposable(() => { + const buffer = xterm.buffer; + this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY); + })); + } + + setOperatingSystem(os: OperatingSystem) { + if (this._isOsSet || !this._xterm) { + return; + } + this._isOsSet = true; + + // Force line data to be sent when the cursor is moved, the main purpose for + // this is because ConPTY will often not do a line feed but instead move the + // cursor, in which case we still want to send the current line's data to tasks. + if (os === OperatingSystem.Windows) { + const xterm = this._xterm; + this._register(xterm.parser.registerCsiHandler({ final: 'H' }, () => { + const buffer = xterm.buffer; + this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY); + return false; + })); + } + } + + private _sendLineData(buffer: IBuffer, lineIndex: number): void { + let line = buffer.getLine(lineIndex); + if (!line) { + return; + } + let lineData = line.translateToString(true); + while (lineIndex > 0 && line.isWrapped) { + line = buffer.getLine(--lineIndex); + if (!line) { + break; + } + lineData = line.translateToString(false) + lineData; + } + this._onLineData.fire(lineData); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts index 9faed2212f..690eab3a63 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts @@ -45,7 +45,7 @@ export class TerminalLinkManager extends DisposableStore { private _widgetManager: TerminalWidgetManager | undefined; private _processCwd: string | undefined; private _standardLinkProviders: ILinkProvider[] = []; - private _standardLinkProvidersDisposables: IDisposable[] = []; + private _linkProvidersDisposables: IDisposable[] = []; constructor( private _xterm: Terminal, @@ -137,18 +137,23 @@ export class TerminalLinkManager extends DisposableStore { this._processCwd = processCwd; } + private _clearLinkProviders(): void { + dispose(this._linkProvidersDisposables); + this._linkProvidersDisposables = []; + } + private _registerStandardLinkProviders(): void { - dispose(this._standardLinkProvidersDisposables); - this._standardLinkProvidersDisposables = []; for (const p of this._standardLinkProviders) { - this._standardLinkProvidersDisposables.push(this._xterm.registerLinkProvider(p)); + this._linkProvidersDisposables.push(this._xterm.registerLinkProvider(p)); } } registerExternalLinkProvider(instance: ITerminalInstance, linkProvider: ITerminalExternalLinkProvider): IDisposable { + // Clear and re-register the standard link providers so they are a lower priority that the new one + this._clearLinkProviders(); const wrappedLinkProvider = this._instantiationService.createInstance(TerminalExternalLinkProviderAdapter, this._xterm, instance, linkProvider, this._wrapLinkHandler.bind(this), this._tooltipCallback.bind(this)); const newLinkProvider = this._xterm.registerLinkProvider(wrappedLinkProvider); - // Re-register the standard link providers so they are a lower priority that the new one + this._linkProvidersDisposables.push(newLinkProvider); this._registerStandardLinkProviders(); return newLinkProvider; } @@ -188,7 +193,10 @@ export class TerminalLinkManager extends DisposableStore { startLineNumber: lineColumnInfo.lineNumber, startColumn: lineColumnInfo.columnNumber }; - await this._editorService.openEditor({ resource: resolvedLink.uri, options: { pinned: true, selection } }); + await this._editorService.openEditor({ + resource: resolvedLink.uri, + options: { pinned: true, selection, revealIfOpened: true } + }); } private _handleHypertextLink(url: string): void { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts index 95b73c6e49..bdf7986154 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts @@ -20,22 +20,22 @@ const pathPrefix = '(\\.\\.?|\\~)'; const pathSeparatorClause = '\\/'; // '":; are allowed in paths but they are often separators so ignore them // Also disallow \\ to prevent a catastropic backtracking case #24795 -const excludedPathCharactersClause = '[^\\0\\s!$`&*()\\[\\]\'":;\\\\]'; +const excludedPathCharactersClause = '[^\\0\\s!`&*()\\[\\]\'":;\\\\]'; /** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */ export const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)+)'; export const winDrivePrefix = '(?:\\\\\\\\\\?\\\\)?[a-zA-Z]:'; const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)'; const winPathSeparatorClause = '(\\\\|\\/)'; -const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]\'":;]'; +const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!`&*()\\[\\]\'":;]'; /** A regex that matches paths in the form \\?\c:\foo c:\foo, ~\foo, .\foo, ..\foo, foo\bar */ export const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)'; /** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160, replacing space with nonBreakningSpace or space ASCII code - 32. */ export const lineAndColumnClause = [ - '((\\S*)", line ((\\d+)( column (\\d+))?))', // "(file path)", line 45 [see #40468] - '((\\S*)",((\\d+)(:(\\d+))?))', // "(file path)",45 [see #78205] + '((\\S*)[\'"], line ((\\d+)( column (\\d+))?))', // "(file path)", line 45 [see #40468] + '((\\S*)[\'"],((\\d+)(:(\\d+))?))', // "(file path)",45 [see #78205] '((\\S*) on line ((\\d+)(, column (\\d+))?))', // (file path) on line 8, column 13 '((\\S*):line ((\\d+)(, column (\\d+))?))', // (file path):line 8, column 13 '(([^\\s\\(\\)]*)(\\s?[\\(\\[](\\d+)(,\\s?(\\d+))?)[\\)\\]])', // (file path)(45), (file path) (45), (file path)(45,18), (file path) (45,18), (file path)(45, 18), (file path) (45, 18), also with [] diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts index 76e0e0a027..ddfd45c96d 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts @@ -131,7 +131,7 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider { private async _activate(link: string) { // Normalize the link and remove any leading ./ or ../ since quick access doesn't understand // that format - link = normalize(link).replace(/^(\.+\/)+/, ''); + link = normalize(link).replace(/^(\.+[\\/])+/, ''); // If any of the names of the folders in the workspace matches // a prefix of the link, remove that prefix and continue @@ -142,17 +142,31 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider { } }); + const sanitizedLink = link.replace(/:\d+(:\d+)?$/, ''); const results = await this._searchService.fileSearch( this._fileQueryBuilder.file(this._workspaceContextService.getWorkspace().folders, { - filePattern: link, + // Remove optional :row:col from the link as openEditor supports it + filePattern: sanitizedLink, maxResults: 2 }) ); // If there was exactly one match, open it if (results.results.length === 1) { - const match = results.results[0]; - await this._editorService.openEditor({ resource: match.resource, options: { pinned: true } }); + const match = link.match(/:(\d+)?(:(\d+))?$/); + const startLineNumber = match?.[1]; + const startColumn = match?.[3]; + await this._editorService.openEditor({ + resource: results.results[0].resource, + options: { + pinned: true, + revealIfOpened: true, + selection: startLineNumber ? { + startLineNumber: parseInt(startLineNumber), + startColumn: startColumn ? parseInt(startColumn) : 0 + } : undefined + } + }); return; } diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 80e256fe23..b1aa8164ab 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -32,6 +32,8 @@ .monaco-workbench .terminal-tab::before { font-family: 'codicon' !important; font-size: 16px !important; +} +.monaco-workbench .terminal-tab:not(.terminal-uri-icon)::before { background-image: none !important; } @@ -39,7 +41,9 @@ .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper { display: none; margin: 0 10px; - bottom: 2px; + height: 100%; + padding-bottom: 2px; + box-sizing: border-box; } .monaco-workbench .editor-instance .terminal-wrapper.active, @@ -47,11 +51,6 @@ display: block; } -.monaco-workbench .pane-body.integrated-terminal .terminal-wrapper.active { - position: absolute; - top: 0; -} - .monaco-workbench .editor-instance .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .terminal-wrapper, .monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .terminal-wrapper { margin-left: 20px; @@ -67,31 +66,26 @@ position: relative; } -.monaco-workbench .editor-instance .terminal-wrapper > div, +.monaco-workbench .editor-instance .terminal-wrapper > div, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper > div { height: 100%; + /* Align the viewport and canvases to the bottom of the panel */ + display: flex; + align-items: flex-end; } -.monaco-workbench .editor-instance .xterm-viewport, +.monaco-workbench .editor-instance .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport { box-sizing: border-box; margin-right: -10px; + /* Override xterm.js' width as we want to size the viewport to fill the panel so the scrollbar is on the right edge */ + width: auto !important; } -.monaco-workbench .editor-instance .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm-viewport, +.monaco-workbench .editor-instance .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm-viewport { margin-right: -20px; } -.monaco-workbench .pane-body.integrated-terminal canvas { - /* Align the viewport and canvases to the bottom of the panel */ - position: absolute; - right: -20px; - bottom: 0; - left: 0; - /* Disable upstream's style */ - top: auto; -} - .monaco-workbench .pane-body.integrated-terminal { font-variant-ligatures: none; } @@ -135,9 +129,6 @@ } .monaco-workbench .pane-body.integrated-terminal .xterm { - position: absolute; - bottom: 0; - left: 0; user-select: none; -webkit-user-select: none; } @@ -201,7 +192,7 @@ border-style: solid; } -.monaco-workbench .part.sidebar > .title > .title-actions .switch-terminal { +.monaco-workbench .part.sidebar > .title > .title-actions .switch-terminal { display: flex; align-items: center; font-size: 11px; @@ -238,6 +229,7 @@ .monaco-workbench .terminal-overflow-guard { overflow: hidden; position: relative; + height: 100%; } .monaco-workbench .pane-body.integrated-terminal .tabs-list-container { diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index b0f5b67dac..17451a419c 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -8,34 +8,39 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, ProcessCapability, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; import { RemoteTerminalChannelClient } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export class RemotePty extends Disposable implements ITerminalChildProcess { - private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter()); - readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; - private readonly _onDidChangeHasChildProcesses = this._register(new Emitter()); - readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; + private readonly _onDidChangeProperty = this._register(new Emitter>()); + readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter()); + readonly onProcessExit = this._onProcessExit.event; private _startBarrier: Barrier; private _inReplay = false; + private _properties: IProcessPropertyMap = { + cwd: '', + initialCwd: '', + fixedDimensions: { cols: undefined, rows: undefined }, + title: '', + shellType: undefined, + hasChildProcesses: true, + resolvedShellLaunchConfig: {}, + overrideDimensions: undefined + }; + + private _capabilities: ProcessCapability[] = []; + get capabilities(): ProcessCapability[] { return this._capabilities; } + get id(): number { return this._id; } constructor( @@ -117,45 +122,48 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { } async getInitialCwd(): Promise { - await this._startBarrier.wait(); - return this._remoteTerminalChannel.getInitialCwd(this._id); + return this._properties.initialCwd; } async getCwd(): Promise { - await this._startBarrier.wait(); - return this._remoteTerminalChannel.getCwd(this._id); + return this._properties.cwd || this._properties.initialCwd; + } + + async refreshProperty(type: ProcessPropertyType): Promise { + return this._remoteTerminalChannel.refreshProperty(this._id, type); + } + + async updateProperty(type: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise { + return this._remoteTerminalChannel.updateProperty(this._id, type, value); } handleData(e: string | IProcessDataEvent) { this._onProcessData.fire(e); } - processBinary(e: string): Promise { - return this._remoteTerminalChannel.processBinary(this._id, e); - } handleExit(e: number | undefined) { this._onProcessExit.fire(e); } + processBinary(e: string): Promise { + return this._remoteTerminalChannel.processBinary(this._id, e); + } handleReady(e: IProcessReadyEvent) { + this._capabilities = e.capabilities; this._onProcessReady.fire(e); } - handleTitleChanged(e: string) { - this._onProcessTitleChanged.fire(e); - } - handleShellTypeChanged(e: TerminalShellType | undefined) { - this._onProcessShellTypeChanged.fire(e); - } - handleOverrideDimensions(e: ITerminalDimensionsOverride | undefined) { - this._onProcessOverrideDimensions.fire(e); - } - handleResolvedShellLaunchConfig(e: IShellLaunchConfig) { - // Revive the cwd URI - if (e.cwd && typeof e.cwd !== 'string') { - e.cwd = URI.revive(e.cwd); + handleDidChangeProperty({ type, value }: IProcessProperty) { + switch (type) { + case ProcessPropertyType.Cwd: + this._properties.cwd = value; + break; + case ProcessPropertyType.InitialCwd: + this._properties.initialCwd = value; + break; + case ProcessPropertyType.ResolvedShellLaunchConfig: + if (value.cwd && typeof value.cwd !== 'string') { + value.cwd = URI.revive(value.cwd); + } } - this._onProcessResolvedShellLaunchConfig.fire(e); - } - handleDidChangeHasChildProcesses(e: boolean) { - this._onDidChangeHasChildProcesses.fire(e); + this._onDidChangeProperty.fire({ type, value }); } async handleReplay(e: IPtyHostProcessReplayEvent) { @@ -164,7 +172,7 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { for (const innerEvent of e.events) { if (innerEvent.cols !== 0 || innerEvent.rows !== 0) { // never override with 0x0 as that is a marker for an unknown initial size - this._onProcessOverrideDimensions.fire({ cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: { cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true } }); } const e: IProcessDataEvent = { data: innerEvent.data, trackCommit: true }; this._onProcessData.fire(e); @@ -175,7 +183,7 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { } // remove size override - this._onProcessOverrideDimensions.fire(undefined); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: undefined }); } handleOrphanQuestion() { diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index 01ace151c4..0a616de072 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -16,12 +16,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon } from 'vs/platform/terminal/common/terminal'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { RemotePty } from 'vs/workbench/contrib/terminal/browser/remotePty'; import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ICompleteTerminalConfiguration, RemoteTerminalChannelClient, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -49,6 +51,7 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal @ILogService private readonly _logService: ILogService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICommandService private readonly _commandService: ICommandService, + @IStorageService private readonly _storageService: IStorageService, @INotificationService notificationService: INotificationService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @@ -63,6 +66,11 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal this._remoteTerminalChannel = channel; channel.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event)); + channel.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)); + channel.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion()); + channel.onDidRequestDetach(e => this._onDidRequestDetach.fire(e)); + channel.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)); + channel.onDidChangeProperty(e => this._ptys.get(e.id)?.handleDidChangeProperty(e.property)); channel.onProcessExit(e => { const pty = this._ptys.get(e.id); if (pty) { @@ -70,15 +78,6 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal this._ptys.delete(e.id); } }); - channel.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)); - channel.onProcessTitleChanged(e => this._ptys.get(e.id)?.handleTitleChanged(e.event)); - channel.onProcessShellTypeChanged(e => this._ptys.get(e.id)?.handleShellTypeChanged(e.event)); - channel.onProcessOverrideDimensions(e => this._ptys.get(e.id)?.handleOverrideDimensions(e.event)); - channel.onProcessResolvedShellLaunchConfig(e => this._ptys.get(e.id)?.handleResolvedShellLaunchConfig(e.event)); - channel.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)); - channel.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion()); - channel.onDidRequestDetach(e => this._onDidRequestDetach.fire(e)); - channel.onProcessDidChangeHasChildProcesses(e => this._ptys.get(e.id)?.handleDidChangeHasChildProcesses(e.event)); const allowedCommands = ['_remoteCLI.openExternal', '_remoteCLI.windowOpen', '_remoteCLI.getSystemStatus', '_remoteCLI.manageExtensions']; channel.onExecuteCommand(async e => { @@ -172,6 +171,15 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal return this._remoteTerminalChannel.acceptDetachInstanceReply(requestId, persistentProcessId); } + async persistTerminalState(): Promise { + if (!this._remoteTerminalChannel) { + throw new Error(`Cannot persist terminal state when there is no remote!`); + } + const ids = Array.from(this._ptys.keys()); + const serialized = await this._remoteTerminalChannel.serializeTerminalState(ids); + this._storageService.store(TerminalStorageKeys.TerminalBufferState, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + async createProcess(shellLaunchConfig: IShellLaunchConfig, configuration: ICompleteTerminalConfiguration, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, unicodeVersion: '6' | '11', shouldPersist: boolean): Promise { if (!this._remoteTerminalChannel) { throw new Error(`Cannot create remote terminal when there is no remote!`); @@ -235,11 +243,16 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal workspaceName: termDto.workspaceName, icon: termDto.icon, color: termDto.color, - isOrphan: termDto.isOrphan + isOrphan: termDto.isOrphan, + fixedDimensions: termDto.fixedDimensions }; }); } + async updateProperty(id: number, property: ProcessPropertyType, value: any): Promise { + await this._remoteTerminalChannel?.updateProperty(id, property, value); + } + async updateTitle(id: number, title: string): Promise { await this._remoteTerminalChannel?.updateTitle(id, title); } @@ -297,6 +310,24 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal throw new Error(`Cannot call getActiveInstanceId when there is no remote`); } + // Revive processes if needed + const serializedState = this._storageService.get(TerminalStorageKeys.TerminalBufferState, StorageScope.WORKSPACE); + if (serializedState) { + try { + await this._remoteTerminalChannel.reviveTerminalProcesses(serializedState); + this._storageService.remove(TerminalStorageKeys.TerminalBufferState, StorageScope.WORKSPACE); + // If reviving processes, send the terminal layout info back to the pty host as it + // will not have been persisted on application exit + const layoutInfo = this._storageService.get(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE); + if (layoutInfo) { + await this._remoteTerminalChannel.setTerminalLayoutInfo(JSON.parse(layoutInfo)); + this._storageService.remove(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE); + } + } catch { + // no-op + } + } + return this._remoteTerminalChannel.getTerminalLayoutInfo(); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 73c2f215da..545be97b85 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -13,7 +13,6 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight, KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import * as panel from 'vs/workbench/browser/panel'; import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; import { registerTerminalActions, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; @@ -94,8 +93,7 @@ const VIEW_CONTAINER = Registry.as(ViewContainerExtensi storageId: TERMINAL_VIEW_ID, hideIfEmpty: true, order: 3, -}, ViewContainerLocation.Panel, { donotRegisterOpenCommand: true }); -Registry.as(panel.Extensions.Panels).setDefaultPanelId(TERMINAL_VIEW_ID); +}, ViewContainerLocation.Panel, { donotRegisterOpenCommand: true, isDefault: true }); Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ id: TERMINAL_VIEW_ID, name: nls.localize('terminal', "Terminal"), @@ -107,8 +105,8 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews id: TerminalCommandId.Toggle, mnemonicTitle: nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal"), keybindings: { - primary: KeyMod.CtrlCmd | KeyCode.US_BACKTICK, - mac: { primary: KeyMod.WinCtrl | KeyCode.US_BACKTICK } + primary: KeyMod.CtrlCmd | KeyCode.Backquote, + mac: { primary: KeyMod.WinCtrl | KeyCode.Backquote } }, order: 3 } @@ -141,7 +139,7 @@ const CTRL_LETTER_OFFSET = 64; if (isWindows) { registerSendSequenceKeybinding(String.fromCharCode('V'.charCodeAt(0) - CTRL_LETTER_OFFSET), { // ctrl+v when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(TerminalContextKeyStrings.ShellType, WindowsShellType.PowerShell), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - primary: KeyMod.CtrlCmd | KeyCode.KEY_V + primary: KeyMod.CtrlCmd | KeyCode.KeyV }); } @@ -149,7 +147,7 @@ if (isWindows) { if (isIOS) { registerSendSequenceKeybinding(String.fromCharCode('C'.charCodeAt(0) - CTRL_LETTER_OFFSET), { // ctrl+c when: ContextKeyExpr.and(TerminalContextKeys.focus), - primary: KeyMod.WinCtrl | KeyCode.KEY_C + primary: KeyMod.WinCtrl | KeyCode.KeyC }); } @@ -167,7 +165,7 @@ if (isWindows) { }); } // Delete word right: alt+d -registerSendSequenceKeybinding('\x1bd', { +registerSendSequenceKeybinding('\u000d', { primary: KeyMod.CtrlCmd | KeyCode.Delete, mac: { primary: KeyMod.Alt | KeyCode.Delete } }); @@ -185,7 +183,22 @@ registerSendSequenceKeybinding(String.fromCharCode('E'.charCodeAt(0) - 64), { }); // Break: ctrl+C registerSendSequenceKeybinding(String.fromCharCode('C'.charCodeAt(0) - 64), { - mac: { primary: KeyMod.CtrlCmd | KeyCode.US_DOT } + mac: { primary: KeyMod.CtrlCmd | KeyCode.Period } +}); +// NUL: ctrl+shift+2 +registerSendSequenceKeybinding('\u0000', { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit2, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Digit2 } +}); +// RS: ctrl+shift+6 +registerSendSequenceKeybinding('\u001e', { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit6, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Digit6 } +}); +// US (Undo): ctrl+/ +registerSendSequenceKeybinding('\u001f', { + primary: KeyMod.CtrlCmd | KeyCode.Slash, + mac: { primary: KeyMod.WinCtrl | KeyCode.Slash } }); setupTerminalCommands(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 9d98ac7016..970e511366 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, ITerminalProfileType, TerminalLocation, ICreateContributedTerminalProfileOptions } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ICreateContributedTerminalProfileOptions, ProcessPropertyType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; import { ICommandTracker, INavigationMode, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; @@ -109,7 +109,7 @@ export interface ITerminalService extends ITerminalInstanceHost { isProcessSupportRegistered: boolean; readonly connectionState: TerminalConnectionState; readonly availableProfiles: ITerminalProfile[]; - readonly allProfiles: ITerminalProfileType[] | undefined; + readonly contributedProfiles: IExtensionTerminalProfile[]; readonly profilesReady: Promise; readonly defaultLocation: TerminalLocation; @@ -188,6 +188,12 @@ export interface ITerminalService extends ITerminalInstanceHost { getFindHost(instance?: ITerminalInstance): ITerminalFindHost; getDefaultProfileName(): string; + resolveLocation(location?: ITerminalLocationOptions): TerminalLocation | undefined + setNativeDelegate(nativeCalls: ITerminalServiceNativeDelegate): void; +} + +export interface ITerminalServiceNativeDelegate { + getWindowCount(): Promise; } /** @@ -400,6 +406,14 @@ export interface ITerminalInstance { readonly icon?: TerminalIcon; readonly color?: string; + readonly processName: string; + readonly sequence?: string; + readonly staticTitle?: string; + readonly workspaceFolder?: string; + readonly cwd?: string; + readonly initialCwd?: string; + readonly capabilities: ProcessCapability[]; + readonly statusList: ITerminalStatusList; /** @@ -562,6 +576,9 @@ export interface ITerminalInstance { readonly navigationMode: INavigationMode | undefined; + description: string | undefined; + + userHome: string | undefined /** * Shows the environment information hover if the widget exists. */ @@ -722,13 +739,27 @@ export interface ITerminalInstance { relaunch(): void; /** - * Sets the title of the terminal instance. + * Sets the title and description of the terminal instance's label. */ - setTitle(title: string, eventSource: TitleEventSource): void; + refreshTabLabels(title: string, eventSource: TitleEventSource): void; waitForTitle(): Promise; - setDimensions(dimensions: ITerminalDimensions): void; + /** + * Sets the terminal instance's dimensions to the values provided via the onDidOverrideDimensions event, + * which allows overriding the the regular dimensions (fit to the size of the panel). + */ + setOverrideDimensions(dimensions: ITerminalDimensions): void; + + /** + * Sets the terminal instance's dimensions to the values provided via quick input. + */ + setFixedDimensions(): Promise; + + /** + * Toggles terminal line wrapping. + */ + toggleSizeToContentWidth(): Promise; addDisposable(disposable: IDisposable): void; @@ -737,6 +768,8 @@ export interface ITerminalInstance { getInitialCwd(): Promise; getCwd(): Promise; + refreshProperty(type: ProcessPropertyType): Promise; + /** * @throws when called before xterm.js is ready. */ diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts index f48fac7702..ec5d4c9d54 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts @@ -17,5 +17,5 @@ KeybindingsRegistry.registerKeybindingRule({ id: TerminalCommandId.New, weight: KeybindingWeight.WorkbenchContrib, when: undefined, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 428d8a24cc..3538840b84 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -45,6 +45,7 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { isAbsolute } from 'vs/base/common/path'; export const switchTerminalActionViewItemSeparator = '─────────'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -146,11 +147,10 @@ export function registerTerminalActions() { precondition: TerminalContextKeys.processSupported }); } - async run(accessor: ServicesAccessor) { + async run(accessor: ServicesAccessor, args?: unknown) { const terminalService = accessor.get(ITerminalService); - const instance = await terminalService.createTerminal({ - location: TerminalLocation.Editor - }); + const options = (typeof args === 'object' && args && 'location' in args) ? args as ICreateTerminalOptions : { location: TerminalLocation.Editor }; + const instance = await terminalService.createTerminal(options); instance.focusWhenReady(); } }); @@ -181,7 +181,7 @@ export function registerTerminalActions() { title: terminalStrings.moveToEditor, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -197,7 +197,7 @@ export function registerTerminalActions() { title: terminalStrings.moveToEditor, f1: false, category, - precondition: ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.isOpen) + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen) }); } async run(accessor: ServicesAccessor) { @@ -219,7 +219,8 @@ export function registerTerminalActions() { id: TerminalCommandId.MoveToTerminalPanel, title: terminalStrings.moveToTerminalPanel, f1: true, - category + category, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor, resource: unknown) { @@ -235,7 +236,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.showTabs', "Show Tabs"), original: 'Show Tabs' }, f1: false, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -260,7 +261,7 @@ export function registerTerminalActions() { when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -286,7 +287,7 @@ export function registerTerminalActions() { when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -308,7 +309,7 @@ export function registerTerminalActions() { when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -328,7 +329,7 @@ export function registerTerminalActions() { when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -347,7 +348,7 @@ export function registerTerminalActions() { when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -366,7 +367,7 @@ export function registerTerminalActions() { when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -380,7 +381,7 @@ export function registerTerminalActions() { title: terminalStrings.focus, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -402,11 +403,11 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backslash, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.or(TerminalContextKeys.tabsFocus, TerminalContextKeys.focus), }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -420,11 +421,11 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.focusNext', "Focus Next Terminal Group"), original: 'Focus Next Terminal Group' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.PageDown, mac: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketRight }, when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.editorFocus.negate()), weight: KeybindingWeight.WorkbenchContrib @@ -444,11 +445,11 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.focusPrevious', "Focus Previous Terminal Group"), original: 'Focus Previous Terminal Group' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.PageUp, mac: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketLeft }, when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.editorFocus.negate()), weight: KeybindingWeight.WorkbenchContrib @@ -500,7 +501,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.runActiveFile', "Run Active File In Active Terminal"), original: 'Run Active File In Active Terminal' }, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -544,10 +545,10 @@ export function registerTerminalActions() { keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, - when: TerminalContextKeys.focus, + when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -567,7 +568,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -584,10 +585,10 @@ export function registerTerminalActions() { keybinding: { primary: KeyMod.CtrlCmd | KeyCode.End, linux: { primary: KeyMod.Shift | KeyCode.End }, - when: TerminalContextKeys.focus, + when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -604,10 +605,10 @@ export function registerTerminalActions() { keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }, - when: TerminalContextKeys.focus, + when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -627,7 +628,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -644,10 +645,10 @@ export function registerTerminalActions() { keybinding: { primary: KeyMod.CtrlCmd | KeyCode.Home, linux: { primary: KeyMod.Shift | KeyCode.Home }, - when: TerminalContextKeys.focus, + when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -729,7 +730,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.textSelected, TerminalContextKeys.notFindVisible), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -746,11 +747,25 @@ export function registerTerminalActions() { title: terminalStrings.changeIcon, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) + }); + } + async run(accessor: ServicesAccessor, resource: unknown) { + doWithInstance(accessor, resource)?.changeIcon(); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.ChangeIconPanel, + title: terminalStrings.changeIcon, + f1: true, + category, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { - return accessor.get(ITerminalService).activeInstance?.changeIcon(); + return accessor.get(ITerminalGroupService).activeInstance?.changeIcon(); } }); registerAction2(class extends Action2 { @@ -760,7 +775,7 @@ export function registerTerminalActions() { title: terminalStrings.changeIcon, f1: false, category, - precondition: ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.tabsSingularSelection) + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.tabsSingularSelection) }); } async run(accessor: ServicesAccessor) { @@ -774,11 +789,25 @@ export function registerTerminalActions() { title: terminalStrings.changeColor, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) + }); + } + async run(accessor: ServicesAccessor, resource: unknown) { + doWithInstance(accessor, resource)?.changeColor(); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.ChangeColorPanel, + title: terminalStrings.changeColor, + f1: true, + category, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { - return accessor.get(ITerminalService).activeInstance?.changeColor(); + return accessor.get(ITerminalGroupService).activeInstance?.changeColor(); } }); registerAction2(class extends Action2 { @@ -788,7 +817,7 @@ export function registerTerminalActions() { title: terminalStrings.changeColor, f1: false, category, - precondition: ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.tabsSingularSelection) + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.tabsSingularSelection) }); } async run(accessor: ServicesAccessor) { @@ -802,11 +831,26 @@ export function registerTerminalActions() { title: terminalStrings.rename, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) + }); + } + async run(accessor: ServicesAccessor, resource: unknown) { + doWithInstance(accessor, resource)?.rename(); + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.RenamePanel, + title: terminalStrings.rename, + f1: false, + category, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { - return accessor.get(ITerminalService).activeInstance?.rename(); + return accessor.get(ITerminalGroupService).activeInstance?.rename(); } }); registerAction2(class extends Action2 { @@ -824,7 +868,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.and(TerminalContextKeys.tabsFocus), weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.tabsSingularSelection), + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.tabsSingularSelection), }); } async run(accessor: ServicesAccessor) { @@ -860,11 +904,11 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + primary: KeyMod.CtrlCmd | KeyCode.KeyF, when: ContextKeyExpr.or(TerminalContextKeys.findFocus, TerminalContextKeys.focus), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -884,7 +928,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.findVisible), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -965,7 +1009,7 @@ export function registerTerminalActions() { title: { value: localize('quickAccessTerminal', "Switch Active Terminal"), original: 'Switch Active Terminal' }, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -984,7 +1028,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1006,7 +1050,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1028,7 +1072,7 @@ export function registerTerminalActions() { when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1050,7 +1094,7 @@ export function registerTerminalActions() { when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1067,7 +1111,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.selectToPreviousLine', "Select To Previous Line"), original: 'Select To Previous Line' }, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1084,7 +1128,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.selectToNextLine', "Select To Next Line"), original: 'Select To Next Line' }, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1205,7 +1249,7 @@ export function registerTerminalActions() { } }] }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor, args?: { name?: string }) { @@ -1214,7 +1258,7 @@ export function registerTerminalActions() { notificationService.warn(localize('workbench.action.terminal.renameWithArg.noName', "No name argument provided")); return; } - accessor.get(ITerminalService).activeInstance?.setTitle(args.name, TitleEventSource.Api); + accessor.get(ITerminalService).activeInstance?.refreshTabLabels(args.name, TitleEventSource.Api); } }); registerAction2(class extends Action2 { @@ -1225,12 +1269,12 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - primary: KeyMod.Alt | KeyCode.KEY_R, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R }, + primary: KeyMod.Alt | KeyCode.KeyR, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyR }, when: ContextKeyExpr.or(TerminalContextKeys.focus, TerminalContextKeys.findFocus), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1248,12 +1292,12 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - primary: KeyMod.Alt | KeyCode.KEY_W, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_W }, + primary: KeyMod.Alt | KeyCode.KeyW, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyW }, when: ContextKeyExpr.or(TerminalContextKeys.focus, TerminalContextKeys.findFocus), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1271,12 +1315,12 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - primary: KeyMod.Alt | KeyCode.KEY_C, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C }, + primary: KeyMod.Alt | KeyCode.KeyC, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC }, when: ContextKeyExpr.or(TerminalContextKeys.focus, TerminalContextKeys.findFocus), weight: KeybindingWeight.WorkbenchContrib }, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1296,7 +1340,7 @@ export function registerTerminalActions() { keybinding: [ { primary: KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] }, + mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyG, secondary: [KeyCode.F3] }, when: ContextKeyExpr.or(TerminalContextKeys.focus, TerminalContextKeys.findFocus), weight: KeybindingWeight.WorkbenchContrib }, @@ -1306,7 +1350,7 @@ export function registerTerminalActions() { weight: KeybindingWeight.WorkbenchContrib } ], - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1323,7 +1367,7 @@ export function registerTerminalActions() { keybinding: [ { primary: KeyMod.Shift | KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] }, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG, secondary: [KeyMod.Shift | KeyCode.F3] }, when: ContextKeyExpr.or(TerminalContextKeys.focus, TerminalContextKeys.findFocus), weight: KeybindingWeight.WorkbenchContrib }, @@ -1333,7 +1377,7 @@ export function registerTerminalActions() { weight: KeybindingWeight.WorkbenchContrib } ], - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } run(accessor: ServicesAccessor) { @@ -1349,7 +1393,7 @@ export function registerTerminalActions() { category, keybinding: [ { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF, when: ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.focus, TerminalContextKeys.textSelected), weight: KeybindingWeight.WorkbenchContrib + 50 } @@ -1399,11 +1443,11 @@ export function registerTerminalActions() { category, precondition: TerminalContextKeys.processSupported, keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_5, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit5, weight: KeybindingWeight.WorkbenchContrib, mac: { - primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH, - secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_5] + primary: KeyMod.CtrlCmd | KeyCode.Backslash, + secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Digit5] }, when: TerminalContextKeys.focus }, @@ -1452,10 +1496,10 @@ export function registerTerminalActions() { category, precondition: TerminalContextKeys.processSupported, keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_5, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit5, mac: { - primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH, - secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_5] + primary: KeyMod.CtrlCmd | KeyCode.Backslash, + secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Digit5] }, weight: KeybindingWeight.WorkbenchContrib, when: TerminalContextKeys.tabsFocus @@ -1485,7 +1529,7 @@ export function registerTerminalActions() { title: terminalStrings.unsplit, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -1499,7 +1543,7 @@ export function registerTerminalActions() { title: terminalStrings.unsplit, f1: false, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -1521,7 +1565,7 @@ export function registerTerminalActions() { id: TerminalCommandId.JoinInstance, title: { value: localize('workbench.action.terminal.joinInstance', "Join Terminals"), original: 'Join Terminals' }, category, - precondition: ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.tabsSingularSelection.toNegated()) + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.tabsSingularSelection.toNegated()) }); } async run(accessor: ServicesAccessor) { @@ -1560,7 +1604,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.selectAll', "Select All"), original: 'Select All' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), keybinding: [{ // Don't use ctrl+a by default as that would override the common go to start // of prompt shell binding @@ -1568,7 +1612,7 @@ export function registerTerminalActions() { // Technically this doesn't need to be here as it will fall back to this // behavior anyway when handed to xterm.js, having this handled by VS Code // makes it easier for users to see how it works though. - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_A }, + mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyA }, weight: KeybindingWeight.WorkbenchContrib, when: TerminalContextKeys.focus }] @@ -1585,11 +1629,11 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.new', "Create New Terminal"), original: 'Create New Terminal' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported), icon: Codicon.plus, keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKTICK, - mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_BACKTICK }, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backquote, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Backquote }, weight: KeybindingWeight.WorkbenchContrib }, description: { @@ -1608,6 +1652,7 @@ export function registerTerminalActions() { const terminalGroupService = accessor.get(ITerminalGroupService); const workspaceContextService = accessor.get(IWorkspaceContextService); const commandService = accessor.get(ICommandService); + const configurationService = accessor.get(IConfigurationService); const folders = workspaceContextService.getWorkspace().folders; if (eventOrOptions && eventOrOptions instanceof MouseEvent && (eventOrOptions.altKey || eventOrOptions.ctrlKey)) { const activeInstance = terminalService.activeInstance; @@ -1620,7 +1665,7 @@ export function registerTerminalActions() { if (terminalService.isProcessSupportRegistered) { eventOrOptions = !eventOrOptions || eventOrOptions instanceof MouseEvent ? {} : eventOrOptions; - eventOrOptions.location = eventOrOptions.location || terminalService.defaultLocation; + let instance: ITerminalInstance | undefined; if (folders.length <= 1) { // Allow terminal service to handle the path when there is only a @@ -1630,12 +1675,23 @@ export function registerTerminalActions() { const options: IPickOptions = { placeHolder: localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") }; - const workspace = await commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]); + const workspace = await commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]); if (!workspace) { // Don't create the instance if the workspace picker was canceled return; } eventOrOptions.cwd = workspace.uri; + const cwdConfig = configurationService.getValue(TerminalSettingId.Cwd, { resource: workspace.uri }); + if (typeof cwdConfig === 'string' && cwdConfig.length > 0) { + if (isAbsolute(cwdConfig)) { + eventOrOptions.cwd = URI.from({ + scheme: workspace.uri.scheme, + path: cwdConfig + }); + } else { + eventOrOptions.cwd = URI.joinPath(workspace.uri, cwdConfig); + } + } instance = await terminalService.createTerminal(eventOrOptions); } terminalService.setActiveInstance(instance); @@ -1654,7 +1710,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.kill', "Kill the Active Terminal Instance"), original: 'Kill the Active Terminal Instance' }, f1: true, category, - precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.isOpen), + precondition: ContextKeyExpr.or(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen), icon: Codicon.trash }); } @@ -1678,10 +1734,10 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.killEditor', "Kill the Active Terminal in Editor Area"), original: 'Kill the Active Terminal in Editor Area' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_W, - win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KEY_W] }, + primary: KeyMod.CtrlCmd | KeyCode.KeyW, + win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KeyW] }, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(TerminalContextKeys.focus, ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeTerminal), TerminalContextKeys.editorFocus) } @@ -1700,7 +1756,7 @@ export function registerTerminalActions() { title: terminalStrings.kill, f1: false, category, - precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.isOpen), + precondition: ContextKeyExpr.or(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen), keybinding: { primary: KeyCode.Delete, mac: { @@ -1734,10 +1790,10 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.clear', "Clear"), original: 'Clear' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), keybinding: [{ primary: 0, - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_K }, + mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyK }, // Weight is higher than work workbench contributions so the keybinding remains // highest priority when chords are registered afterwards weight: KeybindingWeight.WorkbenchContrib + 1, @@ -1785,7 +1841,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.openSettings', "Configure Terminal Settings"), original: 'Configure Terminal Settings' }, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -1793,6 +1849,53 @@ export function registerTerminalActions() { } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.SetDimensions, + title: { value: localize('workbench.action.terminal.setFixedDimensions', "Set Fixed Dimensions"), original: 'Set Fixed Dimensions' }, + f1: true, + category, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen) + }); + } + async run(accessor: ServicesAccessor) { + await accessor.get(ITerminalService).doWithActiveInstance(t => t.setFixedDimensions()); + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.SizeToContentWidth, + title: { value: localize('workbench.action.terminal.sizeToContentWidth', "Toggle Size to Content Width"), original: 'Toggle Size to Content Width' }, + f1: true, + category, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen, TerminalContextKeys.focus), + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyZ, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + async run(accessor: ServicesAccessor) { + await accessor.get(ITerminalService).doWithActiveInstance(t => t.toggleSizeToContentWidth()); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.SizeToContentWidthInstance, + title: terminalStrings.toggleSizeToContentWidth, + f1: false, + category, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus) + }); + } + async run(accessor: ServicesAccessor) { + return getSelectedInstances(accessor)?.[0].toggleSizeToContentWidth(); + } + }); // Some commands depend on platform features if (BrowserFeatures.clipboard.writeText) { registerAction2(class extends Action2 { @@ -1803,11 +1906,11 @@ export function registerTerminalActions() { f1: true, category, // TODO: Why is copy still showing up when text isn't selected? - precondition: ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.textSelected), + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected), keybinding: [{ - primary: KeyMod.CtrlCmd | KeyCode.KEY_C, - win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C] }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C }, + primary: KeyMod.CtrlCmd | KeyCode.KeyC, + win: { primary: KeyMod.CtrlCmd | KeyCode.KeyC, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC] }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC }, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(TerminalContextKeys.textSelected, TerminalContextKeys.focus) }] @@ -1827,11 +1930,11 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.paste', "Paste into Active Terminal"), original: 'Paste into Active Terminal' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), keybinding: [{ - primary: KeyMod.CtrlCmd | KeyCode.KEY_V, - win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V] }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V }, + primary: KeyMod.CtrlCmd | KeyCode.KeyV, + win: { primary: KeyMod.CtrlCmd | KeyCode.KeyV, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyV] }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyV }, weight: KeybindingWeight.WorkbenchContrib, when: TerminalContextKeys.focus }], @@ -1851,7 +1954,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.pasteSelection', "Paste Selection into Active Terminal"), original: 'Paste Selection into Active Terminal' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), keybinding: [{ linux: { primary: KeyMod.Shift | KeyCode.Insert }, weight: KeybindingWeight.WorkbenchContrib, @@ -1873,7 +1976,7 @@ export function registerTerminalActions() { title: switchTerminalTitle, f1: false, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor, item?: string) { @@ -1964,7 +2067,7 @@ export function validateTerminalName(name: string): { content: string, severity: function convertOptionsOrProfileToOptions(optionsOrProfile?: ICreateTerminalOptions | ITerminalProfile): ICreateTerminalOptions | undefined { if (typeof optionsOrProfile === 'object' && 'profileName' in optionsOrProfile) { - return { config: optionsOrProfile as ITerminalProfile }; + return { config: optionsOrProfile as ITerminalProfile, location: (optionsOrProfile as ICreateTerminalOptions).location }; } return optionsOrProfile; } @@ -1982,7 +2085,7 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { title: { value: localize('workbench.action.terminal.newWithProfile', "Create New Terminal (With Profile)"), original: 'Create New Terminal (With Profile)' }, f1: true, category, - precondition: TerminalContextKeys.processSupported, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported), description: { description: 'workbench.action.terminal.newWithProfile', args: [{ @@ -2005,12 +2108,20 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { } async run(accessor: ServicesAccessor, eventOrOptionsOrProfile: MouseEvent | ICreateTerminalOptions | ITerminalProfile | { profileName: string } | undefined, profile?: ITerminalProfile) { const terminalService = accessor.get(ITerminalService); + + if (!terminalService.isProcessSupportRegistered) { + return; + } + const terminalGroupService = accessor.get(ITerminalGroupService); const workspaceContextService = accessor.get(IWorkspaceContextService); const commandService = accessor.get(ICommandService); let event: MouseEvent | PointerEvent | KeyboardEvent | undefined; let options: ICreateTerminalOptions | undefined; + let instance: ITerminalInstance | undefined; + let cwd: string | URI | undefined; + if (typeof eventOrOptionsOrProfile === 'object' && eventOrOptionsOrProfile && 'profileName' in eventOrOptionsOrProfile) { const config = terminalService.availableProfiles.find(profile => profile.profileName === eventOrOptionsOrProfile.profileName); if (!config) { @@ -2024,48 +2135,53 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { options = convertOptionsOrProfileToOptions(eventOrOptionsOrProfile as ICreateTerminalOptions | ITerminalProfile); // {{SQL CARBON EDIT}} Fix typing compile error } - const folders = workspaceContextService.getWorkspace().folders; + // split terminal if (event && (event.altKey || event.ctrlKey)) { const parentTerminal = terminalService.activeInstance; if (parentTerminal) { - const cwd = await getCwdForSplit(terminalService.configHelper, parentTerminal); + cwd = await getCwdForSplit(terminalService.configHelper, parentTerminal); await terminalService.createTerminal({ location: { parentTerminal }, config: options?.config, cwd }); return; } } - if (terminalService.isProcessSupportRegistered) { - let instance: ITerminalInstance | undefined; - let cwd: string | URI | undefined; - if (folders.length > 1) { - // multi-root workspace, create root picker - const options: IPickOptions = { - placeHolder: localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") - }; - const workspace = await commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]); - if (!workspace) { - // Don't create the instance if the workspace picker was canceled - return; - } - cwd = workspace.uri; + const folders = workspaceContextService.getWorkspace().folders; + if (folders.length > 1) { + // multi-root workspace, create root picker + const options: IPickOptions = { + placeHolder: localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") + }; + const workspace = await commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]); + if (!workspace) { + // Don't create the instance if the workspace picker was canceled + return; } + cwd = workspace.uri; + } - if (options) { - instance = await terminalService.createTerminal(options); + if (options) { + options.cwd = cwd; + instance = await terminalService.createTerminal(options); + } else { + instance = await terminalService.showProfileQuickPick('createInstance', cwd); + } + + if (instance) { + terminalService.setActiveInstance(instance); + if (instance.target === TerminalLocation.Editor) { + await instance.focusWhenReady(true); } else { - instance = await terminalService.showProfileQuickPick('createInstance', cwd); + await terminalGroupService.showPanel(true); } - - if (instance) { - terminalService.setActiveInstance(instance); - if (instance.target === TerminalLocation.Editor) { - await instance.focusWhenReady(true); - } else { - await terminalGroupService.showPanel(true); - } - } - } } }); } + +/** doc */ +function doWithInstance(accessor: ServicesAccessor, resource: unknown): ITerminalInstance | undefined { + const terminalService = accessor.get(ITerminalService); + const castedResource = URI.isUri(resource) ? resource : undefined; + const instance = terminalService.getInstanceFromResource(castedResource) || terminalService.activeInstance; + return instance; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index fd5dab1a26..db4d732c24 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -22,7 +22,7 @@ import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; import { isLinux, isWindows } from 'vs/base/common/platform'; const MINIMUM_FONT_SIZE = 6; -const MAXIMUM_FONT_SIZE = 25; +const MAXIMUM_FONT_SIZE = 100; /** * Encapsulates terminal configuration logic, the primary purpose of this file is so that platform diff --git a/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts b/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts index e87a66680a..9e78dd4ba6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; +import { IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/common/decorations'; import { Event, Emitter } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; import { getColorForSeverity } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 7cb8418f12..dbca092da0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -24,14 +24,13 @@ import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/termi import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget'; import { getTerminalActionBarArgs } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; import { ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { openContextMenu } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; const findWidgetSelector = '.simple-find-part-wrapper'; @@ -63,7 +62,6 @@ export class TerminalEditor extends EditorPane { @IStorageService storageService: IStorageService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, - @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, @ITerminalService private readonly _terminalService: ITerminalService, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -77,7 +75,7 @@ export class TerminalEditor extends EditorPane { this._findState = new FindReplaceState(); this._findWidget = instantiationService.createInstance(TerminalFindWidget, this._findState); this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, _contextKeyService)); - this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalInstanceContext, _contextKeyService)); + this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalEditorInstanceContext, _contextKeyService)); } override async setInput(newInput: TerminalEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken) { @@ -88,7 +86,7 @@ export class TerminalEditor extends EditorPane { if (this._lastDimension) { this.layout(this._lastDimension); } - this._editorInput.terminalInstance?.setVisible(true); + this._editorInput.terminalInstance?.setVisible(this.isVisible()); if (this._editorInput.terminalInstance) { // since the editor does not monitor focus changes, for ex. between the terminal // panel and the editors, this is needed so that the active instance gets set @@ -100,6 +98,7 @@ export class TerminalEditor extends EditorPane { override clearInput(): void { super.clearInput(); + this._editorInput?.terminalInstance?.detachFromElement(); this._editorInput = undefined; } @@ -201,7 +200,8 @@ export class TerminalEditor extends EditorPane { override getActionViewItem(action: IAction): IActionViewItem | undefined { switch (action.id) { case TerminalCommandId.CreateWithProfileButton: { - const actions = getTerminalActionBarArgs(TerminalLocation.Editor, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalContributionService.terminalProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const location = { viewColumn: ACTIVE_GROUP }; + const actions = getTerminalActionBarArgs(location, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); const button = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, actions.primaryAction, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}); return button; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts index 7918295798..d0cf2bc2ac 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import Severity from 'vs/base/common/severity'; import { dispose, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IEditorIdentifier, IEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ITerminalInstance, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -70,7 +70,7 @@ export class TerminalEditorInput extends EditorInput { }); } - override copy(): IEditorInput { + override copy(): EditorInput { const instance = this._terminalInstanceService.createInstance(this._copyLaunchConfig || {}, TerminalLocation.Editor); instance.focusWhenReady(); this._copyLaunchConfig = undefined; @@ -78,7 +78,7 @@ export class TerminalEditorInput extends EditorInput { } /** - * Sets the launch config to use for the next call to IEditorInput.copy, which will be used when + * Sets the launch config to use for the next call to EditorInput.copy, which will be used when * the editor's split command is run. */ setCopyLaunchConfig(launchConfig: IShellLaunchConfig) { @@ -221,6 +221,10 @@ export class TerminalEditorInput extends EditorInput { } } + public override getDescription(): string | undefined { + return this._terminalInstance?.description || this._terminalInstance?.shellLaunchConfig.description; + } + public override toUntyped(): IUntypedEditorInput { return { resource: this.resource, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts index af0774397a..eb20d9c0f8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts @@ -10,7 +10,7 @@ import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { EditorActivation } from 'vs/platform/editor/common/editor'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { IShellLaunchConfig, TerminalLocation } from 'vs/platform/terminal/common/terminal'; -import { IEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IRemoteTerminalService, ITerminalEditorService, ITerminalInstance, ITerminalInstanceService, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; @@ -18,7 +18,7 @@ import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/b import { getInstanceFromResource, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { ILocalTerminalService, IOffProcessTerminalService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -100,7 +100,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor })); } - private _getActiveTerminalEditors(): IEditorInput[] { + private _getActiveTerminalEditors(): EditorInput[] { return this._editorService.visibleEditors.filter(e => e instanceof TerminalEditorInput && e.terminalInstance?.instanceId); } @@ -163,13 +163,14 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor if (resource) { await this._editorService.openEditor({ resource, + description: instance.description || instance.shellLaunchConfig.description, options: { pinned: true, forceReload: true, preserveFocus: editorOptions?.preserveFocus } - }, editorOptions?.viewColumn); + }, editorOptions?.viewColumn || ACTIVE_GROUP); } } @@ -230,7 +231,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor this._onDidChangeInstances.fire(); } - getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined { + getInstanceFromResource(resource?: URI): ITerminalInstance | undefined { return getInstanceFromResource(this.instances, resource); } @@ -247,6 +248,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor if (resource) { this._editorService.openEditor({ resource: URI.revive(resource), + description: instance.description, options: { pinned: true, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index ce32cf9194..adaa0cf3a0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -22,6 +22,7 @@ class SplitPaneContainer extends Disposable { private _splitView!: SplitView; private readonly _splitViewDisposables = this._register(new DisposableStore()); private _children: SplitPane[] = []; + private _terminalToPane: Map = new Map(); private _onDidChange: Event = Event.None; get onDidChange(): Event { return this._onDidChange; } @@ -118,6 +119,14 @@ class SplitPaneContainer extends Disposable { } } + getRelativePaneSize(instance: ITerminalInstance): number { + const paneForInstance = this._terminalToPane.get(instance); + if (!paneForInstance) { + return 0; + } + return ((this.orientation === Orientation.HORIZONTAL ? paneForInstance.element.clientWidth : paneForInstance.element.clientHeight) / (this.orientation === Orientation.HORIZONTAL ? this._width : this._height)); + } + private _addChild(instance: ITerminalInstance, index: number): void { const child = new SplitPane(instance, this.orientation === Orientation.HORIZONTAL ? this._height : this._width); child.orientation = this.orientation; @@ -126,6 +135,7 @@ class SplitPaneContainer extends Disposable { } else { this._children.push(child); } + this._terminalToPane.set(instance, this._children[this._children.indexOf(child)]); this._withDisabledLayout(() => this._splitView.addView(child, Sizing.Distribute, index)); this.layout(this._width, this._height); @@ -142,6 +152,7 @@ class SplitPaneContainer extends Disposable { } if (index !== null) { this._children.splice(index, 1); + this._terminalToPane.delete(instance); this._splitView.removeView(index, Sizing.Distribute); instance.detachFromElement(); } @@ -276,8 +287,11 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._onPanelOrientationChanged.fire(this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL); } - addInstance(shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance): void { + addInstance(shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance, parentTerminalId?: number): void { let instance: ITerminalInstance; + // if a parent terminal is provided, find it + // otherwise, parent is the active terminal + const parentIndex = parentTerminalId ? this._terminalInstances.findIndex(t => t.instanceId === parentTerminalId) : this._activeInstanceIndex; if ('instanceId' in shellLaunchConfigOrInstance) { instance = shellLaunchConfigOrInstance; } else { @@ -286,12 +300,12 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { if (this._terminalInstances.length === 0) { this._terminalInstances.push(instance); } else { - this._terminalInstances.splice(this._activeInstanceIndex + 1, 0, instance); + this._terminalInstances.splice(parentIndex + 1, 0, instance); } this._initInstanceListeners(instance); if (this._splitPaneContainer) { - this._splitPaneContainer!.split(instance, this._activeInstanceIndex + 1); + this._splitPaneContainer!.split(instance, parentIndex + 1); } instance.setVisible(this._isVisible); @@ -317,15 +331,13 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } getLayoutInfo(isActive: boolean): ITerminalTabLayoutInfoById { - const isHorizontal = this._splitPaneContainer?.orientation === Orientation.HORIZONTAL; const instances = this.terminalInstances.filter(instance => typeof instance.persistentProcessId === 'number' && instance.shouldPersist); - const totalSize = instances.map(instance => isHorizontal ? instance.cols : instance.rows).reduce((totalValue, currentValue) => totalValue + currentValue, 0); return { isActive: isActive, activePersistentProcessId: this.activeInstance ? this.activeInstance.persistentProcessId : undefined, terminals: instances.map(t => { return { - relativeSize: isHorizontal ? t.cols / totalSize : t.rows / totalSize, + relativeSize: this._splitPaneContainer?.getRelativePaneSize(t) || 0, terminal: t.persistentProcessId || 0 }; }) @@ -469,15 +481,15 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { return ''; } let title = this.terminalInstances[0].title + this._getBellTitle(this.terminalInstances[0]); - if (this.terminalInstances[0].shellLaunchConfig.description) { - title += ` (${this.terminalInstances[0].shellLaunchConfig.description})`; + if (this.terminalInstances[0].description) { + title += ` (${this.terminalInstances[0].description})`; } for (let i = 1; i < this.terminalInstances.length; i++) { const instance = this.terminalInstances[i]; if (instance.title) { title += `, ${instance.title + this._getBellTitle(instance)}`; - if (instance.shellLaunchConfig.description) { - title += ` (${instance.shellLaunchConfig.description})`; + if (instance.description) { + title += ` (${instance.description})`; } } } @@ -501,7 +513,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { split(shellLaunchConfig: IShellLaunchConfig): ITerminalInstance { const instance = this._terminalInstanceService.createInstance(shellLaunchConfig); - this.addInstance(instance); + this.addInstance(instance, shellLaunchConfig.parentTerminalId); this._setActiveInstance(instance); return instance; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index 249a092db1..35de123771 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -20,7 +20,7 @@ import { getInstanceFromResource } from 'vs/workbench/contrib/terminal/browser/t import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; export class TerminalGroupService extends Disposable implements ITerminalGroupService, ITerminalFindHost { declare _serviceBrand: undefined; @@ -84,7 +84,7 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe if (location === ViewContainerLocation.Panel) { const panel = this._viewDescriptorService.getViewContainerByViewId(TERMINAL_VIEW_ID); if (panel && this._viewDescriptorService.getViewContainerModel(panel).activeViewDescriptors.length === 1) { - this._layoutService.setPanelHidden(true); + this._layoutService.setPartHidden(true, Parts.PANEL_PART); TerminalContextKeys.tabsMouse.bindTo(this._contextKeyService).set(false); } } @@ -146,7 +146,11 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe this.groups.push(group); group.addDisposable(group.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance)); group.addDisposable(group.onDidFocusInstance(this._onDidFocusInstance.fire, this._onDidFocusInstance)); - group.addDisposable(group.onDidChangeActiveInstance(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance)); + group.addDisposable(group.onDidChangeActiveInstance(e => { + if (group === this.activeGroup) { + this._onDidChangeActiveInstance.fire(e); + } + })); group.addDisposable(group.onInstancesChanged(this._onDidChangeInstances.fire, this._onDidChangeInstances)); group.addDisposable(group.onDisposed(this._onDidDisposeGroup.fire, this._onDidDisposeGroup)); if (group.terminalInstances.length > 0) { @@ -395,6 +399,19 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe } joinInstances(instances: ITerminalInstance[]) { + const group = this.getGroupForInstance(instances[0]); + if (group) { + let differentGroups = true; + for (let i = 1; i < group.terminalInstances.length; i++) { + if (group.terminalInstances.includes(instances[i])) { + differentGroups = false; + break; + } + } + if (!differentGroups) { + return; + } + } // Find the group of the first instance that is the only instance in the group, if one exists let candidateInstance: ITerminalInstance | undefined = undefined; let candidateGroup: ITerminalGroup | undefined = undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts b/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts index d1f4c4135d..896658f723 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts @@ -6,16 +6,18 @@ import { Codicon, iconRegistry } from 'vs/base/common/codicons'; import { hash } from 'vs/base/common/hash'; import { URI } from 'vs/base/common/uri'; -import { IExtensionTerminalProfile } from 'vs/platform/terminal/common/terminal'; +import { IExtensionTerminalProfile, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ansiColorMap } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; export function getColorClass(colorKey: string): string; +export function getColorClass(profile: ITerminalProfile): string; export function getColorClass(terminal: ITerminalInstance): string | undefined; export function getColorClass(extensionTerminalProfile: IExtensionTerminalProfile): string | undefined; -export function getColorClass(terminalOrColorKey: ITerminalInstance | IExtensionTerminalProfile | string): string | undefined { +export function getColorClass(terminalOrColorKey: ITerminalInstance | IExtensionTerminalProfile | ITerminalProfile | string): string | undefined { let color = undefined; if (typeof terminalOrColorKey === 'string') { color = terminalOrColorKey; @@ -30,7 +32,60 @@ export function getColorClass(terminalOrColorKey: ITerminalInstance | IExtension return undefined; } -export function getUriClasses(terminal: ITerminalInstance | IExtensionTerminalProfile, colorScheme: ColorScheme, extensionContributed?: boolean): string[] | undefined { +export function getStandardColors(colorTheme: IColorTheme): string[] { + const standardColors: string[] = []; + + for (const colorKey in ansiColorMap) { + const color = colorTheme.getColor(colorKey); + if (color && !colorKey.toLowerCase().includes('bright')) { + standardColors.push(colorKey); + } + } + return standardColors; +} + +export function getColorStyleElement(colorTheme: IColorTheme): HTMLElement { + const standardColors = getStandardColors(colorTheme); + const styleElement = document.createElement('style'); + let css = ''; + for (const colorKey of standardColors) { + const colorClass = getColorClass(colorKey); + const color = colorTheme.getColor(colorKey); + if (color) { + css += ( + `.monaco-workbench .${colorClass} .codicon:first-child:not(.codicon-split-horizontal):not(.codicon-trashcan):not(.file-icon)` + + `{ color: ${color} !important; }` + ); + } + } + styleElement.textContent = css; + return styleElement; +} + +export function getColorStyleContent(colorTheme: IColorTheme, editor?: boolean): string { + const standardColors = getStandardColors(colorTheme); + let css = ''; + for (const colorKey of standardColors) { + const colorClass = getColorClass(colorKey); + const color = colorTheme.getColor(colorKey); + if (color) { + if (editor) { + css += ( + `.monaco-workbench .show-file-icons .file-icon.terminal-tab.${colorClass}::before` + + `{ color: ${color} !important; }` + ); + } else { + css += ( + `.monaco-workbench .${colorClass} .codicon:first-child:not(.codicon-split-horizontal):not(.codicon-trashcan):not(.file-icon)` + + `{ color: ${color} !important; }` + ); + } + } + } + return css; +} + +export function getUriClasses(terminal: ITerminalInstance | IExtensionTerminalProfile | ITerminalProfile, colorScheme: ColorScheme, extensionContributed?: boolean): string[] | undefined { const icon = terminal.icon; if (!icon) { return undefined; @@ -60,7 +115,7 @@ export function getUriClasses(terminal: ITerminalInstance | IExtensionTerminalPr return iconClasses; } -export function getIconId(terminal: ITerminalInstance | IExtensionTerminalProfile): string { +export function getIconId(terminal: ITerminalInstance | IExtensionTerminalProfile | ITerminalProfile): string { if (!terminal.icon || (terminal.icon instanceof Object && !('id' in terminal.icon))) { return Codicon.terminal.id; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 86ace1de96..bba811915d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -24,8 +24,8 @@ import { activeContrastBorder, editorBackground, scrollbarSliderActiveBackground import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { ITerminalProcessManager, ProcessState, TERMINAL_VIEW_ID, INavigationMode, DEFAULT_COMMANDS_TO_SKIP_SHELL, TERMINAL_CREATION_COMMANDS, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ansiColorIdentifiers, ansiColorMap, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { ITerminalProcessManager, ProcessState, TERMINAL_VIEW_ID, INavigationMode, DEFAULT_COMMANDS_TO_SKIP_SHELL, TERMINAL_CREATION_COMMANDS, ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -46,19 +46,19 @@ import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTy import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TitleEventSource, TerminalIcon, TerminalSettingPrefix, ITerminalProfileObject, TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TitleEventSource, TerminalIcon, TerminalSettingPrefix, ITerminalProfileObject, TerminalLocation, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; import { IProductService } from 'vs/platform/product/common/productService'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; -import { AutoOpenBarrier } from 'vs/base/common/async'; +import { AutoOpenBarrier, Promises } from 'vs/base/common/async'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; import { ITerminalStatusList, TerminalStatus, TerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { isIOS, isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { DataTransfers } from 'vs/base/browser/dnd'; import { CodeDataTransfers, containsDragType, DragAndDropObserver, IDragAndDropObserverCallbacks } from 'vs/workbench/browser/dnd'; -import { getColorClass } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; +import { getColorClass, getColorStyleElement, getStandardColors } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { Color } from 'vs/base/common/color'; @@ -68,6 +68,12 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin import { getTerminalResourcesFromDragEvent, getTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; +import { isSafari } from 'vs/base/browser/browser'; +import { ISeparator, template } from 'vs/base/common/labels'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -88,6 +94,8 @@ const enum Constants { DefaultCols = 80, DefaultRows = 30, + MaxSupportedCols = 5000, + MaxCanvasWidth = 8000 } let xtermConstructor: Promise | undefined; @@ -102,6 +110,8 @@ interface IGridDimensions { rows: number; } +const scrollbarHeight = 5; + export class TerminalInstance extends Disposable implements ITerminalInstance { private static _lastKnownCanvasDimensions: ICanvasDimensions | undefined; private static _lastKnownGridDimensions: IGridDimensions | undefined; @@ -131,10 +141,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _xtermSearch: SearchAddon | undefined; private _xtermUnicode11: Unicode11Addon | undefined; private _xtermElement: HTMLDivElement | undefined; + private _horizontalScrollbar: DomScrollableElement | undefined; private _terminalHasTextContextKey: IContextKey; private _terminalA11yTreeFocusContextKey: IContextKey; private _cols: number = 0; private _rows: number = 0; + private _fixedCols: number | undefined; + private _fixedRows: number | undefined; + private _cwd: string | undefined = undefined; + private _initialCwd: string | undefined = undefined; private _dimensionsOverride: ITerminalDimensionsOverride | undefined; private _xtermReadyPromise: Promise; private _titleReadyPromise: Promise; @@ -160,12 +175,27 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _hasHadInput: boolean; + readonly statusList: ITerminalStatusList; disableLayout: boolean = false; + + private _capabilities: ProcessCapability[] = []; + private _description?: string; + private _processName: string = ''; + private _sequence?: string; + private _staticTitle?: string; + private _workspaceFolder?: string; + private _labelComputer?: TerminalLabelComputer; + private _userHome?: string; + private _hasScrollBar?: boolean; + target?: TerminalLocation; get instanceId(): number { return this._instanceId; } get resource(): URI { return this._resource; } get cols(): number { + if (this._fixedCols !== undefined) { + return this._fixedCols; + } if (this._dimensionsOverride && this._dimensionsOverride.cols) { if (this._dimensionsOverride.forceExactSize) { return this._dimensionsOverride.cols; @@ -175,6 +205,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return this._cols; } get rows(): number { + if (this._fixedRows !== undefined) { + return this._fixedRows; + } if (this._dimensionsOverride && this._dimensionsOverride.rows) { if (this._dimensionsOverride.forceExactSize) { return this._dimensionsOverride.rows; @@ -183,6 +216,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } return this._rows; } + get fixedCols(): number | undefined { return this._fixedCols; } + get fixedRows(): number | undefined { return this._fixedRows; } get maxCols(): number { return this._cols; } get maxRows(): number { return this._rows; } // TODO: Ideally processId would be merged into processReady @@ -209,6 +244,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get icon(): TerminalIcon | undefined { return this._getIcon(); } get color(): string | undefined { return this._getColor(); } + get processName(): string { return this._processName; } + get sequence(): string | undefined { return this._sequence; } + get staticTitle(): string | undefined { return this._staticTitle; } + get workspaceFolder(): string | undefined { return this._workspaceFolder; } + get cwd(): string | undefined { return this._cwd; } + get initialCwd(): string | undefined { return this._initialCwd; } + get capabilities(): ProcessCapability[] { return this._capabilities; } + get description(): string | undefined { return this._description || this.shellLaunchConfig.description; } + get userHome(): string | undefined { return this._userHome; } // The onExit event is special in that it fires and is disposed after the terminal instance // itself is disposed private readonly _onExit = new Emitter(); @@ -249,6 +293,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { constructor( private readonly _terminalFocusContextKey: IContextKey, + private readonly _terminalHasFixedWidth: IContextKey, private readonly _terminalShellTypeContextKey: IContextKey, private readonly _terminalAltBufferActiveContextKey: IContextKey, private readonly _configHelper: TerminalConfigHelper, @@ -256,6 +301,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { resource: URI | undefined, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, + @IPathService private readonly _pathService: IPathService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @INotificationService private readonly _notificationService: INotificationService, @@ -283,12 +329,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._isVisible = false; this._isDisposed = false; this._instanceId = TerminalInstance._instanceIdCounter++; - this._hasHadInput = false; this._titleReadyPromise = new Promise(c => { this._titleReadyComplete = c; }); + this._fixedRows = _shellLaunchConfig.attachPersistentProcess?.fixedDimensions?.rows; + this._fixedCols = _shellLaunchConfig.attachPersistentProcess?.fixedDimensions?.cols; + // the resource is already set when it's been moved from another window this._resource = resource || getTerminalUri(this._workspaceContextService.getWorkspace().id, this.instanceId, this.title); @@ -309,7 +357,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // When a custom pty is used set the name immediately so it gets passed over to the exthost // and is available when Pseudoterminal.open fires. if (this.shellLaunchConfig.customPtyImplementation) { - this.setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); + this.refreshTabLabels(this._shellLaunchConfig.name, TitleEventSource.Api); } this.statusList = this._instantiationService.createInstance(TerminalStatusList); @@ -328,7 +376,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Re-establish the title after reconnect if (this.shellLaunchConfig.attachPersistentProcess) { - this.setTitle(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); + this.refreshTabLabels(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); + } + + if (this._fixedCols) { + await this._addScrollbar(); } }); @@ -338,9 +390,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } if (e.affectsConfiguration('terminal.integrated') || e.affectsConfiguration('editor.fastScrollSensitivity') || e.affectsConfiguration('editor.mouseWheelScrollSensitivity') || e.affectsConfiguration('editor.multiCursorModifier')) { this.updateConfig(); - // HACK: Trigger another async layout to ensure xterm's CharMeasure is ready to use, - // this hack can be removed when https://github.com/xtermjs/xterm.js/issues/702 is - // supported. this.setVisible(this._isVisible); } const layoutSettings: string[] = [ @@ -361,7 +410,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (e.affectsConfiguration('editor.accessibilitySupport')) { this.updateAccessibilitySupport(); } + if ( + e.affectsConfiguration(TerminalSettingId.TerminalTitle) || + e.affectsConfiguration(TerminalSettingId.TerminalTitleSeparator) || + e.affectsConfiguration(TerminalSettingId.TerminalDescription)) { + this._labelComputer?.refreshLabel(); + } })); + this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._labelComputer?.refreshLabel()); // Clear out initial data events after 10 seconds, hopefully extension hosts are up and // running at that point. @@ -455,7 +511,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } - const computedStyle = window.getComputedStyle(this._container.parentElement!); + const computedStyle = window.getComputedStyle(this._wrapperElement!); const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); this._evaluateColsAndRows(width, height); @@ -531,16 +587,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (!this._wrapperElement) { return undefined; } - - const wrapperElementStyle = getComputedStyle(this._wrapperElement); - const marginLeft = parseInt(wrapperElementStyle.marginLeft!.split('px')[0], 10); - const marginRight = parseInt(wrapperElementStyle.marginRight!.split('px')[0], 10); - const bottom = parseInt(wrapperElementStyle.bottom!.split('px')[0], 10); - - const innerWidth = width - marginLeft - marginRight; - const innerHeight = height - bottom - 1; - - TerminalInstance._lastKnownCanvasDimensions = new dom.Dimension(innerWidth, innerHeight); + TerminalInstance._lastKnownCanvasDimensions = new dom.Dimension(Math.min(Constants.MaxCanvasWidth, width), height + (this._hasScrollBar && !this._horizontalScrollbar ? -scrollbarHeight - 2 : 0)/* bottom padding */); return TerminalInstance._lastKnownCanvasDimensions; } @@ -551,7 +598,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (xtermConstructor) { return xtermConstructor; } - xtermConstructor = new Promise(async (resolve) => { + xtermConstructor = Promises.withAsyncBody(async (resolve) => { const Terminal = await this._terminalInstanceService.getXtermConstructor(); // Localize strings Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input'); @@ -584,6 +631,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { letterSpacing: font.letterSpacing, lineHeight: font.lineHeight, minimumContrastRatio: config.minimumContrastRatio, + cursorBlink: config.cursorBlinking, + cursorStyle: config.cursorStyle === 'line' ? 'bar' : config.cursorStyle, + cursorWidth: config.cursorWidth, bellStyle: 'none', macOptionIsMeta: config.macOptionIsMeta, macOptionClickForcesSelection: config.macOptionClickForcesSelection, @@ -596,14 +646,22 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); this._xterm = xterm; this._xtermCore = (xterm as any)._core as XTermCore; + const lineDataEventAddon = new LineDataEventAddon(); + this._xterm.loadAddon(lineDataEventAddon); this._updateUnicodeVersion(); this.updateAccessibilitySupport(); this._terminalInstanceService.getXtermSearchConstructor().then(addonCtor => { this._xtermSearch = new addonCtor(); xterm.loadAddon(this._xtermSearch); }); + // Write initial text, deferring onLineFeed listener when applicable to avoid firing + // onLineData events containing initialText if (this._shellLaunchConfig.initialText) { - this._xterm.writeln(this._shellLaunchConfig.initialText); + this._xterm.writeln(this._shellLaunchConfig.initialText, () => { + lineDataEventAddon.onLineData(e => this._onLineData.fire(e)); + }); + } else { + lineDataEventAddon.onLineData(e => this._onLineData.fire(e)); } // Delay the creation of the bell listener to avoid showing the bell when the terminal // starts up or reconnects @@ -619,7 +677,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } }); }, 1000); - this._xterm.onLineFeed(() => this._onLineFeed()); this._xterm.onKey(e => this._onKey(e.key, e.domEvent)); this._xterm.onSelectionChange(async () => this._onSelectionChange()); this._xterm.buffer.onBufferChange(() => this._refreshAltBufferContextKey()); @@ -638,15 +695,16 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Init winpty compat and link handler after process creation as they rely on the // underlying process OS this._processManager.onProcessReady((processTraits) => { + // If links are ready, do not re-create the manager. + if (this._areLinksReady) { + return; + } + + if (this._processManager.os) { + lineDataEventAddon.setOperatingSystem(this._processManager.os); + } if (this._processManager.os === OperatingSystem.Windows) { xterm.setOption('windowsMode', processTraits.requiresWindowsMode || false); - // Force line data to be sent when the cursor is moved, the main purpose for - // this is because ConPTY will often not do a line feed but instead move the - // cursor, in which case we still want to send the current line's data to tasks. - xterm.parser.registerCsiHandler({ final: 'H' }, () => { - this._onCursorMove(); - return false; - }); } this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm, this._processManager!); this._areLinksReady = true; @@ -664,7 +722,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._xtermTypeAhead = this._register(this._instantiationService.createInstance(TypeAheadAddon, this._processManager, this._configHelper)); this._xterm.loadAddon(this._xtermTypeAhead); - + this._pathService.userHome().then(userHome => { + this._userHome = userHome.fsPath; + }); return xterm; } @@ -707,8 +767,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._wrapperElement = document.createElement('div'); this._wrapperElement.classList.add('terminal-wrapper'); this._xtermElement = document.createElement('div'); - this._wrapperElement.appendChild(this._xtermElement); + this._container.appendChild(this._wrapperElement); const xterm = await this._xtermReadyPromise; @@ -723,6 +783,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { throw new Error('xterm elements not set after open'); } + this._setAriaLabel(xterm, this._instanceId, this._title); xterm.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => { @@ -818,6 +879,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { xterm.focus(); })); + this._register(dom.addDisposableListener(xterm.element, 'wheel', (e) => { + if (this._hasScrollBar && e.shiftKey) { + e.stopImmediatePropagation(); + e.preventDefault(); + } + })); + // xterm.js currently drops selection on keyup as we need to handle this case. this._register(dom.addDisposableListener(xterm.element, 'keyup', () => { // Wait until keyup has propagated through the DOM before evaluating @@ -844,7 +912,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._initDragAndDrop(container); this._widgetManager.attachToElement(xterm.element); - this._processManager.onProcessReady(() => this._linkManager?.setWidgetManager(this._widgetManager)); + this._processManager.onProcessReady((e) => { + this._linkManager?.setWidgetManager(this._widgetManager); + this._capabilities = e.capabilities; + this._workspaceFolder = path.basename(e.cwd.toString()); + }); // const computedStyle = window.getComputedStyle(this._container); // const computedStyle = window.getComputedStyle(this._container.parentElement!); @@ -997,15 +1069,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (this._wrapperElement.xterm) { this._wrapperElement.xterm = undefined; } - if (this._wrapperElement.parentElement && this._container) { - this._container.removeChild(this._wrapperElement); + if (this._horizontalScrollbar) { + this._horizontalScrollbar.dispose(); + this._horizontalScrollbar = undefined; } } - if (this._xterm) { - const buffer = this._xterm.buffer; - this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY); - this._xterm.dispose(); - } + this._xterm?.dispose(); if (this._pressAnyKeyToCloseListener) { this._pressAnyKeyToCloseListener.dispose(); @@ -1026,7 +1095,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { async detachFromProcess(): Promise { await this._processManager.detachFromProcess(); - this.dispose(); } forceRedraw(): void { @@ -1034,7 +1102,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } this._webglAddon?.clearTextureAtlas(); - // TODO: Do canvas renderer too? + this._xterm?.clearTextureAtlas(); } focus(force?: boolean): void { @@ -1097,13 +1165,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // using cached dimensions of a split terminal). this._resize(); - // Trigger a manual scroll event which will sync the viewport and scroll bar. This is + // Trigger a forced refresh of the viewport to sync the viewport and scroll bar. This is // necessary if the number of rows in the terminal has decreased while it was in the // background since scrollTop changes take no effect but the terminal's position does // change since the number of visible rows decreases. // This can likely be removed after https://github.com/xtermjs/xterm.js/issues/291 is // fixed upstream. - this._xtermCore._onScroll.fire(this._xterm.buffer.active.viewportY); + this._xtermCore.viewport?._innerRefresh(); } } @@ -1147,44 +1215,65 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { protected _createProcessManager(): void { this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._instanceId, this._configHelper); - this._processManager.onProcessReady(() => { + this._processManager.onProcessReady(async (e) => { this._onProcessIdReady.fire(this); + this._initialCwd = await this.getInitialCwd(); + this._capabilities = e.capabilities; // Set the initial name based on the _resolved_ shell launch config, this will also // ensure the resolved icon gets shown + if (!this._labelComputer) { + this._labelComputer = this._register(new TerminalLabelComputer(this._configHelper, this, this._workspaceContextService)); + this._labelComputer.onDidChangeLabel(e => { + this._title = e.title; + this._description = e.description; + this._onTitleChanged.fire(this); + }); + } if (this._shellLaunchConfig.name) { - this.setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); + this.refreshTabLabels(this._shellLaunchConfig.name, TitleEventSource.Api); } else { - // Only listen for process title changes when a name is not provided - if (this._configHelper.config.titleMode === 'sequence') { - // Set the title to the first event if the sequence hasn't set it yet - Event.once(this._processManager.onProcessTitle)(e => { - if (!this._title) { - this.setTitle(e, TitleEventSource.Sequence); - } + // Listen to xterm.js' sequence title change event, trigger this async to ensure + // _xtermReadyPromise is ready constructed since this is called from the ctor + setTimeout(() => { + this._xtermReadyPromise.then(xterm => { + this._messageTitleDisposable = xterm.onTitleChange(e => this._onTitleChange(e)); }); - // Listen to xterm.js' sequence title change event, trigger this async to ensure - // _xtermReadyPromise is ready constructed since this is called from the ctor - setTimeout(() => { - this._xtermReadyPromise.then(xterm => { - this._messageTitleDisposable = xterm.onTitleChange(e => this._onTitleChange(e)); - }); - }); - } else { - this.setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); - this._messageTitleDisposable = this._processManager.onProcessTitle(title => this.setTitle(title ? title : '', TitleEventSource.Process)); - } + }); + this.refreshTabLabels(this._shellLaunchConfig.executable, TitleEventSource.Process); } }); this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); + this._processManager.onDidChangeProperty(({ type, value }) => { + switch (type) { + case ProcessPropertyType.Cwd: + this._cwd = value; + this._labelComputer?.refreshLabel(); + break; + case ProcessPropertyType.InitialCwd: + this._initialCwd = value; + this._cwd = this._initialCwd; + this.refreshTabLabels(this.title, TitleEventSource.Api); + break; + case ProcessPropertyType.Title: + this.refreshTabLabels(value ? value : '', TitleEventSource.Process); + break; + case ProcessPropertyType.OverrideDimensions: + this.setOverrideDimensions(value, true); + break; + case ProcessPropertyType.ResolvedShellLaunchConfig: + this._setResolvedShellLaunchConfig(value); + break; + case ProcessPropertyType.HasChildProcesses: + this._onDidChangeHasChildProcesses.fire(value); + break; + } + }); + this._processManager.onProcessData(ev => { this._initialDataEvents?.push(ev.data); this._onData.fire(ev.data); }); - this._processManager.onProcessOverrideDimensions(e => this.setDimensions(e, true)); - this._processManager.onProcessResolvedShellLaunchConfig(e => this._setResolvedShellLaunchConfig(e)); - this._processManager.onProcessDidChangeHasChildProcesses(e => this._onDidChangeHasChildProcesses.fire(e)); this._processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e)); - this._processManager.onProcessShellTypeChanged(type => this.setShellType(type)); this._processManager.onPtyDisconnect(() => { this._safeSetOption('disableStdin', true); this.statusList.add({ @@ -1297,6 +1386,21 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { break; } this._exitCode = exitCodeOrError.code; + const conptyError = exitCodeOrError.message.match(/.*error code:\s*(\d+).*$/); + if (conptyError) { + const errorCode = conptyError.length > 1 ? parseInt(conptyError[1]) : undefined; + switch (errorCode) { + case 5: + exitCodeOrError.message = `Access was denied to the path containing your executable ${this.shellLaunchConfig.executable}. Manage and change your permissions to get this to work.`; + break; + case 267: + exitCodeOrError.message = `Invalid starting directory ${this.initialCwd}, review your terminal.integrated.cwd setting`; + break; + case 1260: + exitCodeOrError.message = `Windows cannot open this program because it has been prevented by a software restriction policy. For more information, open Event Viewer or contact your system Administrator`; + break; + } + } exitCodeMessage = nls.localize('launchFailed.errorMessage', "The terminal process failed to launch: {0}.", exitCodeOrError.message); break; } @@ -1419,11 +1523,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._processManager.relaunch(this._shellLaunchConfig, this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows, this._accessibilityService.isScreenReaderOptimized(), reset); - // Set title again as when creating the first process - if (this._shellLaunchConfig.name) { - this.setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); - } - this._xtermTypeAhead?.reset(); } @@ -1432,41 +1531,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.reuseTerminal(this._shellLaunchConfig, true); } - private _onLineFeed(): void { - const buffer = this._xterm!.buffer; - const newLine = buffer.active.getLine(buffer.active.baseY + buffer.active.cursorY); - if (newLine && !newLine.isWrapped) { - this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY - 1); - } - } - - private _onCursorMove(): void { - const buffer = this._xterm!.buffer; - this._sendLineData(buffer.active, buffer.active.baseY + buffer.active.cursorY); - } - private _onTitleChange(title: string): void { if (this.isTitleSetByProcess) { - this.setTitle(title, TitleEventSource.Sequence); + this.refreshTabLabels(title, TitleEventSource.Sequence); } } - private _sendLineData(buffer: IBuffer, lineIndex: number): void { - let line = buffer.getLine(lineIndex); - if (!line) { - return; - } - let lineData = line.translateToString(true); - while (lineIndex > 0 && line.isWrapped) { - line = buffer.getLine(--lineIndex); - if (!line) { - break; - } - lineData = line.translateToString(false) + lineData; - } - this._onLineData.fire(lineData); - } - private _onKey(key: string, ev: KeyboardEvent): void { const event = new StandardKeyboardEvent(ev); @@ -1486,7 +1556,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @debounce(2000) private async _updateProcessCwd(): Promise { // reset cwd if it has changed, so file based url paths can be resolved - const cwd = await this.getCwd(); + const cwd = await this.refreshProperty(ProcessPropertyType.Cwd); + if (typeof cwd !== 'string') { + throw new Error('cwd is not a string'); + } if (cwd && this._linkManager) { this._linkManager.processCwd = cwd; } @@ -1514,7 +1587,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._safeSetOption('customGlyphs', config.customGlyphs); const suggestedRendererType = TerminalInstance._suggestedRendererType; // @meganrogge @Tyriar remove if the issue related to iPads and webgl is resolved - if ((!isIOS && config.gpuAcceleration === 'auto' && suggestedRendererType === undefined) || config.gpuAcceleration === 'on') { + if ((!isSafari && config.gpuAcceleration === 'auto' && suggestedRendererType === undefined) || config.gpuAcceleration === 'on') { this._enableWebglRenderer(); } else { this._disposeOfWebglRenderer(); @@ -1648,10 +1721,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } - if (this._xterm && this._xterm.element) { - this._xterm.element.style.width = terminalWidth + 'px'; - } - this._resize(); // Signal the container is ready @@ -1692,6 +1761,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } if (cols !== this._xterm.cols || rows !== this._xterm.rows) { + if (this._fixedRows || this._fixedCols) { + await this.updateProperty(ProcessPropertyType.FixedDimensions, { cols: this._fixedCols, rows: this._fixedRows }); + } this._onDimensionsChanged.fire(); } @@ -1700,7 +1772,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (this._isVisible) { // HACK: Force the renderer to unpause by simulating an IntersectionObserver event. - // This is to fix an issue where dragging the window to the top of the screen to + // This is to fix an issue where dragging the windpow to the top of the screen to // maximize on Windows/Linux would fire an event saying that the terminal was not // visible. if (this._xterm.getOption('rendererType') === 'canvas') { @@ -1725,22 +1797,45 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private _setAriaLabel(xterm: XTermTerminal | undefined, terminalId: number, title: string | undefined): void { - if (xterm) { + if (xterm && xterm.textarea) { + let label: string; if (title && title.length > 0) { - xterm.textarea?.setAttribute('aria-label', nls.localize('terminalTextBoxAriaLabelNumberAndTitle', "Terminal {0}, {1}", terminalId, title)); + label = nls.localize('terminalTextBoxAriaLabelNumberAndTitle', "Terminal {0}, {1}", terminalId, title); } else { - xterm.textarea?.setAttribute('aria-label', nls.localize('terminalTextBoxAriaLabel', "Terminal {0}", terminalId)); + label = nls.localize('terminalTextBoxAriaLabel', "Terminal {0}", terminalId); } + const navigateUpKeybinding = this._keybindingService.lookupKeybinding(TerminalCommandId.NavigationModeFocusPrevious)?.getLabel(); + const navigateDownKeybinding = this._keybindingService.lookupKeybinding(TerminalCommandId.NavigationModeFocusNext)?.getLabel(); + if (navigateUpKeybinding && navigateDownKeybinding) { + label += `\n${nls.localize('terminalNavigationMode', "Use {0} and {1} to navigate the terminal buffer", navigateUpKeybinding, navigateDownKeybinding)}`; + } + xterm.textarea.setAttribute('aria-label', label); } } - setTitle(title: string | undefined, eventSource: TitleEventSource): void { + refreshTabLabels(title: string | undefined, eventSource: TitleEventSource): void { + title = this._updateTitleProperties(title, eventSource); + const titleChanged = title !== this._title; + this._title = title; + this._labelComputer?.refreshLabel(); + this._setAriaLabel(this._xterm, this._instanceId, this._title); + + if (this._titleReadyComplete) { + this._titleReadyComplete(title); + this._titleReadyComplete = undefined; + } + + if (titleChanged) { + this._onTitleChanged.fire(this); + } + } + + private _updateTitleProperties(title: string | undefined, eventSource: TitleEventSource): string { if (!title) { - return; + return this._processName; } switch (eventSource) { case TitleEventSource.Process: - if (this._processManager.os === OperatingSystem.Windows) { // Extract the file name without extension title = path.win32.parse(title).name; @@ -1752,10 +1847,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { title = title.substring(0, firstSpaceIndex); } } + this._processName = title; break; case TitleEventSource.Api: // If the title has not been set by the API or the rename command, unregister the handler that // automatically updates the terminal name + this._staticTitle = title; dispose(this._messageTitleDisposable); this._messageTitleDisposable = undefined; break; @@ -1763,34 +1860,26 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // On Windows, some shells will fire this with the full path which we want to trim // to show just the file name. This should only happen if the title looks like an // absolute Windows file path - if (this._processManager.os === OperatingSystem.Windows && title.match(/^[a-zA-Z]:\\.+\.[a-zA-Z]{1,3}/)) { - title = path.win32.parse(title).name; + this._sequence = title; + if (this._processManager.os === OperatingSystem.Windows) { + if (title.match(/^[a-zA-Z]:\\.+\.[a-zA-Z]{1,3}/)) { + title = path.win32.parse(title).name; + this._sequence = title; + } else { + this._sequence = undefined; + } } break; } - - // Remove special characters that could mess with rendering - title = title.replace(/[\n\r\t]/g, ''); - - const didTitleChange = title !== this._title; - this._title = title; this._titleSource = eventSource; - if (didTitleChange) { - this._setAriaLabel(this._xterm, this._instanceId, this._title); - - if (this._titleReadyComplete) { - this._titleReadyComplete(title); - this._titleReadyComplete = undefined; - } - this._onTitleChanged.fire(this); - } + return title; } waitForTitle(): Promise { return this._titleReadyPromise; } - setDimensions(dimensions: ITerminalDimensionsOverride | undefined, immediate: boolean = false): void { + setOverrideDimensions(dimensions: ITerminalDimensionsOverride | undefined, immediate: boolean = false): void { if (this._dimensionsOverride && this._dimensionsOverride.forceExactSize && !dimensions && this._rows === 0 && this._cols === 0) { // this terminal never had a real size => keep the last dimensions override exact size this._cols = this._dimensionsOverride.cols; @@ -1804,6 +1893,131 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + async setFixedDimensions(): Promise { + const cols = await this._quickInputService.input({ + title: nls.localize('setTerminalDimensionsColumn', "Set Fixed Dimensions: Column"), + placeHolder: 'Enter a number of columns or leave empty for automatic width', + validateInput: async (text) => text.length > 0 && !text.match(/^\d+$/) ? { content: 'Enter a number or leave empty size automatically', severity: Severity.Error } : undefined + }); + if (cols === undefined) { + return; + } + this._fixedCols = this._parseFixedDimension(cols); + this._terminalHasFixedWidth.set(!!this._fixedCols); + const rows = await this._quickInputService.input({ + title: nls.localize('setTerminalDimensionsRow', "Set Fixed Dimensions: Row"), + placeHolder: 'Enter a number of rows or leave empty for automatic height', + validateInput: async (text) => text.length > 0 && !text.match(/^\d+$/) ? { content: 'Enter a number or leave empty size automatically', severity: Severity.Error } : undefined + }); + if (rows === undefined) { + return; + } + this._fixedRows = this._parseFixedDimension(rows); + this._addScrollbar(); + this._resize(); + this.focus(); + } + + private _parseFixedDimension(value: string): number | undefined { + if (value === '') { + return undefined; + } + const parsed = parseInt(value); + if (parsed <= 0) { + throw new Error(`Could not parse dimension "${value}"`); + } + return parsed; + } + + async toggleSizeToContentWidth(): Promise { + if (!this._xterm?.buffer.active) { + return; + } + if (this._hasScrollBar) { + this._terminalHasFixedWidth.set(false); + this._fixedCols = undefined; + this._fixedRows = undefined; + this._hasScrollBar = false; + this._initDimensions(); + await this._resize(); + this._horizontalScrollbar?.setScrollDimensions({ scrollWidth: 0 }); + } else { + let maxCols = 0; + if (!this._xterm.buffer.active.getLine(0)) { + return; + } + const lineWidth = this._xterm.buffer.active.getLine(0)!.length; + for (let i = this._xterm.buffer.active.length - 1; i >= this._xterm.buffer.active.viewportY; i--) { + const lineInfo = this._getWrappedLineCount(i, this._xterm.buffer.active); + maxCols = Math.max(maxCols, ((lineInfo.lineCount * lineWidth) - lineInfo.endSpaces) || 0); + i = lineInfo.currentIndex; + } + maxCols = Math.min(maxCols, Constants.MaxSupportedCols); + this._fixedCols = maxCols; + await this._addScrollbar(); + } + this.focus(); + } + + private async _addScrollbar(): Promise { + const charWidth = this._configHelper?.getFont(this._xtermCore).charWidth; + if (!this._xterm?.element || !this._wrapperElement || !this._container || !charWidth || !this._fixedCols) { + return; + } + if (this._fixedCols < this._xterm.buffer.active.getLine(0)!.length) { + // no scrollbar needed + return; + } + this._hasScrollBar = true; + this._initDimensions(); + this._fixedRows = this.rows; + await this._resize(); + this._terminalHasFixedWidth.set(true); + if (!this._horizontalScrollbar) { + this._horizontalScrollbar = this._register(new DomScrollableElement(this._wrapperElement, { + vertical: ScrollbarVisibility.Hidden, + horizontal: ScrollbarVisibility.Auto, + useShadows: false, + scrollYToX: false, + consumeMouseWheelIfScrollbarIsNeeded: false + })); + this._container.appendChild(this._horizontalScrollbar.getDomNode()); + } + this._horizontalScrollbar.setScrollDimensions( + { + width: this._xterm.element.clientWidth, + scrollWidth: this._fixedCols * charWidth + }); + this._horizontalScrollbar!.getDomNode().style.paddingBottom = '16px'; + + // work around for https://github.com/xtermjs/xterm.js/issues/3482 + for (let i = this._xterm.buffer.active.viewportY; i < this._xterm.buffer.active.length; i++) { + let line = this._xterm.buffer.active.getLine(i); + (line as any)._line.isWrapped = false; + } + } + + private _getWrappedLineCount(index: number, buffer: IBuffer): { lineCount: number, currentIndex: number, endSpaces: number } { + let line = buffer.getLine(index); + if (!line) { + throw new Error('Could not get line'); + } + let currentIndex = index; + let endSpaces = -1; + for (let i = line?.length || 0; i > 0; i--) { + if (line && !line?.getCell(i)?.getChars()) { + endSpaces++; + } else { + break; + } + } + while (line?.isWrapped && currentIndex > 0) { + currentIndex--; + line = buffer.getLine(currentIndex); + } + return { lineCount: index - currentIndex + 1, currentIndex, endSpaces }; + } + private _setResolvedShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig): void { this._shellLaunchConfig.args = shellLaunchConfig.args; this._shellLaunchConfig.cwd = shellLaunchConfig.cwd; @@ -1881,7 +2095,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (this.target === TerminalLocation.Editor) { backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || theme.getColor(editorBackground); } else { - backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || (location === ViewContainerLocation.Sidebar ? theme.getColor(SIDE_BAR_BACKGROUND) : theme.getColor(PANEL_BACKGROUND)); + backgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || (location === ViewContainerLocation.Panel ? theme.getColor(PANEL_BACKGROUND) : theme.getColor(SIDE_BAR_BACKGROUND)); } const cursorColor = theme.getColor(TERMINAL_CURSOR_FOREGROUND_COLOR) || foregroundColor; const cursorAccentColor = theme.getColor(TERMINAL_CURSOR_BACKGROUND_COLOR) || backgroundColor; @@ -1922,12 +2136,24 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { xterm.setOption('logLevel', isDebug ? 'info' : 'debug'); } - getInitialCwd(): Promise { - return this._processManager.getInitialCwd(); + async getInitialCwd(): Promise { + if (!this._initialCwd) { + this._initialCwd = await this._processManager.getInitialCwd(); + } + return this._initialCwd; } - getCwd(): Promise { - return this._processManager.getCwd(); + async getCwd(): Promise { + return await this._processManager.getInitialCwd(); + } + + async refreshProperty(type: ProcessPropertyType): Promise { + await this.processReady; + return this._processManager.refreshProperty(type); + } + + async updateProperty(type: ProcessPropertyType, value: any): Promise { + return this._processManager.updateProperty(type, value); } registerLinkProvider(provider: ITerminalExternalLinkProvider): IDisposable { @@ -1945,7 +2171,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); } if (title) { - this.setTitle(title, TitleEventSource.Api); + this.refreshTabLabels(title, TitleEventSource.Api); } } @@ -1968,36 +2194,19 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (!icon) { return; } - - const standardColors: string[] = []; const colorTheme = this._themeService.getColorTheme(); - for (const colorKey in ansiColorMap) { - const color = colorTheme.getColor(colorKey); - if (color && !colorKey.toLowerCase().includes('bright')) { - standardColors.push(colorKey); - } - } - - const styleElement = document.createElement('style'); - let css = ''; + const standardColors: string[] = getStandardColors(colorTheme); + const styleElement = getColorStyleElement(colorTheme); const items: (IQuickPickItem | IQuickPickSeparator)[] = []; for (const colorKey of standardColors) { const colorClass = getColorClass(colorKey); items.push({ label: `$(${Codicon.circleFilled.id}) ${colorKey.replace('terminal.ansi', '')}`, id: colorKey, description: colorKey, iconClasses: [colorClass] }); - const color = colorTheme.getColor(colorKey); - if (color) { - css += ( - `.monaco-workbench .${colorClass} .codicon:first-child:not(.codicon-split-horizontal):not(.codicon-trashcan):not(.file-icon)` + - `{ color: ${color} !important; }` - ); - } } items.push({ type: 'separator' }); const showAllColorsItem = { label: 'Reset to default' }; items.push(showAllColorsItem); - styleElement.textContent = css; document.body.appendChild(styleElement); const quickPick = this._quickInputService.createQuickPick(); @@ -2197,3 +2406,95 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = `); } }); + +export interface ITerminalLabelTemplateProperties { + cwd?: string | null | undefined; + cwdFolder?: string | null | undefined; + workspaceFolder?: string | null | undefined; + local?: string | null | undefined; + process?: string | null | undefined; + sequence?: string | null | undefined; + task?: string | null | undefined; + separator?: string | ISeparator | null | undefined; +} + +const enum TerminalLabelType { + Title = 'title', + Description = 'description' +} + +export class TerminalLabelComputer extends Disposable { + private _title: string = ''; + private _description: string = ''; + get title(): string | undefined { return this._title; } + get description(): string | undefined { return this._description; } + + private readonly _onDidChangeLabel = this._register(new Emitter<{ title: string, description: string }>()); + readonly onDidChangeLabel = this._onDidChangeLabel.event; + constructor( + private readonly _configHelper: TerminalConfigHelper, + private readonly _instance: Pick, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService + ) { + super(); + } + refreshLabel(): void { + this._title = this.computeLabel(this._configHelper.config.tabs.title, TerminalLabelType.Title); + this._description = this.computeLabel(this._configHelper.config.tabs.description, TerminalLabelType.Description); + if (this._title !== this._instance.title || this._description !== this._instance.description) { + this._onDidChangeLabel.fire({ title: this._title, description: this._description }); + } + } + + computeLabel( + labelTemplate: string, + labelType: TerminalLabelType + ) { + const templateProperties: ITerminalLabelTemplateProperties = { + cwd: this._instance.cwd || this._instance.initialCwd || '', + cwdFolder: '', + workspaceFolder: this._instance.workspaceFolder, + local: this._instance.shellLaunchConfig.description === 'Local' ? 'Local' : undefined, + process: this._instance.processName, + sequence: this._instance.sequence, + task: this._instance.shellLaunchConfig.description === 'Task' ? 'Task' : undefined, + separator: { label: this._configHelper.config.tabs.separator } + }; + labelTemplate = labelTemplate.trim(); + if (!labelTemplate) { + return labelType === TerminalLabelType.Title ? (this._instance.processName || '') : ''; + } + if (this._instance.staticTitle && labelType === TerminalLabelType.Title) { + return this._instance.staticTitle.replace(/[\n\r\t]/g, '') || templateProperties.process?.replace(/[\n\r\t]/g, '') || ''; + } + const detection = this._instance.capabilities.includes(ProcessCapability.CwdDetection); + const zeroRootWorkspace = this._workspaceContextService.getWorkspace().folders.length === 0 && this.pathsEqual(templateProperties.cwd, this._instance.userHome || this._configHelper.config.cwd); + const singleRootWorkspace = this._workspaceContextService.getWorkspace().folders.length === 1 && this.pathsEqual(templateProperties.cwd, this._configHelper.config.cwd || this._workspaceContextService.getWorkspace().folders[0]?.uri.fsPath); + templateProperties.cwdFolder = (!templateProperties.cwd || !detection || zeroRootWorkspace || singleRootWorkspace) ? '' : path.basename(templateProperties.cwd); + + //Remove special characters that could mess with rendering + let label = template(labelTemplate, (templateProperties as unknown) as { [key: string]: string | ISeparator | undefined | null; }).replace(/[\n\r\t]/g, '').trim(); + return label === '' && labelType === TerminalLabelType.Title ? (this._instance.processName || '') : label; + } + + pathsEqual(path1?: string | null, path2?: string) { + if (!path1 && !path2) { + return true; + } else if (!path1 || !path2) { + return false; + } else if (path1 === path2) { + return true; + } + const split1 = path1.includes('/') ? path1.split('/') : path1.split('\\'); + const split2 = path2.includes('/') ? path2.split('/') : path2.split('\\'); + if (split1.length !== split2.length) { + return false; + } + for (let i = 0; i < split1.length; i++) { + if (split1[i] !== split2[i]) { + return false; + } + } + return true; + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index ffa426f498..dcbc861471 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -32,6 +32,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst declare _serviceBrand: undefined; private readonly _localTerminalService?: ILocalTerminalService; private _terminalFocusContextKey: IContextKey; + private _terminalHasFixedWidth: IContextKey; private _terminalShellTypeContextKey: IContextKey; private _terminalAltBufferActiveContextKey: IContextKey; private _configHelper: TerminalConfigHelper; @@ -48,6 +49,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst super(); this._localTerminalService = localTerminalService; this._terminalFocusContextKey = TerminalContextKeys.focus.bindTo(this._contextKeyService); + this._terminalHasFixedWidth = TerminalContextKeys.terminalHasFixedWidth.bindTo(this._contextKeyService); this._terminalShellTypeContextKey = TerminalContextKeys.shellType.bindTo(this._contextKeyService); this._terminalAltBufferActiveContextKey = TerminalContextKeys.altBufferActive.bindTo(this._contextKeyService); this._configHelper = _instantiationService.createInstance(TerminalConfigHelper); @@ -59,6 +61,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst const shellLaunchConfig = this._convertProfileToShellLaunchConfig(config); const instance = this._instantiationService.createInstance(TerminalInstance, this._terminalFocusContextKey, + this._terminalHasFixedWidth, this._terminalShellTypeContextKey, this._terminalAltBufferActiveContextKey, this._configHelper, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 4cfb77dab0..eeef772618 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -9,15 +9,15 @@ import { Schemas } from 'vs/base/common/network'; import { localize } from 'vs/nls'; import { MenuRegistry, MenuId, IMenuActionOptions, MenuItemAction, IMenu } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyAndExpr, ContextKeyEqualsExpr, ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionTerminalProfile, ITerminalProfile, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { ICreateTerminalOptions, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, ITerminalLocationOptions, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalCommandId, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys, TerminalContextKeyStrings } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; -import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; const enum ContextMenuGroup { Create = '1_create', @@ -169,6 +169,17 @@ export function setupTerminalMenus(): void { group: ContextMenuGroup.Config } }, + { + id: MenuId.TerminalInstanceContext, + item: { + command: { + id: TerminalCommandId.SizeToContentWidth, + title: terminalStrings.toggleSizeToContentWidth + }, + group: ContextMenuGroup.Config + } + }, + { id: MenuId.TerminalInstanceContext, item: { @@ -183,6 +194,94 @@ export function setupTerminalMenus(): void { ] ); + MenuRegistry.appendMenuItems( + [ + { + id: MenuId.TerminalEditorInstanceContext, + item: { + group: ContextMenuGroup.Create, + command: { + id: TerminalCommandId.Split, + title: terminalStrings.split.value + } + } + }, + { + id: MenuId.TerminalEditorInstanceContext, + item: { + command: { + id: TerminalCommandId.New, + title: localize('workbench.action.terminal.new.short', "New Terminal") + }, + group: ContextMenuGroup.Create + } + }, + { + id: MenuId.TerminalEditorInstanceContext, + item: { + command: { + id: TerminalCommandId.KillEditor, + title: terminalStrings.kill.value + }, + group: ContextMenuGroup.Kill + } + }, + { + id: MenuId.TerminalEditorInstanceContext, + item: { + command: { + id: TerminalCommandId.CopySelection, + title: localize('workbench.action.terminal.copySelection.short', "Copy") + }, + group: ContextMenuGroup.Edit, + order: 1 + } + }, + { + id: MenuId.TerminalEditorInstanceContext, + item: { + command: { + id: TerminalCommandId.Paste, + title: localize('workbench.action.terminal.paste.short', "Paste") + }, + group: ContextMenuGroup.Edit, + order: 2 + } + }, + { + id: MenuId.TerminalEditorInstanceContext, + item: { + command: { + id: TerminalCommandId.Clear, + title: localize('workbench.action.terminal.clear', "Clear") + }, + group: ContextMenuGroup.Clear, + } + }, + { + id: MenuId.TerminalEditorInstanceContext, + item: { + command: { + id: TerminalCommandId.SelectAll, + title: localize('workbench.action.terminal.selectAll', "Select All"), + }, + group: ContextMenuGroup.Edit, + order: 3 + } + }, + { + id: MenuId.TerminalEditorInstanceContext, + item: { + command: { + id: TerminalCommandId.SizeToContentWidth, + title: terminalStrings.toggleSizeToContentWidth + }, + group: ContextMenuGroup.Config + } + } + ] + ); + MenuRegistry.appendMenuItems( [ { @@ -244,10 +343,10 @@ export function setupTerminalMenus(): void { }, group: 'navigation', order: 0, - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), ContextKeyExpr.not(`config.${TerminalSettingId.TabsEnabled}`) - ]), + ), } }, { @@ -260,8 +359,8 @@ export function setupTerminalMenus(): void { }, group: 'navigation', order: 0, - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), ContextKeyExpr.has(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.or( ContextKeyExpr.and( @@ -281,7 +380,7 @@ export function setupTerminalMenus(): void { ), ContextKeyExpr.equals(`config.${TerminalSettingId.TabsShowActiveTerminal}`, 'always') ) - ]), + ), } }, { @@ -294,8 +393,8 @@ export function setupTerminalMenus(): void { }, group: 'navigation', order: 2, - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), ContextKeyExpr.or( ContextKeyExpr.not(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.and( @@ -315,7 +414,7 @@ export function setupTerminalMenus(): void { ), ContextKeyExpr.equals(`config.${TerminalSettingId.TabsShowActions}`, 'always') ) - ]) + ) } }, { @@ -328,8 +427,8 @@ export function setupTerminalMenus(): void { }, group: 'navigation', order: 3, - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), ContextKeyExpr.or( ContextKeyExpr.not(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.and( @@ -349,7 +448,7 @@ export function setupTerminalMenus(): void { ), ContextKeyExpr.equals(`config.${TerminalSettingId.TabsShowActions}`, 'always') ) - ]) + ) } }, { @@ -361,9 +460,10 @@ export function setupTerminalMenus(): void { }, group: 'navigation', order: 0, - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID) - ]) + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), + ContextKeyExpr.or(TerminalContextKeys.webExtensionContributedProfile, TerminalContextKeys.processSupported) + ) } } ] @@ -387,7 +487,7 @@ export function setupTerminalMenus(): void { item: { command: { id: TerminalCommandId.MoveToEditor, - title: terminalStrings.moveToEditor.short + title: terminalStrings.moveToEditor.value }, group: ContextMenuGroup.Create, order: 2 @@ -397,8 +497,8 @@ export function setupTerminalMenus(): void { id: MenuId.TerminalInlineTabContext, item: { command: { - id: TerminalCommandId.ChangeIcon, - title: localize('workbench.action.terminal.changeIcon', "Change Icon...") + id: TerminalCommandId.ChangeIconPanel, + title: terminalStrings.changeIcon.value }, group: ContextMenuGroup.Edit } @@ -407,8 +507,8 @@ export function setupTerminalMenus(): void { id: MenuId.TerminalInlineTabContext, item: { command: { - id: TerminalCommandId.ChangeColor, - title: localize('workbench.action.terminal.changeColor', "Change Color...") + id: TerminalCommandId.ChangeColorPanel, + title: terminalStrings.changeColor.value }, group: ContextMenuGroup.Edit } @@ -417,8 +517,18 @@ export function setupTerminalMenus(): void { id: MenuId.TerminalInlineTabContext, item: { command: { - id: TerminalCommandId.Rename, - title: localize('workbench.action.terminal.rename', "Rename...") + id: TerminalCommandId.RenamePanel, + title: terminalStrings.rename.value + }, + group: ContextMenuGroup.Edit + } + }, + { + id: MenuId.TerminalInlineTabContext, + item: { + command: { + id: TerminalCommandId.SizeToContentWidthInstance, + title: localize('workbench.action.terminal.sizeToContentWidthInstance', "Toggle Size to Content Width") }, group: ContextMenuGroup.Edit } @@ -454,7 +564,7 @@ export function setupTerminalMenus(): void { item: { command: { id: TerminalCommandId.MoveToEditorInstance, - title: terminalStrings.moveToEditor.short + title: terminalStrings.moveToEditor.value }, group: ContextMenuGroup.Create, order: 2 @@ -490,6 +600,16 @@ export function setupTerminalMenus(): void { group: ContextMenuGroup.Edit } }, + { + id: MenuId.TerminalTabContext, + item: { + command: { + id: TerminalCommandId.SizeToContentWidthInstance, + title: localize('workbench.action.terminal.sizeToContentWidthInstance', "Toggle Size to Content Width") + }, + group: ContextMenuGroup.Edit + } + }, { id: MenuId.TerminalTabContext, item: { @@ -560,6 +680,14 @@ export function setupTerminalMenus(): void { when: ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeTerminal), group: '3_files' }); + MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { + command: { + id: TerminalCommandId.SizeToContentWidth, + title: terminalStrings.toggleSizeToContentWidth + }, + when: ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeTerminal), + group: '3_files' + }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { @@ -572,7 +700,7 @@ export function setupTerminalMenus(): void { }); } -export function getTerminalActionBarArgs(location: TerminalLocation, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], instantiationService: IInstantiationService, terminalService: ITerminalService, contextKeyService: IContextKeyService, commandService: ICommandService, dropdownMenu: IMenu): { +export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], instantiationService: IInstantiationService, terminalService: ITerminalService, contextKeyService: IContextKeyService, commandService: ICommandService, dropdownMenu: IMenu): { primaryAction: MenuItemAction, dropdownAction: IAction, dropdownMenuActions: IAction[], @@ -582,6 +710,7 @@ export function getTerminalActionBarArgs(location: TerminalLocation, profiles: I let dropdownActions: IAction[] = []; let submenuActions: IAction[] = []; + const splitLocation = (location === TerminalLocation.Editor || (typeof location === 'object' && 'viewColumn' in location && location.viewColumn === ACTIVE_GROUP)) ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; for (const p of profiles) { const isDefault = p.profileName === defaultProfileName; const options: IMenuActionOptions = { @@ -591,19 +720,26 @@ export function getTerminalActionBarArgs(location: TerminalLocation, profiles: I } as ICreateTerminalOptions, shouldForwardArgs: true }; + const splitOptions: IMenuActionOptions = { + arg: { + config: p, + splitLocation + } as ICreateTerminalOptions, + shouldForwardArgs: true + }; if (isDefault) { dropdownActions.unshift(new MenuItemAction({ id: TerminalCommandId.NewWithProfile, title: localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, options, contextKeyService, commandService)); - submenuActions.unshift(new MenuItemAction({ id: TerminalCommandId.Split, title: localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, options, contextKeyService, commandService)); + submenuActions.unshift(new MenuItemAction({ id: TerminalCommandId.Split, title: localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, splitOptions, contextKeyService, commandService)); } else { dropdownActions.push(new MenuItemAction({ id: TerminalCommandId.NewWithProfile, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, options, contextKeyService, commandService)); - submenuActions.push(new MenuItemAction({ id: TerminalCommandId.Split, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, options, contextKeyService, commandService)); + submenuActions.push(new MenuItemAction({ id: TerminalCommandId.Split, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, splitOptions, contextKeyService, commandService)); } } for (const contributed of contributedProfiles) { const isDefault = contributed.title === defaultProfileName; const title = isDefault ? localize('defaultTerminalProfile', "{0} (Default)", contributed.title.replace(/[\n\r\t]/g, '')) : contributed.title.replace(/[\n\r\t]/g, ''); - dropdownActions.push(new Action(TerminalCommandId.NewWithProfile, title, undefined, true, () => terminalService.createTerminal({ + dropdownActions.push(new Action('contributed', title, undefined, true, () => terminalService.createTerminal({ config: { extensionIdentifier: contributed.extensionIdentifier, id: contributed.id, @@ -611,8 +747,7 @@ export function getTerminalActionBarArgs(location: TerminalLocation, profiles: I }, location }))); - const splitLocation = location === TerminalLocation.Editor ? { viewColumn: SIDE_GROUP } : location; - submenuActions.push(new Action(TerminalCommandId.NewWithProfile, title, undefined, true, () => terminalService.createTerminal({ + submenuActions.push(new Action('contributed-split', title, undefined, true, () => terminalService.createTerminal({ config: { extensionIdentifier: contributed.extensionIdentifier, id: contributed.id, @@ -622,8 +757,14 @@ export function getTerminalActionBarArgs(location: TerminalLocation, profiles: I }))); } + const defaultProfileAction = dropdownActions.find(d => d.label.endsWith('(Default)')); + if (defaultProfileAction) { + dropdownActions = dropdownActions.filter(d => d !== defaultProfileAction).sort((a, b) => a.label.localeCompare(b.label)); + dropdownActions.unshift(defaultProfileAction); + } + if (dropdownActions.length > 0) { - dropdownActions.push(new SubmenuAction('split.profile', 'Split...', submenuActions)); + dropdownActions.push(new SubmenuAction('split.profile', localize('splitTerminal', 'Split Terminal'), submenuActions)); dropdownActions.push(new Separator()); } @@ -636,22 +777,17 @@ export function getTerminalActionBarArgs(location: TerminalLocation, profiles: I } } - const defaultProfileAction = dropdownActions.find(d => d.label.endsWith('(Default)')); - if (defaultProfileAction) { - dropdownActions = dropdownActions.filter(d => d !== defaultProfileAction); - dropdownActions.unshift(defaultProfileAction); - } - const defaultSubmenuProfileAction = submenuActions.find(d => d.label.endsWith('(Default)')); if (defaultSubmenuProfileAction) { - submenuActions = submenuActions.filter(d => d !== defaultSubmenuProfileAction); + submenuActions = submenuActions.filter(d => d !== defaultSubmenuProfileAction).sort((a, b) => a.label.localeCompare(b.label)); submenuActions.unshift(defaultSubmenuProfileAction); } + const primaryActionLocation = terminalService.resolveLocation(location); const primaryAction = instantiationService.createInstance( MenuItemAction, { - id: location === TerminalLocation.Panel ? TerminalCommandId.New : TerminalCommandId.CreateTerminalEditor, + id: primaryActionLocation === TerminalLocation.Editor ? TerminalCommandId.CreateTerminalEditor : TerminalCommandId.New, title: localize('terminal.new', "New Terminal"), icon: Codicon.plus }, @@ -666,5 +802,5 @@ export function getTerminalActionBarArgs(location: TerminalLocation, profiles: I }); const dropdownAction = new Action('refresh profiles', 'Launch Profile...', 'codicon-chevron-down', true); - return { primaryAction, dropdownAction, dropdownMenuActions: dropdownActions, className: 'terminal-tab-actions' }; + return { primaryAction, dropdownAction, dropdownMenuActions: dropdownActions, className: `terminal-tab-actions-${terminalService.resolveLocation(location)}` }; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index 3c4f96bfd3..37a8a8ee20 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -5,26 +5,19 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, IProcessProperty, ProcessPropertyType, ProcessCapability } from 'vs/platform/terminal/common/terminal'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; export class TerminalProcessExtHostProxy extends Disposable implements ITerminalChildProcess, ITerminalProcessExtHostProxy { readonly id = 0; readonly shouldPersist = false; - + private _capabilities: ProcessCapability[] = []; + get capabilities(): ProcessCapability[] { return this._capabilities; } private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData: Event = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter()); - readonly onProcessExit: Event = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter()); get onProcessReady(): Event { return this._onProcessReady.event; } - private readonly _onProcessTitleChanged = this._register(new Emitter()); - readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter()); - get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); - get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessResolvedShellLaunchConfig.event; } private readonly _onStart = this._register(new Emitter()); readonly onStart: Event = this._onStart.event; @@ -44,8 +37,10 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal readonly onRequestCwd: Event = this._onRequestCwd.event; private readonly _onRequestLatency = this._register(new Emitter()); readonly onRequestLatency: Event = this._onRequestLatency.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; + private readonly _onDidChangeProperty = this._register(new Emitter>()); + readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter()); + readonly onProcessExit: Event = this._onProcessExit.event; private _pendingInitialCwdRequests: ((value: string | PromiseLike) => void)[] = []; @@ -66,11 +61,31 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal } emitTitle(title: string): void { - this._onProcessTitleChanged.fire(title); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: title }); } emitReady(pid: number, cwd: string): void { - this._onProcessReady.fire({ pid, cwd }); + this._onProcessReady.fire({ pid, cwd, capabilities: this.capabilities }); + } + + emitProcessProperty({ type, value }: IProcessProperty): void { + switch (type) { + case ProcessPropertyType.Cwd: + this.emitCwd(value); + break; + case ProcessPropertyType.InitialCwd: + this.emitInitialCwd(value); + break; + case ProcessPropertyType.Title: + this.emitTitle(value); + break; + case ProcessPropertyType.OverrideDimensions: + this.emitOverrideDimensions(value); + break; + case ProcessPropertyType.ResolvedShellLaunchConfig: + this.emitResolvedShellLaunchConfig(value); + break; + } } emitExit(exitCode: number | undefined): void { @@ -79,11 +94,11 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal } emitOverrideDimensions(dimensions: ITerminalDimensions | undefined): void { - this._onProcessOverrideDimensions.fire(dimensions); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: dimensions }); } emitResolvedShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig): void { - this._onProcessResolvedShellLaunchConfig.fire(shellLaunchConfig); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.ResolvedShellLaunchConfig, value: shellLaunchConfig }); } emitInitialCwd(initialCwd: string): void { @@ -153,4 +168,17 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal this._pendingLatencyRequests.push(resolve); }); } + + async refreshProperty(type: ProcessPropertyType): Promise { + if (type === ProcessPropertyType.Cwd) { + return this.getCwd(); + } else if (type === ProcessPropertyType.InitialCwd) { + return this.getInitialCwd(); + } + } + + async updateProperty(type: ProcessPropertyType, value: any): Promise { + if (type === ProcessPropertyType.FixedDimensions && type === ProcessPropertyType.FixedDimensions && typeof value !== 'string' && value && ('cols' in value || 'rows' in value)) { + } + } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 92bd3e9ec5..abd3f1c81f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -22,7 +22,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IEnvironmentVariableInfo, IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, TerminalShellType, ITerminalDimensions, TerminalSettingId, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, ITerminalDimensions, TerminalSettingId, IProcessReadyEvent, IProcessProperty, ProcessPropertyType, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; import { localize } from 'vs/nls'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; @@ -94,20 +94,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce readonly onBeforeProcessData = this._onBeforeProcessData.event; private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData = this._onProcessData.event; - private readonly _onProcessTitle = this._register(new Emitter()); - readonly onProcessTitle = this._onProcessTitle.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onProcessExit = this._register(new Emitter()); - readonly onProcessExit = this._onProcessExit.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; - private readonly _onProcessDidChangeHasChildProcesses = this._register(new Emitter()); - readonly onProcessDidChangeHasChildProcesses = this._onProcessDidChangeHasChildProcesses.event; + private readonly _onDidChangeProperty = this._register(new Emitter>()); + readonly onDidChangeProperty = this._onDidChangeProperty.event; private readonly _onEnvironmentVariableInfoChange = this._register(new Emitter()); readonly onEnvironmentVariableInfoChanged = this._onEnvironmentVariableInfoChange.event; + private readonly _onProcessExit = this._register(new Emitter()); + readonly onProcessExit = this._onProcessExit.event; get persistentProcessId(): number | undefined { return this._process?.id; } get shouldPersist(): boolean { return this._process ? this._process.shouldPersist : false; } @@ -179,15 +171,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } async detachFromProcess(): Promise { - if (!this._process) { - return; - } - if (this._process.detach) { - await this._process.detach(); - } else { - throw new Error('This terminal process does not support detaching'); - } - this._process = null; + await this._process?.detach?.(); } async createProcess( @@ -267,7 +251,15 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce 'terminal.integrated.cwd': this._configurationService.getValue(TerminalSettingId.Cwd) as string, 'terminal.integrated.detectLocale': terminalConfig.detectLocale }; - newProcess = await this._remoteTerminalService.createProcess(shellLaunchConfig, configuration, activeWorkspaceRootUri, cols, rows, this._configHelper.config.unicodeVersion, shouldPersist); + try { + newProcess = await this._remoteTerminalService.createProcess(shellLaunchConfig, configuration, activeWorkspaceRootUri, cols, rows, this._configHelper.config.unicodeVersion, shouldPersist); + } catch (e) { + if (e?.message === 'Could not fetch remote environment') { + this._logService.trace(`Could not fetch remote environment, silently failing`); + return undefined; + } + throw e; + } } if (!this._isDisposed) { this._setupPtyHostListeners(this._remoteTerminalService); @@ -313,6 +305,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce newProcess.onProcessReady((e: IProcessReadyEvent) => { this.shellProcessId = e.pid; this._initialCwd = e.cwd; + this._onDidChangeProperty.fire({ type: ProcessPropertyType.InitialCwd, value: this._initialCwd }); this._onProcessReady.fire(e); if (this._preLaunchInputQueue.length > 0 && this._process) { @@ -321,22 +314,16 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._preLaunchInputQueue.length = 0; } }), - newProcess.onProcessTitleChanged(title => this._onProcessTitle.fire(title)), - newProcess.onProcessShellTypeChanged(type => this._onProcessShellTypeChanged.fire(type)), - newProcess.onProcessExit(exitCode => this._onExit(exitCode)) + newProcess.onProcessExit(exitCode => this._onExit(exitCode)), + newProcess.onDidChangeProperty(({ type, value }) => { + switch (type) { + case ProcessPropertyType.HasChildProcesses: + this._hasChildProcesses = value; + break; + } + this._onDidChangeProperty.fire({ type, value }); + }) ]; - if (newProcess.onProcessOverrideDimensions) { - this._processListeners.push(newProcess.onProcessOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e))); - } - if (newProcess.onProcessResolvedShellLaunchConfig) { - this._processListeners.push(newProcess.onProcessResolvedShellLaunchConfig(e => this._onProcessResolvedShellLaunchConfig.fire(e))); - } - if (newProcess.onDidChangeHasChildProcesses) { - this._processListeners.push(newProcess.onDidChangeHasChildProcesses(e => { - this._hasChildProcesses = e; - this._onProcessDidChangeHasChildProcesses.fire(e); - })); - } setTimeout(() => { if (this.processState === ProcessState.Launching) { @@ -559,13 +546,23 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce return Promise.resolve(this._latency); } + async refreshProperty(type: ProcessPropertyType): Promise { + if (!this._process) { + throw new Error('Cannot refresh property when process is undefined'); + } + return this._process.refreshProperty(type); + } + + async updateProperty(type: ProcessPropertyType, value: any): Promise { + return this._process?.updateProperty(type, value); + } + acknowledgeDataEvent(charCount: number): void { this._ackDataBufferer.ack(charCount); } private _onExit(exitCode: number | undefined): void { this._process = null; - // If the process is marked as launching then mark the process as killed // during launch. This typically means that there is a problem with the // shell and args. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index bf5f12f391..3b99f3c32f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -22,6 +22,7 @@ import { debounce } from 'vs/base/common/decorators'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { URI, UriComponents } from 'vs/base/common/uri'; import { equals } from 'vs/base/common/arrays'; +import { deepClone } from 'vs/base/common/objects'; export interface IProfileContextProvider { getDefaultSystemShell: (remoteAuthority: string | undefined, os: OperatingSystem) => Promise; @@ -179,6 +180,15 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro typeof (thing).scheme === 'string'; } + private _setIconForAutomation(options: IShellLaunchConfigResolveOptions, profile: ITerminalProfile): ITerminalProfile { + if (options.allowAutomationShell) { + const profileClone = deepClone(profile); + profileClone.icon = Codicon.tools; + return profileClone; + } + return profile; + } + private async _getUnresolvedDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { // If automation shell is allowed, prefer that if (options.allowAutomationShell) { @@ -192,7 +202,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro // allow users to migrate, see https://github.com/microsoft/vscode/issues/123171 const shellSettingProfile = await this._getUnresolvedShellSettingDefaultProfile(options); if (shellSettingProfile) { - return shellSettingProfile; + return this._setIconForAutomation(options, shellSettingProfile); } // Return the real default profile if it exists and is valid, wait for profiles to be ready @@ -200,16 +210,16 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro await this._terminalService.profilesReady; const defaultProfile = this._getUnresolvedRealDefaultProfile(options.os); if (defaultProfile) { - return defaultProfile; + return this._setIconForAutomation(options, defaultProfile); } // If there is no real default profile, create a fallback default profile based on the shell // and shellArgs settings in addition to the current environment. - return this._getUnresolvedFallbackDefaultProfile(options); + return this._setIconForAutomation(options, await this._getUnresolvedFallbackDefaultProfile(options)); } private _getUnresolvedRealDefaultProfile(os: OperatingSystem): ITerminalProfile | undefined { - const defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}.${this._getOsKey(os)}`); + const defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${this._getOsKey(os)}`); if (defaultProfileName && typeof defaultProfileName === 'string') { return this._terminalService.availableProfiles.find(e => e.profileName === defaultProfileName); } @@ -217,9 +227,9 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } private async _getUnresolvedShellSettingDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { - let executable = this._configurationService.getValue(`${TerminalSettingPrefix.Shell}.${this._getOsKey(options.os)}`); + let executable = this._configurationService.getValue(`${TerminalSettingPrefix.Shell}${this._getOsKey(options.os)}`); if (!this._isValidShell(executable)) { - const shellArgs = this._configurationService.inspect(`${TerminalSettingPrefix.ShellArgs}.${this._getOsKey(options.os)}`); + const shellArgs = this._configurationService.inspect(`${TerminalSettingPrefix.ShellArgs}${this._getOsKey(options.os)}`); // && !this.getSafeConfigValue('shellArgs', options.os, false)) { if (!shellArgs.userValue && !shellArgs.workspaceValue) { return undefined; @@ -231,7 +241,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } let args: string | string[] | undefined; - const shellArgsSetting = this._configurationService.getValue(`${TerminalSettingPrefix.ShellArgs}.${this._getOsKey(options.os)}`); + const shellArgsSetting = this._configurationService.getValue(`${TerminalSettingPrefix.ShellArgs}${this._getOsKey(options.os)}`); if (this._isValidShellArgs(shellArgsSetting, options.os)) { args = shellArgsSetting; } @@ -260,8 +270,12 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro const executable = await this._context.getDefaultSystemShell(options.remoteAuthority, options.os); // Try select an existing profile to fallback to, based on the default system shell - const existingProfile = this._terminalService.availableProfiles.find(e => path.parse(e.path).name === path.parse(executable).name); + let existingProfile = this._terminalService.availableProfiles.find(e => path.parse(e.path).name === path.parse(executable).name); if (existingProfile) { + if (options.allowAutomationShell) { + existingProfile = deepClone(existingProfile); + existingProfile.icon = Codicon.tools; + } return existingProfile; } @@ -288,14 +302,23 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro private _getUnresolvedAutomationShellProfile(options: IShellLaunchConfigResolveOptions): ITerminalProfile | undefined { const automationShell = this._configurationService.getValue(`terminal.integrated.automationShell.${this._getOsKey(options.os)}`); - if (!automationShell || typeof automationShell !== 'string') { - return undefined; + if (automationShell && typeof automationShell === 'string') { + return { + path: automationShell, + profileName: generatedProfileName, + isDefault: false, + icon: Codicon.tools + }; } - return { - path: automationShell, - profileName: generatedProfileName, - isDefault: false - }; + + // Use automationProfile second + const automationProfile = this._configurationService.getValue(`terminal.integrated.automationProfile.${this._getOsKey(options.os)}`); + if (this._isValidAutomationProfile(automationProfile, options.os)) { + automationProfile.icon = automationProfile.icon ?? Codicon.tools; + return automationProfile; + } + + return undefined; } private async _resolveProfile(profile: ITerminalProfile, options: IShellLaunchConfigResolveOptions): Promise { @@ -422,6 +445,16 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro return createdProfile; } + private _isValidAutomationProfile(profile: unknown, os: OperatingSystem): profile is ITerminalProfile { + if (!profile === undefined || typeof profile !== 'object' || profile === null) { + return false; + } + if ('path' in profile && typeof (profile as { path: unknown }).path === 'string') { + return true; + } + return false; + } + private _argsMatch(args1: string | string[] | undefined, args2: string | string[] | undefined): boolean { if (!args1 && !args2) { return true; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts index 829b5567ef..aecb983169 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts @@ -15,6 +15,7 @@ import { killTerminalIcon, renameTerminalIcon } from 'vs/workbench/contrib/termi import { getColorClass, getIconId, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; let terminalPicks: Array = []; export class TerminalQuickAccessProvider extends PickerQuickAccessProvider { @@ -22,6 +23,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider { if (terminal.target === TerminalLocation.Editor) { - this._terminalEditorService.openEditor(terminal); + const existingEditors = this._editorService.findEditors(terminal.resource); + this._terminalEditorService.openEditor(terminal, { viewColumn: existingEditors?.[0].groupId }); this._terminalEditorService.setActiveInstance(terminal); } else { this._terminalGroupService.showPanel(!event.inBackground); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 59e697e9d9..a95a5a6980 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -23,7 +23,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalProfileType, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; +import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; import { iconForeground } from 'vs/platform/theme/common/colorRegistry'; import { IconDefinition } from 'vs/platform/theme/common/iconRegistry'; @@ -31,11 +31,11 @@ import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; -import { ICreateTerminalOptions, IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalProfileProvider, ITerminalService, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalProfileProvider, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { refreshTerminalActions } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; -import { getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; +import { getColorClass, getColorStyleContent, getColorStyleElement, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { getInstanceFromResource, getTerminalUri, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; @@ -48,6 +48,8 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService, ShutdownReason, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; export class TerminalService implements ITerminalService { declare _serviceBrand: undefined; @@ -55,6 +57,7 @@ export class TerminalService implements ITerminalService { private _hostActiveTerminals: Map = new Map(); private _isShuttingDown: boolean; + private _ifNoProfilesTryAgain: boolean = true; private _backgroundedTerminalInstances: ITerminalInstance[] = []; private _backgroundedTerminalDisposables: Map = new Map(); private _findState: FindReplaceState; @@ -62,16 +65,20 @@ export class TerminalService implements ITerminalService { private _linkProviders: Set = new Set(); private _linkProviderDisposables: Map = new Map(); private _processSupportContextKey: IContextKey; + private _webExtensionContributedProfileContextKey: IContextKey; + private _terminalHasBeenCreated: IContextKey; private readonly _localTerminalService?: ILocalTerminalService; private readonly _primaryOffProcessTerminalService?: IOffProcessTerminalService; private _defaultProfileName?: string; private _profilesReadyBarrier: AutoOpenBarrier; private _availableProfiles: ITerminalProfile[] | undefined; - private _contributedProfiles: IExtensionTerminalProfile[] | undefined; + private _contributedProfiles: IExtensionTerminalProfile[] = []; private _configHelper: TerminalConfigHelper; private _remoteTerminalsInitPromise: Promise | undefined; private _localTerminalsInitPromise: Promise | undefined; private _connectionState: TerminalConnectionState; + private _nativeDelegate?: ITerminalServiceNativeDelegate; + private _shutdownWindowCount?: number; private _editable: { instance: ITerminalInstance, data: IEditableData } | undefined; @@ -82,14 +89,8 @@ export class TerminalService implements ITerminalService { this._refreshAvailableProfiles(); return this._availableProfiles || []; } - get allProfiles(): ITerminalProfileType[] | undefined { - if (this._availableProfiles) { - const profiles: ITerminalProfileType[] = []; - profiles.concat(this._availableProfiles); - profiles.concat(this._terminalContributionService.terminalProfiles); - return profiles; - } - return undefined; + get contributedProfiles(): IExtensionTerminalProfile[] { + return this._contributedProfiles || []; } get configHelper(): ITerminalConfigHelper { return this._configHelper; } get instances(): ITerminalInstance[] { @@ -173,6 +174,7 @@ export class TerminalService implements ITerminalService { @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @IEditorResolverService editorResolverService: IEditorResolverService, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, @IExtensionService private readonly _extensionService: IExtensionService, @INotificationService private readonly _notificationService: INotificationService, @IThemeService private readonly _themeService: IThemeService, @@ -182,7 +184,6 @@ export class TerminalService implements ITerminalService { this._isShuttingDown = false; this._findState = new FindReplaceState(); this._configHelper = _instantiationService.createInstance(TerminalConfigHelper); - editorResolverService.registerEditor( `${Schemas.vscodeTerminal}:/**`, { @@ -228,6 +229,11 @@ export class TerminalService implements ITerminalService { // we update detected profiles when an instance is created so that, // for example, we detect if you've installed a pwsh this.onDidCreateInstance(() => this._refreshAvailableProfiles()); + + // in web, we don't want to show the dropdown unless there's a web extension + // that contributes a profile + this._extensionService.onDidChangeExtensions(() => this._refreshAvailableProfiles()); + this.onDidReceiveInstanceLinks(instance => this._setInstanceLinkProviders(instance)); // Hide the panel if there are no more instances, provided that VS Code is not shutting @@ -242,6 +248,8 @@ export class TerminalService implements ITerminalService { this._handleInstanceContextKeys(); this._processSupportContextKey = TerminalContextKeys.processSupported.bindTo(this._contextKeyService); this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); + this._webExtensionContributedProfileContextKey = TerminalContextKeys.webExtensionContributedProfile.bindTo(this._contextKeyService); + this._terminalHasBeenCreated = TerminalContextKeys.terminalHasBeenCreated.bindTo(this._contextKeyService); lifecycleService.onBeforeShutdown(async e => e.veto(this._onBeforeShutdown(e.reason), 'veto.terminal')); lifecycleService.onWillShutdown(e => this._onWillShutdown(e)); @@ -251,7 +259,7 @@ export class TerminalService implements ITerminalService { if (e.affectsConfiguration(TerminalSettingPrefix.DefaultProfile + platformKey) || e.affectsConfiguration(TerminalSettingPrefix.Profiles + platformKey) || e.affectsConfiguration(TerminalSettingId.UseWslProfiles)) { - this._refreshAvailableProfiles(); + await this._refreshAvailableProfiles(); } }); @@ -271,18 +279,25 @@ export class TerminalService implements ITerminalService { this._connectionState = TerminalConnectionState.Connecting; const isPersistentRemote = !!this._environmentService.remoteAuthority && enableTerminalReconnection; - let initPromise: Promise = isPersistentRemote - ? this._remoteTerminalsInitPromise = this._reconnectToRemoteTerminals() - : enableTerminalReconnection - ? this._localTerminalsInitPromise = this._reconnectToLocalTerminals() - : Promise.resolve(); + + if (isPersistentRemote) { + this._remoteTerminalsInitPromise = this._reconnectToRemoteTerminals(); + } else if (enableTerminalReconnection) { + this._localTerminalsInitPromise = this._reconnectToLocalTerminals(); + } else { + this._connectionState = TerminalConnectionState.Connected; + } this._primaryOffProcessTerminalService = !!this._environmentService.remoteAuthority ? this._remoteTerminalService : (this._localTerminalService || this._remoteTerminalService); this._primaryOffProcessTerminalService.onDidRequestDetach(async (e) => { const instanceToDetach = this.getInstanceFromResource(getTerminalUri(e.workspaceId, e.instanceId)); if (instanceToDetach) { const persistentProcessId = instanceToDetach?.persistentProcessId; if (persistentProcessId && !instanceToDetach.shellLaunchConfig.isFeatureTerminal && !instanceToDetach.shellLaunchConfig.customPtyImplementation) { - this._terminalEditorService.detachInstance(instanceToDetach); + if (instanceToDetach.target === TerminalLocation.Editor) { + this._terminalEditorService.detachInstance(instanceToDetach); + } else { + this._terminalGroupService.getGroupForInstance(instanceToDetach)?.removeInstance(instanceToDetach); + } await instanceToDetach.detachFromProcess(); await this._primaryOffProcessTerminalService?.acceptDetachInstanceReply(e.requestId, persistentProcessId); } else { @@ -292,8 +307,6 @@ export class TerminalService implements ITerminalService { } }); - initPromise.then(() => this._setConnected()); - // Wait up to 5 seconds for profiles to be ready so it's assured that we know the actual // default terminal before launching the first terminal. This isn't expected to ever take // this long. @@ -371,7 +384,7 @@ export class TerminalService implements ITerminalService { private async _reconnectToRemoteTerminals(): Promise { const layoutInfo = await this._remoteTerminalService.getTerminalLayoutInfo(); this._remoteTerminalService.reduceConnectionGraceTime(); - const reconnectCounter = this._recreateTerminalGroups(layoutInfo); + const reconnectCounter = await this._recreateTerminalGroups(layoutInfo); /* __GDPR__ "terminalReconnection" : { "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } @@ -392,7 +405,7 @@ export class TerminalService implements ITerminalService { } const layoutInfo = await this._localTerminalService.getTerminalLayoutInfo(); if (layoutInfo && layoutInfo.tabs.length > 0) { - this._recreateTerminalGroups(layoutInfo); + await this._recreateTerminalGroups(layoutInfo); } // now that terminals have been restored, // attach listeners to update local state when terminals are changed @@ -422,7 +435,7 @@ export class TerminalService implements ITerminalService { } } else { // add split terminals to this group - await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, location: { parentTerminal: terminalInstance } }); + terminalInstance = await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, location: { parentTerminal: terminalInstance } }); } } const activeInstance = this.instances.find(t => { @@ -491,22 +504,50 @@ export class TerminalService implements ITerminalService { } @throttle(2000) - private async _refreshAvailableProfiles(): Promise { - const result = await this._detectProfiles(); - const profilesChanged = !equals(result, this._availableProfiles); - const contributedProfilesChanged = !equals(this._terminalContributionService.terminalProfiles, this._contributedProfiles); + private _refreshAvailableProfiles(): void { + this._refreshAvailableProfilesNow(); + } + + private async _refreshAvailableProfilesNow(): Promise { + const platformKey = await this._getPlatformKey(); + const profiles = await this._detectProfiles(); + const profilesChanged = !equals(profiles, this._availableProfiles); + const excludedContributedProfiles: string[] = []; + const configProfiles: { [key: string]: any } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey); + for (const [profileName, value] of Object.entries(configProfiles)) { + if (value === null) { + excludedContributedProfiles.push(profileName); + } + } + const filteredContributedProfiles = Array.from(this._terminalContributionService.terminalProfiles.filter(p => !excludedContributedProfiles.includes(p.title))); + const contributedProfilesChanged = !equals(filteredContributedProfiles, this._contributedProfiles); + + if (profiles.length === 0 && this._ifNoProfilesTryAgain) { + // available profiles get updated when a terminal is created + // or relevant config changes. + // if there are no profiles, we want to refresh them again + // since terminal creation can't happen in this case and users + // might not think to try changing the config + this._ifNoProfilesTryAgain = false; + await this._refreshAvailableProfilesNow(); + } if (profilesChanged || contributedProfilesChanged) { - this._availableProfiles = result; - this._contributedProfiles = Array.from(this._terminalContributionService.terminalProfiles); + this._availableProfiles = profiles; + this._contributedProfiles = filteredContributedProfiles; this._onDidChangeAvailableProfiles.fire(this._availableProfiles); this._profilesReadyBarrier.open(); - await this._refreshPlatformConfig(result); + this._updateWebContextKey(); + await this._refreshPlatformConfig(profiles); } } + private _updateWebContextKey(): void { + this._webExtensionContributedProfileContextKey.set(isWeb && this._contributedProfiles.length > 0); + } + private async _refreshPlatformConfig(profiles: ITerminalProfile[]) { const env = await this._remoteAgentService.getEnvironment(); - registerTerminalDefaultProfileConfiguration({ os: env?.os || OS, profiles }, this._terminalContributionService.terminalProfiles); + registerTerminalDefaultProfileConfiguration({ os: env?.os || OS, profiles }, this._contributedProfiles); refreshTerminalActions(profiles); } @@ -527,19 +568,38 @@ export class TerminalService implements ITerminalService { } private _onBeforeShutdown(reason: ShutdownReason): boolean | Promise { + // Never veto on web as this would block all windows from being closed. This disables + // process revive as we can't handle it on shutdown. + if (isWeb) { + this._isShuttingDown = true; + return false; + } + return this._onBeforeShutdownAsync(reason); + } + + private async _onBeforeShutdownAsync(reason: ShutdownReason): Promise { if (this.instances.length === 0) { // No terminal instances, don't veto return false; } - const shouldPersistTerminals = this._configHelper.config.enablePersistentSessions && reason === ShutdownReason.RELOAD; - if (!shouldPersistTerminals) { + // Persist terminal _buffer state_, note that even if this happens the dirty terminal prompt + // still shows as that cannot be revived + this._shutdownWindowCount = await this._nativeDelegate?.getWindowCount(); + const shouldReviveProcesses = this._shouldReviveProcesses(reason); + if (shouldReviveProcesses) { + await this._primaryOffProcessTerminalService?.persistTerminalState(); + } + + // Persist terminal _processes_ + const shouldPersistProcesses = this._configHelper.config.enablePersistentSessions && reason === ShutdownReason.RELOAD; + if (!shouldPersistProcesses) { const hasDirtyInstances = ( (this.configHelper.config.confirmOnExit === 'always' && this.instances.length > 0) || (this.configHelper.config.confirmOnExit === 'hasChildProcesses' && this.instances.some(e => e.hasChildProcesses)) ); if (hasDirtyInstances) { - return this._onBeforeShutdownAsync(); + return this._onBeforeShutdownConfirmation(reason); } } @@ -548,12 +608,34 @@ export class TerminalService implements ITerminalService { return false; } - private async _onBeforeShutdownAsync(): Promise { + setNativeDelegate(nativeDelegate: ITerminalServiceNativeDelegate): void { + this._nativeDelegate = nativeDelegate; + } + + private _shouldReviveProcesses(reason: ShutdownReason): boolean { + if (!this._configHelper.config.enablePersistentSessions) { + return false; + } + switch (this.configHelper.config.persistentSessionReviveProcess) { + case 'onExit': { + // Allow on close if it's the last window on Windows or Linux + if (reason === ShutdownReason.CLOSE && (this._shutdownWindowCount === 1 && !isMacintosh)) { + return true; + } + return reason === ShutdownReason.LOAD || reason === ShutdownReason.QUIT; + } + case 'onExitAndWindowClose': return reason !== ShutdownReason.RELOAD; + default: return false; + } + } + + private async _onBeforeShutdownConfirmation(reason: ShutdownReason): Promise { // veto if configured to show confirmation and the user chose not to exit const veto = await this._showTerminalCloseConfirmation(); if (!veto) { this._isShuttingDown = true; } + return veto; } @@ -561,14 +643,21 @@ export class TerminalService implements ITerminalService { // Don't touch processes if the shutdown was a result of reload as they will be reattached const shouldPersistTerminals = this._configHelper.config.enablePersistentSessions && e.reason === ShutdownReason.RELOAD; if (shouldPersistTerminals) { - this.instances.forEach(instance => instance.detachFromProcess()); + for (const instance of this.instances) { + instance.detachFromProcess(); + } return; } // Force dispose of all terminal instances - this.instances.forEach(instance => instance.dispose(true)); + for (const instance of this.instances) { + instance.dispose(); + } - this._localTerminalService?.setTerminalLayoutInfo(undefined); + // Clear terminal layout info only when not persisting + if (!this._shouldReviveProcesses(e.reason)) { + this._primaryOffProcessTerminalService?.setTerminalLayoutInfo(undefined); + } } getFindState(): FindReplaceState { @@ -577,6 +666,10 @@ export class TerminalService implements ITerminalService { @debounce(500) private _saveState(): void { + // Avoid saving state when shutting down as that would override process state to be revived + if (this._isShuttingDown) { + return; + } if (!this.configHelper.config.enablePersistentSessions) { return; } @@ -644,8 +737,10 @@ export class TerminalService implements ITerminalService { async initializeTerminals(): Promise { if (this._remoteTerminalsInitPromise) { await this._remoteTerminalsInitPromise; + this._setConnected(); } else if (this._localTerminalsInitPromise) { await this._localTerminalsInitPromise; + this._setConnected(); } if (this._terminalGroupService.groups.length === 0 && this.isProcessSupportRegistered) { this.createTerminal({ location: TerminalLocation.Panel }); @@ -852,6 +947,7 @@ export class TerminalService implements ITerminalService { const platformKey = await this._getPlatformKey(); const profilesKey = `${TerminalSettingPrefix.Profiles}${platformKey}`; const defaultProfileKey = `${TerminalSettingPrefix.DefaultProfile}${platformKey}`; + const defaultProfileName = this._configurationService.getValue(defaultProfileKey); const options: IPickOptions = { placeHolder: type === 'createInstance' ? nls.localize('terminal.integrated.selectProfileToCreate', "Select the terminal profile to create") : nls.localize('terminal.integrated.chooseDefaultProfile', "Select your default terminal profile"), @@ -891,13 +987,15 @@ export class TerminalService implements ITerminalService { const quickPickItems: (IProfileQuickPickItem | IQuickPickSeparator)[] = []; const configProfiles = profiles.filter(e => !e.isAutoDetected); const autoDetectedProfiles = profiles.filter(e => e.isAutoDetected); + if (configProfiles.length > 0) { quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles', "profiles") }); - quickPickItems.push(...configProfiles.map(e => this._createProfileQuickPickItem(e))); + quickPickItems.push(...this._sortProfileQuickPickItems(configProfiles.map(e => this._createProfileQuickPickItem(e)), defaultProfileName)); } quickPickItems.push({ type: 'separator', label: nls.localize('ICreateContributedTerminalProfileOptions', "contributed") }); - for (const contributed of this._terminalContributionService.terminalProfiles) { + const contributedProfiles: IProfileQuickPickItem[] = []; + for (const contributed of this.contributedProfiles) { if (typeof contributed.icon === 'string' && contributed.icon.startsWith('$(')) { contributed.icon = contributed.icon.substring(2, contributed.icon.length - 1); } @@ -911,7 +1009,7 @@ export class TerminalService implements ITerminalService { if (colorClass) { iconClasses.push(colorClass); } - quickPickItems.push({ + contributedProfiles.push({ label: `$(${icon.id}) ${contributed.title}`, profile: { extensionIdentifier: contributed.extensionIdentifier, @@ -920,16 +1018,24 @@ export class TerminalService implements ITerminalService { id: contributed.id, color: contributed.color }, + profileName: contributed.title, iconClasses }); } - if (autoDetectedProfiles.length > 0) { - quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles.detected', "detected") }); - quickPickItems.push(...autoDetectedProfiles.map(e => this._createProfileQuickPickItem(e))); + if (contributedProfiles.length > 0) { + quickPickItems.push(...this._sortProfileQuickPickItems(contributedProfiles, defaultProfileName)); } + if (autoDetectedProfiles.length > 0) { + quickPickItems.push({ type: 'separator', label: nls.localize('terminalProfiles.detected', "detected") }); + quickPickItems.push(...this._sortProfileQuickPickItems(autoDetectedProfiles.map(e => this._createProfileQuickPickItem(e)), defaultProfileName)); + } + const styleElement = getColorStyleElement(this._themeService.getColorTheme()); + document.body.appendChild(styleElement); + const value = await this._quickInputService.pick(quickPickItems, options); + document.body.removeChild(styleElement); if (!value) { return undefined; // {{SQL CARBON EDIT}} Strict nulls } @@ -939,11 +1045,11 @@ export class TerminalService implements ITerminalService { if ('id' in value.profile) { await this._createContributedTerminalProfile(value.profile.extensionIdentifier, value.profile.id, { - splitActiveTerminal: !!(keyMods?.alt && activeInstance), icon: value.profile.icon, - color: value.profile.color + color: value.profile.color, + location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : this.defaultLocation }); - return undefined; // {{SQL CARBON EDIT}} strict-nulls + return undefined; // {{SQL CARBON EDIT}} - add return type } else { if (keyMods?.alt && activeInstance) { // create split, only valid if there's an active instance @@ -1063,9 +1169,15 @@ export class TerminalService implements ITerminalService { }]; const icon = (profile.icon && ThemeIcon.isThemeIcon(profile.icon)) ? profile.icon : Codicon.terminal; const label = `$(${icon.id}) ${profile.profileName}`; + const colorClass = getColorClass(profile); + const iconClasses = []; + if (colorClass) { + iconClasses.push(colorClass); + } + if (profile.args) { if (typeof profile.args === 'string') { - return { label, description: `${profile.path} ${profile.args}`, profile, buttons }; + return { label, description: `${profile.path} ${profile.args}`, profile, profileName: profile.profileName, buttons, iconClasses }; } const argsString = profile.args.map(e => { if (e.includes(' ')) { @@ -1073,9 +1185,21 @@ export class TerminalService implements ITerminalService { } return e; }).join(' '); - return { label, description: `${profile.path} ${argsString}`, profile, buttons }; + return { label, description: `${profile.path} ${argsString}`, profile, profileName: profile.profileName, buttons, iconClasses }; } - return { label, description: profile.path, profile, buttons }; + return { label, description: profile.path, profile, profileName: profile.profileName, buttons, iconClasses }; + } + + private _sortProfileQuickPickItems(items: IProfileQuickPickItem[], defaultProfileName: string) { + return items.sort((a, b) => { + if (b.profileName === defaultProfileName) { + return 1; + } + if (a.profileName === defaultProfileName) { + return -1; + } + return a.profileName.localeCompare(b.profileName); + }); } private _convertProfileToShellLaunchConfig(shellLaunchConfigOrProfile?: IShellLaunchConfig | ITerminalProfile, cwd?: string | URI): IShellLaunchConfig { @@ -1121,6 +1245,17 @@ export class TerminalService implements ITerminalService { async createTerminal(options?: ICreateTerminalOptions): Promise { + // Await the initialization of available profiles as long as this is not a pty terminal or a + // local terminal in a remote workspace as profile won't be used in those cases and these + // terminals need to be launched before remote connections are established. + if (!this._availableProfiles) { + const isPtyTerminal = options?.config && 'customPtyImplementation' in options.config; + const isLocalInRemoteTerminal = this._remoteAgentService.getConnection() && URI.isUri(options?.cwd) && options?.cwd.scheme === Schemas.vscodeFileResource; + if (!isPtyTerminal && !isLocalInRemoteTerminal) { + await this._refreshAvailableProfilesNow(); + } + } + const config = options?.config || this._availableProfiles?.find(p => p.profileName === this._defaultProfileName); const shellLaunchConfig = config && 'extensionIdentifier' in config ? {} : this._convertProfileToShellLaunchConfig((config as IShellLaunchConfig | ITerminalProfile) || {}); // {{SQL CARBON EDIT}} Cast to avoid compile error @@ -1134,15 +1269,23 @@ export class TerminalService implements ITerminalService { // Launch the contributed profile if (contributedProfile) { + const resolvedLocation = this.resolveLocation(options?.location); + const splitActiveTerminal = typeof options?.location === 'object' && 'splitActiveTerminal' in options.location ? options.location.splitActiveTerminal : false; + let location: TerminalLocation | { viewColumn: number, preserveState?: boolean } | { splitActiveTerminal: boolean } | undefined; + if (splitActiveTerminal) { + location = resolvedLocation === TerminalLocation.Editor ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; + } else { + location = typeof options?.location === 'object' && 'viewColumn' in options.location ? options.location : resolvedLocation; + } await this._createContributedTerminalProfile(contributedProfile.extensionIdentifier, contributedProfile.id, { icon: contributedProfile.icon, color: contributedProfile.color, - splitActiveTerminal: typeof options?.location === 'object' && 'splitActiveTerminal' in options.location ? true : false + location }); - // TODO shouldn't the below use defaultLocation? - const instanceHost = options?.location === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; + const instanceHost = resolvedLocation === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; const instance = instanceHost.instances[instanceHost.instances.length - 1]; await instance.focusWhenReady(); + this._terminalHasBeenCreated.set(true); return instance; } @@ -1159,18 +1302,18 @@ export class TerminalService implements ITerminalService { this._backgroundedTerminalDisposables.set(instance.instanceId, [ instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance) ]); + this._terminalHasBeenCreated.set(true); return instance; } this._evaluateLocalCwd(shellLaunchConfig); - const location = this._resolveLocation(options?.location) || this.defaultLocation; + const location = this.resolveLocation(options?.location) || this.defaultLocation; const parent = this._getSplitParent(options?.location); - + this._terminalHasBeenCreated.set(true); if (parent) { return this._splitTerminal(shellLaunchConfig, location, parent); - } else { - return this._createTerminal(shellLaunchConfig, location, options); } + return this._createTerminal(shellLaunchConfig, location, options); } private _splitTerminal(shellLaunchConfig: IShellLaunchConfig, location: TerminalLocation, parent: ITerminalInstance): ITerminalInstance { @@ -1190,6 +1333,7 @@ export class TerminalService implements ITerminalService { if (!group) { throw new Error(`Cannot split a terminal without a group ${parent}`); } + shellLaunchConfig.parentTerminalId = parent.instanceId; instance = group.split(shellLaunchConfig); this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex)); } @@ -1211,19 +1355,19 @@ export class TerminalService implements ITerminalService { return instance; } - private _resolveLocation(location?: ITerminalLocationOptions): TerminalLocation | undefined { - if (!location) { - return undefined; // {{SQL CARBON EDIT}} Return undefined directly to avoid compile error - } else if (typeof location === 'object') { + resolveLocation(location?: ITerminalLocationOptions): TerminalLocation | undefined { + if (location && typeof location === 'object') { if ('parentTerminal' in location) { - return location.parentTerminal.target; + // since we don't set the target unless it's an editor terminal, this is necessary + return !location.parentTerminal.target ? TerminalLocation.Panel : location.parentTerminal.target; } else if ('viewColumn' in location) { return TerminalLocation.Editor; } else if ('splitActiveTerminal' in location) { - return this._activeInstance?.target || this.defaultLocation; + // since we don't set the target unless it's an editor terminal, this is necessary + return !this._activeInstance?.target ? TerminalLocation.Panel : this._activeInstance?.target; } } - return location; + return location; } private _getSplitParent(location?: ITerminalLocationOptions): ITerminalInstance | undefined { @@ -1237,6 +1381,11 @@ export class TerminalService implements ITerminalService { private _getEditorOptions(location?: ITerminalLocationOptions): TerminalEditorLocation | undefined { if (location && typeof location === 'object' && 'viewColumn' in location) { + // When ACTIVE_GROUP is used, resolve it to an actual group to ensure the is created in + // the active group even if it is locked + if (location.viewColumn === ACTIVE_GROUP) { + location.viewColumn = this._editorGroupsService.activeGroup.index; + } return location; } return undefined; @@ -1282,7 +1431,8 @@ export class TerminalService implements ITerminalService { } interface IProfileQuickPickItem extends IQuickPickItem { - profile: ITerminalProfile | IExtensionTerminalProfile + profile: ITerminalProfile | IExtensionTerminalProfile; + profileName: string; } class TerminalEditorStyle extends Themable { @@ -1304,7 +1454,8 @@ class TerminalEditorStyle extends Themable { private _registerListeners(): void { this._register(this._terminalService.onDidChangeInstanceIcon(() => this.updateStyles())); this._register(this._terminalService.onDidChangeInstanceColor(() => this.updateStyles())); - this._register(this._terminalService.onDidChangeInstances(() => this.updateStyles())); + this._register(this._terminalService.onDidCreateInstance(() => this.updateStyles())); + this._register(this._terminalService.onDidChangeAvailableProfiles(() => this.updateStyles())); } override updateStyles(): void { @@ -1353,20 +1504,8 @@ class TerminalEditorStyle extends Themable { if (iconForegroundColor) { css += `.monaco-workbench .show-file-icons .file-icon.terminal-tab::before { color: ${iconForegroundColor}; }`; } - for (const instance of this._terminalService.instances) { - const colorClass = getColorClass(instance); - if (!colorClass || !instance.color) { - continue; - } - const color = colorTheme.getColor(instance.color); - if (color) { - css += ( - `.monaco-workbench .show-file-icons .file-icon.terminal-tab.${colorClass}::before` + - `{ color: ${color} !important; }` - ); - } - } + css += getColorStyleContent(colorTheme, true); this._styleElement.textContent = css; } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 71fbb75371..5455b2b4fc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -207,7 +207,7 @@ export class TerminalTabbedView extends Disposable { const style = window.getComputedStyle(this._tabListElement); ctx.font = `${style.fontStyle} ${style.fontSize} ${style.fontFamily}`; const maxInstanceWidth = this._terminalGroupService.instances.reduce((p, c) => { - return Math.max(p, ctx.measureText(c.title + (c.shellLaunchConfig.description || '')).width + this._getAdditionalWidth(c)); + return Math.max(p, ctx.measureText(c.title + (c.description || '')).width + this._getAdditionalWidth(c)); }, 0); idealWidth = Math.ceil(Math.max(maxInstanceWidth, TerminalTabsListSizes.WideViewMinimumWidth)); } @@ -222,7 +222,7 @@ export class TerminalTabbedView extends Disposable { private _getAdditionalWidth(instance: ITerminalInstance): number { // Size to include padding, icon, status icon (if any), split annotation (if any), + a little more - const additionalWidth = 30; + const additionalWidth = 40; const statusIconWidth = instance.statusList.statuses.length > 0 ? STATUS_ICON_WIDTH : 0; const splitAnnotationWidth = (this._terminalGroupService.getGroupForInstance(instance)?.terminalInstances.length || 0) > 1 ? SPLIT_ANNOTATION_WIDTH : 0; return additionalWidth + splitAnnotationWidth + statusIconWidth; @@ -340,25 +340,19 @@ export class TerminalTabbedView extends Disposable { event.stopPropagation(); })); this._register(dom.addDisposableListener(terminalContainer, 'mousedown', async (event: MouseEvent) => { - if (this._terminalGroupService.instances.length === 0) { + const terminal = this._terminalGroupService.activeInstance; + if (this._terminalGroupService.instances.length === 0 || !terminal) { + this._cancelContextMenu = true; return; } if (event.which === 2 && isLinux) { // Drop selection and focus terminal on Linux to enable middle button paste when click // occurs on the selection itself. - const terminal = this._terminalGroupService.activeInstance; - if (terminal) { - terminal.focus(); - } + terminal.focus(); } else if (event.which === 3) { const rightClickBehavior = this._terminalService.configHelper.config.rightClickBehavior; if (rightClickBehavior === 'copyPaste' || rightClickBehavior === 'paste') { - const terminal = this._terminalGroupService.activeInstance; - if (!terminal) { - return; - } - // copyPaste: Shift+right click should open context menu if (rightClickBehavior === 'copyPaste' && event.shiftKey) { openContextMenu(event, this._parentElement, this._instanceMenu, this._contextMenuService); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index eb9b39d661..cf991d5cc5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -23,7 +23,7 @@ import { Action } from 'vs/base/common/actions'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { TerminalDecorationsProvider } from 'vs/workbench/contrib/terminal/browser/terminalDecorationsProvider'; import { DEFAULT_LABELS_CONTAINER, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; +import { IDecorationsService } from 'vs/workbench/services/decorations/common/decorations'; import { IHoverAction, IHoverService } from 'vs/workbench/services/hover/browser/hover'; import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -289,11 +289,12 @@ class TerminalTabsRenderer implements IListRenderer this._terminalService.createTerminal({ location: { parentTerminal: e } })); }), new Action(TerminalCommandId.KillInstance, terminalStrings.kill.short, ThemeIcon.asClassName(Codicon.trashcan), true, async () => { - this._runForSelectionOrInstance(instance, e => e.dispose()); + this._runForSelectionOrInstance(instance, e => this._terminalService.safeDisposeTerminal(e)); }) ]; // TODO: Cache these in a way that will use the correct instance diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index e68a83349a..d3b7f72a45 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -25,7 +25,6 @@ import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions import { ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalSettingId, ITerminalProfile, TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { ActionViewItem, SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; @@ -75,8 +74,7 @@ export class TerminalViewPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IMenuService private readonly _menuService: IMenuService, @ICommandService private readonly _commandService: ICommandService, - @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, - @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, + @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService ) { super(options, keybindingService, _contextMenuService, configurationService, _contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); this._terminalService.onDidRegisterProcessSupport(() => { @@ -204,7 +202,7 @@ export class TerminalViewPane extends ViewPane { this._tabButtons.dispose(); } - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalContributionService.terminalProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); this._tabButtons = new DropdownWithPrimaryActionViewItem(actions.primaryAction, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}, this._keybindingService, this._notificationService, this._contextKeyService); this._updateTabActionBar(this._terminalService.availableProfiles); return this._tabButtons; @@ -228,7 +226,7 @@ export class TerminalViewPane extends ViewPane { } private _updateTabActionBar(profiles: ITerminalProfile[]): void { - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalContributionService.terminalProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalService.contributedProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); this._tabButtons?.update(actions.dropdownAction, actions.dropdownMenuActions); } @@ -266,6 +264,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const sidebarBackgroundColor = theme.getColor(TERMINAL_BACKGROUND_COLOR) || theme.getColor(SIDE_BAR_BACKGROUND); collector.addRule(`.monaco-workbench .part.sidebar .pane-body.integrated-terminal .terminal-outer-container { background-color: ${sidebarBackgroundColor ? sidebarBackgroundColor.toString() : ''}; }`); + collector.addRule(`.monaco-workbench .part.auxiliarybar .pane-body.integrated-terminal .terminal-outer-container { background-color: ${sidebarBackgroundColor ? sidebarBackgroundColor.toString() : ''}; }`); const borderColor = theme.getColor(TERMINAL_BORDER_COLOR); if (borderColor) { @@ -354,8 +353,8 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { super(new MenuItemAction( { id: action.id, - title: getSingleTabLabel(_terminalGroupService.activeInstance), - tooltip: getSingleTabTooltip(_terminalGroupService.activeInstance) + title: getSingleTabLabel(_terminalGroupService.activeInstance, _terminalService.configHelper.config.tabs.separator), + tooltip: getSingleTabTooltip(_terminalGroupService.activeInstance, _terminalService.configHelper.config.tabs.separator) }, { id: TerminalCommandId.Split, @@ -376,7 +375,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { this._register(this._terminalService.onDidChangeInstanceColor(e => this.updateLabel(e))); this._register(this._terminalService.onDidChangeInstanceTitle(e => { if (e === this._terminalGroupService.activeInstance) { - this._action.tooltip = getSingleTabTooltip(e); + this._action.tooltip = getSingleTabTooltip(e, this._terminalService.configHelper.config.tabs.separator); this.updateLabel(); } })); @@ -444,7 +443,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { } } label.style.color = colorStyle; - dom.reset(label, ...renderLabelWithIcons(getSingleTabLabel(instance, ThemeIcon.isThemeIcon(this._commandAction.item.icon) ? this._commandAction.item.icon : undefined))); + dom.reset(label, ...renderLabelWithIcons(getSingleTabLabel(instance, this._terminalService.configHelper.config.tabs.separator, ThemeIcon.isThemeIcon(this._commandAction.item.icon) ? this._commandAction.item.icon : undefined))); if (this._altCommand) { label.classList.remove(this._altCommand); @@ -486,14 +485,14 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { } } -function getSingleTabLabel(instance: ITerminalInstance | undefined, icon?: ThemeIcon) { +function getSingleTabLabel(instance: ITerminalInstance | undefined, separator: string, icon?: ThemeIcon) { // Don't even show the icon if there is no title as the icon would shift around when the title // is added if (!instance || !instance.title) { return ''; } let iconClass = ThemeIcon.isThemeIcon(instance.icon) ? instance.icon?.id : Codicon.terminal.id; - const label = `$(${icon?.id || iconClass}) ${getSingleTabTooltip(instance)}`; + const label = `$(${icon?.id || iconClass}) ${getSingleTabTooltip(instance, separator)}`; const primaryStatus = instance.statusList.primary; if (!primaryStatus?.icon) { @@ -502,14 +501,14 @@ function getSingleTabLabel(instance: ITerminalInstance | undefined, icon?: Theme return `${label} $(${primaryStatus.icon.id})`; } -function getSingleTabTooltip(instance: ITerminalInstance | undefined): string { +function getSingleTabTooltip(instance: ITerminalInstance | undefined, separator: string): string { if (!instance) { return ''; } - if (!instance.shellLaunchConfig.description) { + if (!instance.description) { return instance.title; } - return `${instance.title} ${instance.shellLaunchConfig.description}`; + return `${instance.title} ${separator} ${instance.description}`; } class TerminalThemeIconStyle extends Themable { @@ -557,7 +556,7 @@ class TerminalThemeIconStyle extends Themable { const iconClasses = getUriClasses(instance, colorTheme.type); if (uri instanceof URI && iconClasses && iconClasses.length > 1) { css += ( - `.monaco-workbench .${iconClasses[0]} .monaco-highlighted-label .codicon, .monaco-action-bar .terminal-uri-icon.single-terminal-tab.action-label:not(.alt-command) .codicon,` + + `.monaco-workbench .${iconClasses[0]} .monaco-highlighted-label .codicon, .monaco-action-bar .terminal-uri-icon.single-terminal-tab.action-label:not(.alt-command) .codicon` + `{background-image: ${dom.asCSSUrl(uri)};}` ); } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts index bf9e01ce47..8a42e3c0c3 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts @@ -3,12 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable @typescript-eslint/naming-convention */ + import { IBufferCell } from 'xterm'; export type XTermAttributes = Omit & { clone?(): XTermAttributes }; export interface XTermCore { - _onScroll: IEventEmitter; + viewport?: { + _innerRefresh(): void; + }; _onKey: IEventEmitter<{ key: string }>; _charSizeService: { diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index 2874ab75e3..4205c7da36 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -18,7 +18,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Schemas } from 'vs/base/common/network'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IRequestResolveVariablesEvent, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, IProcessProperty, ProcessPropertyType, ProcessCapability, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; import { IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; // {{SQL CARBON EDIT}} Remove unused import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; @@ -94,36 +94,24 @@ export class RemoteTerminalChannelClient { get onProcessExit(): Event<{ id: number, event: number | undefined }> { return this._channel.listen<{ id: number, event: number | undefined }>('$onProcessExitEvent'); } - get onProcessReady(): Event<{ id: number, event: { pid: number, cwd: string, requireWindowsMode?: boolean } }> { - return this._channel.listen<{ id: number, event: { pid: number, cwd: string, requiresWindowsMode?: boolean } }>('$onProcessReadyEvent'); + get onProcessReady(): Event<{ id: number, event: { pid: number, cwd: string, capabilities: ProcessCapability[], requireWindowsMode?: boolean } }> { + return this._channel.listen<{ id: number, event: { pid: number, cwd: string, capabilities: ProcessCapability[], requiresWindowsMode?: boolean } }>('$onProcessReadyEvent'); } get onProcessReplay(): Event<{ id: number, event: IPtyHostProcessReplayEvent }> { return this._channel.listen<{ id: number, event: IPtyHostProcessReplayEvent }>('$onProcessReplayEvent'); } - get onProcessTitleChanged(): Event<{ id: number, event: string }> { - return this._channel.listen<{ id: number, event: string }>('$onProcessTitleChangedEvent'); - } - get onProcessShellTypeChanged(): Event<{ id: number, event: TerminalShellType | undefined }> { - return this._channel.listen<{ id: number, event: TerminalShellType | undefined }>('$onProcessShellTypeChangedEvent'); - } - get onProcessOverrideDimensions(): Event<{ id: number, event: ITerminalDimensionsOverride | undefined }> { - return this._channel.listen<{ id: number, event: ITerminalDimensionsOverride | undefined }>('$onProcessOverrideDimensionsEvent'); - } - get onProcessResolvedShellLaunchConfig(): Event<{ id: number, event: IShellLaunchConfig }> { - return this._channel.listen<{ id: number, event: IShellLaunchConfig }>('$onProcessResolvedShellLaunchConfigEvent'); - } get onProcessOrphanQuestion(): Event<{ id: number }> { return this._channel.listen<{ id: number }>('$onProcessOrphanQuestion'); } - get onProcessDidChangeHasChildProcesses(): Event<{ id: number, event: boolean }> { - return this._channel.listen<{ id: number, event: boolean }>('$onProcessDidChangeHasChildProcesses'); - } get onExecuteCommand(): Event<{ reqId: number, commandId: string, commandArgs: any[] }> { return this._channel.listen<{ reqId: number, commandId: string, commandArgs: any[] }>('$onExecuteCommand'); } get onDidRequestDetach(): Event<{ requestId: number, workspaceId: string, instanceId: number }> { return this._channel.listen<{ requestId: number, workspaceId: string, instanceId: number }>('$onDidRequestDetach'); } + get onDidChangeProperty(): Event<{ id: number, property: IProcessProperty }> { + return this._channel.listen<{ id: number, property: IProcessProperty }>('$onDidChangeProperty'); + } constructor( private readonly _remoteAuthority: string, @@ -290,6 +278,14 @@ export class RemoteTerminalChannelClient { return this._channel.call('$updateIcon', [id, icon, color]); } + refreshProperty(id: number, property: ProcessPropertyType): Promise { + return this._channel.call('$refreshProperty', [id, property]); + } + + updateProperty(id: number, property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise { + return this._channel.call('$updateProperty', [id, property, value]); + } + getTerminalLayoutInfo(): Promise { // {{SQL CARBON EDIT}} - temp disable this code since it refers to non-implemented method // - currently remote code is ahead of OSS code and they need to catch-up (karlb 5/13/2021) @@ -302,4 +298,12 @@ export class RemoteTerminalChannelClient { return this._channel.call('$getTerminalLayoutInfo', args); */ } + + reviveTerminalProcesses(state: string): Promise { + return this._channel.call('$reviveTerminalProcesses', [state]); + } + + serializeTerminalState(ids: number[]): Promise { + return this._channel.call('$serializeTerminalState', [ids]); + } } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index aeb79e65e5..77834bc8be 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -97,6 +97,7 @@ export interface IOffProcessTerminalService { reduceConnectionGraceTime(): Promise; requestDetachInstance(workspaceId: string, instanceId: number): Promise; acceptDetachInstanceReply(requestId: number, persistentProcessId?: number): Promise; + persistTerminalState(): Promise; } export const ILocalTerminalService = createDecorator('localTerminalService'); @@ -153,7 +154,7 @@ export interface ITerminalConfiguration { gpuAcceleration: 'auto' | 'on' | 'canvas' | 'off'; rightClickBehavior: 'default' | 'copyPaste' | 'paste' | 'selectWord'; cursorBlinking: boolean; - cursorStyle: string; + cursorStyle: 'block' | 'underline' | 'line'; cursorWidth: number; drawBoldTextInBrightColors: boolean; fastScrollSensitivity: number; @@ -187,7 +188,6 @@ export interface ITerminalConfiguration { splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; windowsEnableConpty: boolean; wordSeparators: string; - titleMode: 'executable' | 'sequence'; enableFileLinks: boolean; unicodeVersion: '6' | '11'; experimentalLinkProvider: boolean; @@ -201,10 +201,15 @@ export interface ITerminalConfiguration { showActiveTerminal: 'always' | 'singleTerminal' | 'singleTerminalOrNarrow' | 'singleGroup' | 'never'; location: 'left' | 'right'; focusMode: 'singleClick' | 'doubleClick'; + title: string; + description: string; + separator: string; }, bellDuration: number; defaultLocation: TerminalLocationString; customGlyphs: boolean; + persistentSessionReviveProcess: 'onExit' | 'onExitAndWindowClose' | 'never'; + ignoreProcessNames: string[]; } export const DEFAULT_LOCAL_ECHO_EXCLUDE: ReadonlyArray = ['vim', 'vi', 'nano', 'tmux']; @@ -237,6 +242,7 @@ export interface IRemoteTerminalAttachTarget { isOrphan: boolean; icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } } | undefined; color: string | undefined; + fixedDimensions: IFixedTerminalDimensions | undefined; } export interface ICommandTracker { @@ -287,13 +293,9 @@ export interface ITerminalProcessManager extends IDisposable { readonly onProcessReady: Event; readonly onBeforeProcessData: Event; readonly onProcessData: Event; - readonly onProcessTitle: Event; - readonly onProcessShellTypeChanged: Event; - readonly onProcessExit: Event; - readonly onProcessOverrideDimensions: Event; - readonly onProcessResolvedShellLaunchConfig: Event; - readonly onProcessDidChangeHasChildProcesses: Event; readonly onEnvironmentVariableInfoChanged: Event; + readonly onDidChangeProperty: Event>; + readonly onProcessExit: Event; dispose(immediate?: boolean): void; detachFromProcess(): Promise; @@ -310,6 +312,8 @@ export interface ITerminalProcessManager extends IDisposable { getInitialCwd(): Promise; getCwd(): Promise; getLatency(): Promise; + refreshProperty(property: ProcessPropertyType): any; + updateProperty(property: ProcessPropertyType, value: any): any; } export const enum ProcessState { @@ -335,14 +339,10 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { readonly instanceId: number; emitData(data: string): void; - emitTitle(title: string): void; + emitProcessProperty(property: IProcessProperty): void; emitReady(pid: number, cwd: string): void; - emitExit(exitCode: number | undefined): void; - emitOverrideDimensions(dimensions: ITerminalDimensions | undefined): void; - emitResolvedShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig): void; - emitInitialCwd(initialCwd: string): void; - emitCwd(cwd: string): void; emitLatency(latency: number): void; + emitExit(exitCode: number | undefined): void; onInput: Event; onBinary: Event; @@ -406,6 +406,8 @@ export const enum TerminalCommandId { ResizePaneRight = 'workbench.action.terminal.resizePaneRight', ResizePaneUp = 'workbench.action.terminal.resizePaneUp', CreateWithProfileButton = 'workbench.action.terminal.createProfileButton', + SizeToContentWidth = 'workbench.action.terminal.sizeToContentWidth', + SizeToContentWidthInstance = 'workbench.action.terminal.sizeToContentWidthInstance', ResizePaneDown = 'workbench.action.terminal.resizePaneDown', Focus = 'workbench.action.terminal.focus', FocusNext = 'workbench.action.terminal.focusNext', @@ -425,10 +427,13 @@ export const enum TerminalCommandId { Clear = 'workbench.action.terminal.clear', ClearSelection = 'workbench.action.terminal.clearSelection', ChangeIcon = 'workbench.action.terminal.changeIcon', + ChangeIconPanel = 'workbench.action.terminal.changeIconPanel', ChangeIconInstance = 'workbench.action.terminal.changeIconInstance', ChangeColor = 'workbench.action.terminal.changeColor', + ChangeColorPanel = 'workbench.action.terminal.changeColorPanel', ChangeColorInstance = 'workbench.action.terminal.changeColorInstance', Rename = 'workbench.action.terminal.rename', + RenamePanel = 'workbench.action.terminal.renamePanel', RenameInstance = 'workbench.action.terminal.renameInstance', RenameWithArgs = 'workbench.action.terminal.renameWithArg', FindFocus = 'workbench.action.terminal.focusFind', @@ -455,6 +460,7 @@ export const enum TerminalCommandId { MoveToEditor = 'workbench.action.terminal.moveToEditor', MoveToEditorInstance = 'workbench.action.terminal.moveToEditorInstance', MoveToTerminalPanel = 'workbench.action.terminal.moveToTerminalPanel', + SetDimensions = 'workbench.action.terminal.setDimensions', } export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ @@ -476,6 +482,7 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ TerminalCommandId.FocusPreviousPane, TerminalCommandId.FocusPrevious, TerminalCommandId.Focus, + TerminalCommandId.SizeToContentWidth, TerminalCommandId.Kill, TerminalCommandId.KillEditor, TerminalCommandId.MoveToEditor, @@ -513,6 +520,8 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ TerminalCommandId.NavigationModeFocusNext, TerminalCommandId.NavigationModeFocusPrevious, 'editor.action.toggleTabFocusMode', + 'notifications.hideList', + 'notifications.hideToasts', 'workbench.action.quickOpen', 'workbench.action.quickOpenPreviousEditor', 'workbench.action.showCommands', diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index ab234401ce..50f3e2c555 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -3,13 +3,30 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { localize } from 'vs/nls'; import { DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, DEFAULT_COMMANDS_TO_SKIP_SHELL, SUGGESTIONS_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT, DEFAULT_LOCAL_ECHO_EXCLUDE } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalLocationString, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Registry } from 'vs/platform/registry/common/platform'; +const terminalDescriptors = '\n- ' + [ + '`\${cwd}`: ' + localize("cwd", "the terminal's current working directory"), + '`\${cwdFolder}`: ' + localize('cwdFolder', "the terminal's current working directory, displayed for multi-root workspaces or in a single root workspace when the value differs from the initial working directory. This will not be displayed for Windows."), + '`\${workspaceFolder}`: ' + localize('workspaceFolder', "the workspace in which the terminal was launched"), + '`\${local}`: ' + localize('local', "indicates a local terminal in a remote workspace"), + '`\${process}`: ' + localize('process', "the name of the terminal process"), + '`\${separator}`: ' + localize('separator', "a conditional separator (\" - \") that only shows when surrounded by variables with values or static text."), + '`\${sequence}`: ' + localize('sequence', "the name provided to xterm.js by the process"), + '`\${task}`: ' + localize('task', "indicates this terminal is associated with a task"), +].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations + +let terminalTitleDescription = localize('terminalTitle', "Controls the terminal title. Variables are substituted based on the context:"); +terminalTitleDescription += terminalDescriptors; + +let terminalDescriptionDescription = localize('terminalDescription', "Controls the terminal description, which appears to the right of the title. Variables are substituted based on the context:"); +terminalDescriptionDescription += terminalDescriptors; + const terminalConfiguration: IConfigurationNode = { id: 'terminal', order: 100, @@ -134,7 +151,9 @@ const terminalConfiguration: IConfigurationNode = { [TerminalSettingId.FontSize]: { description: localize('terminal.integrated.fontSize', "Controls the font size in pixels of the terminal."), type: 'number', - default: isMacintosh ? 12 : 14 + default: isMacintosh ? 12 : 14, + minimum: 6, + maximum: 100 }, [TerminalSettingId.LetterSpacing]: { description: localize('terminal.integrated.letterSpacing', "Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters."), @@ -247,6 +266,21 @@ const terminalConfiguration: IConfigurationNode = { default: 'auto', description: localize('terminal.integrated.gpuAcceleration', "Controls whether the terminal will leverage the GPU to do its rendering.") }, + [TerminalSettingId.TerminalTitleSeparator]: { + 'type': 'string', + 'default': ' - ', + 'markdownDescription': localize("terminal.integrated.tabs.separator", "Separator used by `terminal.integrated.title` and `terminal.integrated.description`.") + }, + [TerminalSettingId.TerminalTitle]: { + 'type': 'string', + 'default': '${process}', + 'markdownDescription': terminalTitleDescription + }, + [TerminalSettingId.TerminalDescription]: { + 'type': 'string', + 'default': '${task}${separator}${local}${separator}${cwdFolder}', + 'markdownDescription': terminalDescriptionDescription + }, [TerminalSettingId.RightClickBehavior]: { type: 'string', enum: ['default', 'copyPaste', 'paste', 'selectWord'], @@ -263,7 +297,8 @@ const terminalConfiguration: IConfigurationNode = { restricted: true, description: localize('terminal.integrated.cwd', "An explicit start path where the terminal will be launched, this is used as the current working directory (cwd) for the shell process. This may be particularly useful in workspace settings if the root directory is not a convenient cwd."), type: 'string', - default: undefined + default: undefined, + scope: ConfigurationScope.RESOURCE }, [TerminalSettingId.ConfirmOnExit]: { description: localize('terminal.integrated.confirmOnExit', "Controls whether to confirm when the window closes if there are active terminal sessions."), @@ -277,7 +312,7 @@ const terminalConfiguration: IConfigurationNode = { default: 'never' }, [TerminalSettingId.ConfirmOnKill]: { - description: localize('terminal.integrated.confirmOnKill', "Controls whether to confirm killing terminals when they have child processes. When set to editor, terminals in the editor area will be marked as dirty when they have child processes. Note that child process detection may not work well for shells like Git Bash which don't run their processes as child processes of the shell."), + description: localize('terminal.integrated.confirmOnKill', "Controls whether to confirm killing terminals when they have child processes. When set to editor, terminals in the editor area will be marked as changed when they have child processes. Note that child process detection may not work well for shells like Git Bash which don't run their processes as child processes of the shell."), type: 'string', enum: ['never', 'editor', 'panel', 'always'], enumDescriptions: [ @@ -383,17 +418,7 @@ const terminalConfiguration: IConfigurationNode = { [TerminalSettingId.WordSeparators]: { description: localize('terminal.integrated.wordSeparators', "A string containing all characters to be considered word separators by the double click to select word feature."), type: 'string', - default: ' ()[]{}\',"`─' - }, - [TerminalSettingId.TitleMode]: { - description: localize('terminal.integrated.titleMode', "Determines how the terminal's title is set, this shows up in the terminal's tab or dropdown entry."), - type: 'string', - enum: ['executable', 'sequence'], - markdownEnumDescriptions: [ - localize('titleMode.executable', "The title is set by the terminal, the name of the detected foreground process will be used."), - localize('titleMode.sequence', "The title is set by the process via an escape sequence, this is useful if your shell dynamically sets the title.") - ], - default: 'executable' + default: ' ()[]{}\',"`─‘’' }, [TerminalSettingId.EnableFileLinks]: { description: localize('terminal.integrated.enableFileLinks', "Whether to enable file links in the terminal. Links can be slow when working on a network drive in particular because each file link is verified against the file system. Changing this will take effect only in new terminals."), @@ -451,6 +476,17 @@ const terminalConfiguration: IConfigurationNode = { type: 'boolean', default: true }, + [TerminalSettingId.PersistentSessionReviveProcess]: { + description: localize('terminal.integrated.persistentSessionReviveProcess', "When the terminal process must be shutdown (eg. on window or application close), this determines when the previous terminal session contents should be restored and processes be recreated when the workspace is next opened. Restoring of the process current working directory depends on whether it is supported by the shell."), + type: 'string', + enum: ['onExit', 'onExitAndWindowClose', 'never'], + markdownEnumDescriptions: [ + localize('terminal.integrated.persistentSessionReviveProcess.onExit', "Revive the processes after the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu)."), + localize('terminal.integrated.persistentSessionReviveProcess.onExitAndWindowClose', "Revive the processes after the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), or when the window is closed."), + localize('terminal.integrated.persistentSessionReviveProcess.never', "Never restore the terminal buffers or recreate the process.") + ], + default: 'onExit' + }, [TerminalSettingId.CustomGlyphs]: { description: localize('terminal.integrated.customGlyphs', "Whether to draw custom glyphs for block element and box drawing characters instead of using the font, which typically yields better rendering with continuous lines. Note that this doesn't work with the DOM renderer"), type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 61b9537cc7..9654821863 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -11,10 +11,13 @@ export const enum TerminalContextKeyStrings { Count = 'terminalCount', GroupCount = 'terminalGroupCount', TabsNarrow = 'isTerminalTabsNarrow', + HasFixedWidth = 'terminalHasFixedWidth', ProcessSupported = 'terminalProcessSupported', Focus = 'terminalFocus', EditorFocus = 'terminalEditorFocus', TabsFocus = 'terminalTabsFocus', + WebExtensionContributedProfile = 'terminalWebExtensionContributedProfile', + TerminalHasBeenCreated = 'terminalHasBeenCreated', TabsMouse = 'terminalTabsMouse', AltBufferActive = 'terminalAltBufferActive', A11yTreeFocus = 'terminalA11yTreeFocus', @@ -46,9 +49,18 @@ export namespace TerminalContextKeys { /** Whether the terminal tabs view is narrow. */ export const tabsNarrow = new RawContextKey(TerminalContextKeyStrings.TabsNarrow, false, true); + /** Whether the terminal tabs view is narrow. */ + export const terminalHasFixedWidth = new RawContextKey(TerminalContextKeyStrings.HasFixedWidth, false, true); + /** Whether the terminal tabs widget is focused. */ export const tabsFocus = new RawContextKey(TerminalContextKeyStrings.TabsFocus, false, localize('terminalTabsFocusContextKey', "Whether the terminal tabs widget is focused.")); + /** Whether a web extension has contributed a profile */ + export const webExtensionContributedProfile = new RawContextKey(TerminalContextKeyStrings.WebExtensionContributedProfile, false, true); + + /** Whether at least one terminal has been created */ + export const terminalHasBeenCreated = new RawContextKey(TerminalContextKeyStrings.TerminalHasBeenCreated, false, true); + /** Whether the mouse is within the terminal tabs list. */ export const tabsMouse = new RawContextKey(TerminalContextKeyStrings.TabsMouse, false, true); diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index 2cfa848dd1..3055d14a7e 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -326,7 +326,7 @@ export function getDefaultShellArgs( } const platformKey = platformOverride === Platform.Windows ? 'windows' : platformOverride === Platform.Mac ? 'osx' : 'linux'; - let args = fetchSetting(`${TerminalSettingPrefix.ShellArgs}.${platformKey}`); + let args = fetchSetting(`${TerminalSettingPrefix.ShellArgs}${platformKey}`); if (!args) { return []; } @@ -339,7 +339,7 @@ export function getDefaultShellArgs( try { resolvedArgs.push(variableResolver(arg)); } catch (e) { - logService.error(`Could not resolve ${TerminalSettingPrefix.ShellArgs}.${platformKey}`, e); + logService.error(`Could not resolve ${TerminalSettingPrefix.ShellArgs}${platformKey}`, e); resolvedArgs.push(arg); } } diff --git a/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts b/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts index 8baa5db356..8b85cc4e8c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts @@ -9,4 +9,6 @@ export const enum TerminalStorageKeys { TabsListWidthHorizontal = 'tabs-list-width-horizontal', TabsListWidthVertical = 'tabs-list-width-vertical', EnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollections', + TerminalBufferState = 'terminal.integrated.bufferState', + TerminalLayoutInfo = 'terminal.integrated.layoutInfo' } diff --git a/src/vs/workbench/contrib/terminal/common/terminalStrings.ts b/src/vs/workbench/contrib/terminal/common/terminalStrings.ts index 7bf7b70c9c..f86690ed6a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalStrings.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalStrings.ts @@ -30,7 +30,6 @@ export const terminalStrings = { moveToEditor: { value: localize('moveToEditor', "Move Terminal into Editor Area"), original: 'Move Terminal into Editor Area', - short: 'Move into Editor Area' }, moveToTerminalPanel: { value: localize('workbench.action.terminal.moveToTerminalPanel', "Move Terminal into Panel"), @@ -53,9 +52,12 @@ export const terminalStrings = { value: localize('unsplitTerminal', "Unsplit Terminal"), original: 'Unsplit Terminal' }, - rename: - { + rename: { value: localize('workbench.action.terminal.rename', "Rename..."), original: 'Rename...' + }, + toggleSizeToContentWidth: { + value: localize('workbench.action.terminal.sizeToContentWidthInstance', "Toggle Size to Content Width"), + original: 'Toggle Size to Content Width' } }; diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts index 53455f3089..0cf95d5295 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts @@ -6,8 +6,9 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, ProcessCapability, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; +import { URI } from 'vs/base/common/uri'; /** * Responsible for establishing and maintaining a connection with an existing terminal process @@ -15,25 +16,28 @@ import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminal */ export class LocalPty extends Disposable implements ITerminalChildProcess { private _inReplay = false; - + private _properties: IProcessPropertyMap = { + cwd: '', + initialCwd: '', + fixedDimensions: { cols: undefined, rows: undefined }, + title: '', + shellType: undefined, + hasChildProcesses: true, + resolvedShellLaunchConfig: {}, + overrideDimensions: undefined + }; + private _capabilities: ProcessCapability[] = []; + get capabilities(): ProcessCapability[] { return this._capabilities; } private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData = this._onProcessData.event; private readonly _onProcessReplay = this._register(new Emitter()); readonly onProcessReplay = this._onProcessReplay.event; - private readonly _onProcessExit = this._register(new Emitter()); - readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter()); readonly onProcessReady = this._onProcessReady.event; - private readonly _onProcessTitleChanged = this._register(new Emitter()); - readonly onProcessTitleChanged = this._onProcessTitleChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter()); - readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); - readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; - private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - private readonly _onDidChangeHasChildProcesses = this._register(new Emitter()); - readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; + private readonly _onDidChangeProperty = this._register(new Emitter>()); + readonly onDidChangeProperty = this._onDidChangeProperty.event; + private readonly _onProcessExit = this._register(new Emitter()); + readonly onProcessExit = this._onProcessExit.event; constructor( readonly id: number, @@ -70,11 +74,17 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { } this._localPtyService.resize(this.id, cols, rows); } - getInitialCwd(): Promise { - return this._localPtyService.getInitialCwd(this.id); + async getInitialCwd(): Promise { + return this._properties.initialCwd; } - getCwd(): Promise { - return this._localPtyService.getCwd(this.id); + async getCwd(): Promise { + return this._properties.cwd || this._properties.initialCwd; + } + async refreshProperty(type: ProcessPropertyType): Promise { + return this._localPtyService.refreshProperty(this.id, type); + } + async updateProperty(type: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise { + return this._localPtyService.updateProperty(this.id, type, value); } getLatency(): Promise { // TODO: The idea here was to add the result plus the time it took to get the latency @@ -97,22 +107,23 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { this._onProcessExit.fire(e); } handleReady(e: IProcessReadyEvent) { + this._capabilities = e.capabilities; this._onProcessReady.fire(e); } - handleTitleChanged(e: string) { - this._onProcessTitleChanged.fire(e); - } - handleShellTypeChanged(e: TerminalShellType) { - this._onProcessShellTypeChanged.fire(e); - } - handleOverrideDimensions(e: ITerminalDimensionsOverride | undefined) { - this._onProcessOverrideDimensions.fire(e); - } - handleResolvedShellLaunchConfig(e: IShellLaunchConfig) { - this._onProcessResolvedShellLaunchConfig.fire(e); - } - handleDidChangeHasChildProcesses(e: boolean) { - this._onDidChangeHasChildProcesses.fire(e); + handleDidChangeProperty({ type, value }: IProcessProperty) { + switch (type) { + case ProcessPropertyType.Cwd: + this._properties.cwd = value; + break; + case ProcessPropertyType.InitialCwd: + this._properties.initialCwd = value; + break; + case ProcessPropertyType.ResolvedShellLaunchConfig: + if (value.cwd && typeof value.cwd !== 'string') { + value.cwd = URI.revive(value.cwd); + } + } + this._onDidChangeProperty.fire({ type, value }); } async handleReplay(e: IPtyHostProcessReplayEvent) { @@ -121,7 +132,7 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { for (const innerEvent of e.events) { if (innerEvent.cols !== 0 || innerEvent.rows !== 0) { // never override with 0x0 as that is a marker for an unknown initial size - this._onProcessOverrideDimensions.fire({ cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: { cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true } }); } const e: IProcessDataEvent = { data: innerEvent.data, trackCommit: true }; this._onProcessData.fire(e); @@ -132,7 +143,7 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { } // remove size override - this._onProcessOverrideDimensions.fire(undefined); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: undefined }); } handleOrphanQuestion() { diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts index 17b25a6b1b..7eb043b8d1 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts @@ -14,11 +14,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IProcessPropertyMap, IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILocalTerminalService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; import { LocalPty } from 'vs/workbench/contrib/terminal/electron-sandbox/localPty'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; @@ -47,6 +49,7 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe @ILabelService private readonly _labelService: ILabelService, @INotificationService notificationService: INotificationService, @IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService, + @IStorageService private readonly _storageService: IStorageService, @IConfigurationResolverService configurationResolverService: IConfigurationResolverService, @IHistoryService historyService: IHistoryService, ) { @@ -54,6 +57,7 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe // Attach process listeners this._localPtyService.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event)); + this._localPtyService.onDidChangeProperty(e => this._ptys.get(e.id)?.handleDidChangeProperty(e.property)); this._localPtyService.onProcessExit(e => { const pty = this._ptys.get(e.id); if (pty) { @@ -62,10 +66,6 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe } }); this._localPtyService.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)); - this._localPtyService.onProcessTitleChanged(e => this._ptys.get(e.id)?.handleTitleChanged(e.event)); - this._localPtyService.onProcessOverrideDimensions(e => this._ptys.get(e.id)?.handleOverrideDimensions(e.event)); - this._localPtyService.onProcessResolvedShellLaunchConfig(e => this._ptys.get(e.id)?.handleResolvedShellLaunchConfig(e.event)); - this._localPtyService.onProcessDidChangeHasChildProcesses(e => this._ptys.get(e.id)?.handleDidChangeHasChildProcesses(e.event)); this._localPtyService.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)); this._localPtyService.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion()); this._localPtyService.onDidRequestDetach(e => this._onDidRequestDetach.fire(e)); @@ -138,6 +138,12 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe return this._localPtyService.acceptDetachInstanceReply(requestId, persistentProcessId); } + async persistTerminalState(): Promise { + const ids = Array.from(this._ptys.keys()); + const serialized = await this._localPtyService.serializeTerminalState(ids); + this._storageService.store(TerminalStorageKeys.TerminalBufferState, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + async updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { await this._localPtyService.updateTitle(id, title, titleSource); } @@ -146,6 +152,10 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe await this._localPtyService.updateIcon(id, icon, color); } + updateProperty(id: number, property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise { + return this._localPtyService.updateProperty(id, property, value); + } + async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, unicodeVersion: '6' | '11', env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise { const executableEnv = await this._shellEnvironmentService.getShellEnv(); const id = await this._localPtyService.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, windowsEnableConpty, shouldPersist, this._getWorkspaceId(), this._getWorkspaceName()); @@ -200,13 +210,35 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe tabs: layoutInfo ? layoutInfo.tabs : [] }; await this._localPtyService.setTerminalLayoutInfo(args); + // Store in the storage service as well to be used when reviving processes as normally this + // is stored in memory on the pty host + this._storageService.store(TerminalStorageKeys.TerminalLayoutInfo, JSON.stringify(args), StorageScope.WORKSPACE, StorageTarget.MACHINE); } async getTerminalLayoutInfo(): Promise { const layoutArgs: IGetTerminalLayoutInfoArgs = { workspaceId: this._getWorkspaceId() }; - return await this._localPtyService.getTerminalLayoutInfo(layoutArgs); + + // Revive processes if needed + const serializedState = this._storageService.get(TerminalStorageKeys.TerminalBufferState, StorageScope.WORKSPACE); + if (serializedState) { + try { + await this._localPtyService.reviveTerminalProcesses(serializedState); + this._storageService.remove(TerminalStorageKeys.TerminalBufferState, StorageScope.WORKSPACE); + // If reviving processes, send the terminal layout info back to the pty host as it + // will not have been persisted on application exit + const layoutInfo = this._storageService.get(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE); + if (layoutInfo) { + await this._localPtyService.setTerminalLayoutInfo(JSON.parse(layoutInfo)); + this._storageService.remove(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE); + } + } catch { + // no-op + } + } + + return this._localPtyService.getTerminalLayoutInfo(layoutArgs); } private _getWorkspaceId(): string { diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts index 8b6c0854d5..ac081de2f7 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts @@ -30,6 +30,10 @@ export class TerminalNativeContribution extends Disposable implements IWorkbench ipcRenderer.on('vscode:openFiles', (_: unknown, request: INativeOpenFileRequest) => this._onOpenFileRequest(request)); this._register(nativeHostService.onDidResumeOS(() => this._onOsResume())); + this._terminalService.setNativeDelegate({ + getWindowCount: () => nativeHostService.getWindowCount() + }); + const connection = remoteAgentService.getConnection(); if (connection && connection.remoteAuthority) { registerRemoteContributions(); diff --git a/src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.test.ts new file mode 100644 index 0000000000..938c045445 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/addons/lineDataEventAddon.test.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Terminal } from 'xterm'; +import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/addons/lineDataEventAddon'; +import { OperatingSystem } from 'vs/base/common/platform'; +import { deepStrictEqual } from 'assert'; + +async function writeP(terminal: Terminal, data: string): Promise { + return new Promise(r => terminal.write(data, r)); +} + +suite('LineDataEventAddon', () => { + let xterm: Terminal; + let lineDataEventAddon: LineDataEventAddon; + + suite('onLineData', () => { + let events: string[]; + + setup(() => { + xterm = new Terminal({ + cols: 4 + }); + lineDataEventAddon = new LineDataEventAddon(); + xterm.loadAddon(lineDataEventAddon); + + events = []; + lineDataEventAddon.onLineData(e => events.push(e)); + }); + + test('should fire when a non-wrapped line ends with a line feed', async () => { + await writeP(xterm, 'foo'); + deepStrictEqual(events, []); + await writeP(xterm, '\n\r'); + deepStrictEqual(events, ['foo']); + await writeP(xterm, 'bar'); + deepStrictEqual(events, ['foo']); + await writeP(xterm, '\n'); + deepStrictEqual(events, ['foo', 'bar']); + }); + + test('should not fire soft wrapped lines', async () => { + await writeP(xterm, 'foo.'); + deepStrictEqual(events, []); + await writeP(xterm, 'bar.'); + deepStrictEqual(events, []); + await writeP(xterm, 'baz.'); + deepStrictEqual(events, []); + }); + + test('should fire when a wrapped line ends with a line feed', async () => { + await writeP(xterm, 'foo.bar.baz.'); + deepStrictEqual(events, []); + await writeP(xterm, '\n\r'); + deepStrictEqual(events, ['foo.bar.baz.']); + }); + + test('should not fire on cursor move when the backing process is not on Windows', async () => { + await writeP(xterm, 'foo.\x1b[H'); + deepStrictEqual(events, []); + }); + + test('should fire on cursor move when the backing process is on Windows', async () => { + lineDataEventAddon.setOperatingSystem(OperatingSystem.Windows); + await writeP(xterm, 'foo\x1b[H'); + deepStrictEqual(events, ['foo']); + }); + }); +}); diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts index fd2071d5cc..ec7aac002b 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts @@ -17,6 +17,7 @@ const unixLinks = [ '/foo', '~/foo', './foo', + './$foo', '../foo', '/foo/bar', '/foo/bar+more', @@ -30,6 +31,7 @@ const windowsLinks = [ 'c:/foo', '.\\foo', './foo', + './$foo', '..\\foo', '~\\foo', '~/foo', @@ -69,7 +71,8 @@ const supportedLinkFormats: LinkFormatInfo[] = [ { urlFormat: '{0} [{1},{2}]', line: '5', column: '3' }, { urlFormat: '{0}[{1}, {2}]', line: '5', column: '3' }, { urlFormat: '{0} [{1}, {2}]', line: '5', column: '3' }, - { urlFormat: '{0}",{1}', line: '5' } + { urlFormat: '{0}",{1}', line: '5' }, + { urlFormat: '{0}\',{1}', line: '5' } ]; suite('Workbench - TerminalValidatedLocalLinkProvider', () => { diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts index 61a26e91d3..0da93f108d 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts @@ -106,7 +106,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk }); configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; - assert.strictEqual(configHelper.getFont().fontSize, 25, 'The maximum terminal font size should be used when terminal.integrated.fontSize more than it'); + assert.strictEqual(configHelper.getFont().fontSize, 100, 'The maximum terminal font size should be used when terminal.integrated.fontSize more than it'); await configurationService.setUserConfiguration('editor', { fontFamily: 'foo' diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts new file mode 100644 index 0000000000..d4caf9a622 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.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 { strictEqual } from 'assert'; +import { isWindows } from 'vs/base/common/platform'; +import { TerminalLabelComputer } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; +import { IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ProcessCapability } from 'vs/platform/terminal/common/terminal'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { fixPath, getUri } from 'vs/workbench/contrib/search/test/browser/queryBuilder.test'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { basename } from 'vs/base/common/path'; + +function createInstance(partial?: Partial): Pick { + return { + shellLaunchConfig: {}, + cwd: 'cwd', + initialCwd: undefined, + processName: '', + sequence: undefined, + workspaceFolder: undefined, + staticTitle: undefined, + capabilities: isWindows ? [] : [ProcessCapability.CwdDetection], + title: '', + description: '', + userHome: undefined, + ...partial + }; +} +const root1 = '/foo/root1'; +const ROOT_1 = fixPath(root1); +const root2 = '/foo/root2'; +const ROOT_2 = fixPath(root2); +const emptyRoot = '/foo'; +const ROOT_EMPTY = fixPath(emptyRoot); +suite('Workbench - TerminalInstance', () => { + suite('refreshLabel', () => { + let configurationService: TestConfigurationService; + let terminalLabelComputer: TerminalLabelComputer; + let instantiationService: TestInstantiationService; + let mockContextService: TestContextService; + let mockMultiRootContextService: TestContextService; + let emptyContextService: TestContextService; + let mockWorkspace: Workspace; + let mockMultiRootWorkspace: Workspace; + let emptyWorkspace: Workspace; + let capabilities: ProcessCapability[]; + let configHelper: TerminalConfigHelper; + setup(async () => { + instantiationService = new TestInstantiationService(); + instantiationService.stub(IWorkspaceContextService, new TestContextService()); + capabilities = isWindows ? [] : [ProcessCapability.CwdDetection]; + + const ROOT_1_URI = getUri(ROOT_1); + mockContextService = new TestContextService(); + mockWorkspace = new Workspace('workspace', [toWorkspaceFolder(ROOT_1_URI)]); + mockContextService.setWorkspace(mockWorkspace); + + const ROOT_2_URI = getUri(ROOT_2); + mockMultiRootContextService = new TestContextService(); + mockMultiRootWorkspace = new Workspace('multi-root-workspace', [toWorkspaceFolder(ROOT_1_URI), toWorkspaceFolder(ROOT_2_URI)]); + mockMultiRootContextService.setWorkspace(mockMultiRootWorkspace); + + const ROOT_EMPTY_URI = getUri(ROOT_EMPTY); + emptyContextService = new TestContextService(); + emptyWorkspace = new Workspace('empty workspace', [], ROOT_EMPTY_URI); + emptyContextService.setWorkspace(emptyWorkspace); + }); + + test('should resolve to "" when the template variables are empty', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '', description: '' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: '' }), mockContextService); + terminalLabelComputer.refreshLabel(); + // TODO: + // terminalLabelComputer.onLabelChanged(e => { + // strictEqual(e.title, ''); + // strictEqual(e.description, ''); + // }); + strictEqual(terminalLabelComputer.title, ''); + strictEqual(terminalLabelComputer.description, ''); + }); + test('should resolve cwd', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${cwd}', description: '${cwd}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, cwd: ROOT_1 }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, ROOT_1); + strictEqual(terminalLabelComputer.description, ROOT_1); + }); + test('should resolve cwdFolder in a single root workspace if cwd differs from root', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${cwdFolder}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, cwd: ROOT_2, processName: 'zsh' }), mockContextService); + terminalLabelComputer.refreshLabel(); + if (isWindows) { + strictEqual(terminalLabelComputer.title, 'zsh'); + strictEqual(terminalLabelComputer.description, ''); + } else { + strictEqual(terminalLabelComputer.title, 'zsh'); + strictEqual(terminalLabelComputer.description, basename(ROOT_2)); + } + }); + test('should resolve workspaceFolder', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${workspaceFolder}', description: '${workspaceFolder}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh', workspaceFolder: 'folder' }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'folder'); + strictEqual(terminalLabelComputer.description, 'folder'); + }); + test('should resolve local', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${local}', description: '${local}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { description: 'Local' } }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'Local'); + strictEqual(terminalLabelComputer.description, 'Local'); + }); + test('should resolve process', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${process}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh' }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'zsh'); + strictEqual(terminalLabelComputer.description, 'zsh'); + }); + test('should resolve sequence', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' - ', title: '${sequence}', description: '${sequence}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, sequence: 'sequence' }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'sequence'); + strictEqual(terminalLabelComputer.description, 'sequence'); + }); + test('should resolve task', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${task}', description: '${task}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { description: 'Task' } }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'zsh ~ Task'); + strictEqual(terminalLabelComputer.description, 'Task'); + }); + test('should resolve separator', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${separator}', description: '${separator}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'zsh', shellLaunchConfig: { description: 'Task' } }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'zsh'); + strictEqual(terminalLabelComputer.description, ''); + }); + test('should always return static title when specified', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}', description: '${workspaceFolder}' } } } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', staticTitle: 'my-title' }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'my-title'); + strictEqual(terminalLabelComputer.description, 'folder'); + }); + test('should provide cwdFolder for all cwds only when in multi-root', () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } }, cwd: ROOT_1 } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', cwd: ROOT_1 }), mockContextService); + terminalLabelComputer.refreshLabel(); + // single-root, cwd is same as root + strictEqual(terminalLabelComputer.title, 'process'); + strictEqual(terminalLabelComputer.description, ''); + // multi-root + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } }, cwd: ROOT_1 } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', cwd: ROOT_2 }), mockMultiRootContextService); + terminalLabelComputer.refreshLabel(); + if (isWindows) { + strictEqual(terminalLabelComputer.title, 'process'); + strictEqual(terminalLabelComputer.description, ''); + } else { + strictEqual(terminalLabelComputer.title, 'process ~ root2'); + strictEqual(terminalLabelComputer.description, 'root2'); + } + }); + test('should hide cwdFolder in single folder workspaces when cwd matches the workspace\'s default cwd even when slashes differ', async () => { + configurationService = new TestConfigurationService({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } }, cwd: '\\foo\\root1' } }); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', cwd: ROOT_1 }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'process'); + strictEqual(terminalLabelComputer.description, ''); + if (!isWindows) { + terminalLabelComputer = new TerminalLabelComputer(configHelper, createInstance({ capabilities, processName: 'process', workspaceFolder: 'folder', cwd: ROOT_2 }), mockContextService); + terminalLabelComputer.refreshLabel(); + strictEqual(terminalLabelComputer.title, 'process ~ root2'); + strictEqual(terminalLabelComputer.description, 'root2'); + } + }); + }); +}); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts index 98777f6762..a9137468a0 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts @@ -15,13 +15,16 @@ import { EnvironmentVariableService } from 'vs/workbench/contrib/terminal/common import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Workbench - TerminalProcessManager', () => { + let disposables: DisposableStore; let instantiationService: ITestInstantiationService; let manager: TerminalProcessManager; setup(async () => { - instantiationService = workbenchInstantiationService(); + disposables = new DisposableStore(); + instantiationService = workbenchInstantiationService(undefined, disposables); const configurationService = new TestConfigurationService(); await configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); await configurationService.setUserConfiguration('terminal', { @@ -39,6 +42,10 @@ suite('Workbench - TerminalProcessManager', () => { manager = instantiationService.createInstance(TerminalProcessManager, 1, configHelper); }); + teardown(() => { + disposables.dispose(); + }); + suite('process persistence', () => { suite('local', () => { test('regular terminal should persist', async () => { diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index 72b3e65ea5..a646ae72d6 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -144,8 +144,15 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes break; } + // parent needs to be re-rendered on an expand update, so that its + // children are rewritten. + const needsParentUpdate = existing.test.expand === TestItemExpandState.NotExpandable && patch.expand; existing.update(patch); - this.addUpdated(existing); + if (needsParentUpdate) { + this.changes.addedOrRemoved(existing); + } else { + this.changes.updated(existing); + } break; } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts index 9de446b219..2162e22ecd 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts @@ -10,7 +10,7 @@ import { IActionableTestTreeElement, TestExplorerTreeElement, TestItemTreeElemen export const testIdentityProvider: IIdentityProvider = { getId(element) { - return element.treeId; + return element.treeId + '\0' + element.test.expand; } }; diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 18c392b94f..8c6aa075c7 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -191,14 +191,6 @@ cursor: pointer; } -.monaco-editor .testing-inline-message-content { - cursor: pointer; -} - -.monaco-editor .testing-inline-message-line { - background: red; -} - .testing-diff-title-widget { line-height: 19px; font-size: 12px; @@ -209,3 +201,9 @@ text-overflow: ellipsis; white-space: nowrap; } + +.test-message-inline-content { + font-family: var(--testMessageDecorationFontFamily); + font-size: var(--testMessageDecorationFontSize); + cursor: pointer; +} diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 998fde0677..60bc3aa217 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -7,19 +7,19 @@ import { Codicon } from 'vs/base/common/codicons'; import { Iterable } from 'vs/base/common/iterator'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { isDefined } from 'vs/base/common/types'; -import { Range } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; import { Action2, IAction2Options, MenuId } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyAndExpr, ContextKeyEqualsExpr, ContextKeyFalseExpr, ContextKeyGreaterExpr, ContextKeyTrueExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyGreaterExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { FocusedViewContext } from 'vs/workbench/common/views'; +import { FocusedViewContext, ViewContainerLocation } from 'vs/workbench/common/views'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IActionableTestTreeElement, TestItemTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; @@ -36,7 +36,7 @@ import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { expandAndGetTestById, IMainThreadTestCollection, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; const category = CATEGORIES.Test; @@ -270,11 +270,11 @@ abstract class ExecuteSelectedAction extends ViewAction { ? ActionOrder.Debug : ActionOrder.Coverage, group: 'navigation', - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId), + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', Testing.ExplorerViewId), TestingContextKeys.isRunning.isEqualTo(false), TestingContextKeys.capabilityToContextKey[group].isEqualTo(true), - ]) + ) }], category, viewId: Testing.ExplorerViewId, @@ -360,7 +360,7 @@ export class RunAllAction extends RunOrDebugAllTestsAction { icon: icons.testingRunAllIcon, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyCode.KEY_A), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyCode.KeyA), }, }, TestRunProfileBitset.Run, @@ -379,7 +379,7 @@ export class DebugAllAction extends RunOrDebugAllTestsAction { icon: icons.testingDebugIcon, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyMod.CtrlCmd | KeyCode.KEY_A), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyA), }, }, TestRunProfileBitset.Debug, @@ -397,16 +397,16 @@ export class CancelTestRunAction extends Action2 { icon: icons.testingCancelIcon, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyMod.CtrlCmd | KeyCode.KEY_X), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyX), }, menu: { id: MenuId.ViewTitle, order: ActionOrder.Run, group: 'navigation', - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId), - ContextKeyEqualsExpr.create(TestingContextKeys.isRunning.serialize(), true), - ]) + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', Testing.ExplorerViewId), + ContextKeyExpr.equals(TestingContextKeys.isRunning.serialize(), true), + ) } }); } @@ -437,7 +437,7 @@ export class TestingViewAsListAction extends ViewAction { id: MenuId.ViewTitle, order: ActionOrder.DisplayMode, group: 'viewAs', - when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId) + when: ContextKeyExpr.equals('view', Testing.ExplorerViewId) } }); } @@ -462,7 +462,7 @@ export class TestingViewAsTreeAction extends ViewAction { id: MenuId.ViewTitle, order: ActionOrder.DisplayMode, group: 'viewAs', - when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId) + when: ContextKeyExpr.equals('view', Testing.ExplorerViewId) } }); } @@ -488,7 +488,7 @@ export class TestingSortByStatusAction extends ViewAction { id: MenuId.ViewTitle, order: ActionOrder.Sort, group: 'sortBy', - when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId) + when: ContextKeyExpr.equals('view', Testing.ExplorerViewId) } }); } @@ -513,7 +513,7 @@ export class TestingSortByLocationAction extends ViewAction id: MenuId.ViewTitle, order: ActionOrder.Sort, group: 'sortBy', - when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId) + when: ContextKeyExpr.equals('view', Testing.ExplorerViewId) } }); } @@ -526,6 +526,31 @@ export class TestingSortByLocationAction extends ViewAction } } +export class TestingSortByDurationAction extends ViewAction { + public static readonly ID = 'testing.sortByDuration'; + constructor() { + super({ + id: TestingSortByDurationAction.ID, + viewId: Testing.ExplorerViewId, + title: localize('testing.sortByDuration', "Sort by Duration"), + toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByDuration), + menu: { + id: MenuId.ViewTitle, + order: ActionOrder.Sort, + group: 'sortBy', + when: ContextKeyExpr.equals('view', Testing.ExplorerViewId) + } + }); + } + + /** + * @override + */ + public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) { + view.viewModel.viewSorting = TestExplorerViewSorting.ByDuration; + } +} + export class ShowMostRecentOutputAction extends Action2 { public static readonly ID = 'testing.showMostRecentOutput'; constructor() { @@ -536,14 +561,14 @@ export class ShowMostRecentOutputAction extends Action2 { icon: Codicon.terminal, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyMod.CtrlCmd | KeyCode.KEY_O), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyO), }, precondition: TestingContextKeys.hasAnyResults.isEqualTo(true), menu: [{ id: MenuId.ViewTitle, order: ActionOrder.Collapse, group: 'navigation', - when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId), + when: ContextKeyExpr.equals('view', Testing.ExplorerViewId), }, { id: MenuId.CommandPalette, when: TestingContextKeys.hasAnyResults.isEqualTo(true) @@ -569,7 +594,7 @@ export class CollapseAllAction extends ViewAction { id: MenuId.ViewTitle, order: ActionOrder.Collapse, group: 'displayAction', - when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId) + when: ContextKeyExpr.equals('view', Testing.ExplorerViewId) } }); } @@ -599,7 +624,7 @@ export class ClearTestResultsAction extends Action2 { id: MenuId.ViewTitle, order: ActionOrder.ClearResults, group: 'displayAction', - when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId) + when: ContextKeyExpr.equals('view', Testing.ExplorerViewId) }], }); } @@ -648,15 +673,15 @@ abstract class ToggleAutoRun extends Action2 { id: ToggleAutoRun.ID, title, icon: icons.testingAutorunIcon, - toggled: whenToggleIs === true ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE, + toggled: whenToggleIs === true ? ContextKeyExpr.true() : ContextKeyExpr.false(), menu: { id: MenuId.ViewTitle, order: ActionOrder.AutoRun, group: 'navigation', - when: ContextKeyAndExpr.create([ - ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId), + when: ContextKeyExpr.and( + ContextKeyExpr.equals('view', Testing.ExplorerViewId), TestingContextKeys.autoRun.isEqualTo(whenToggleIs) - ]) + ) } }); } @@ -705,21 +730,34 @@ abstract class ExecuteTestAtCursor extends Action2 { } const testService = accessor.get(ITestService); - let bestNode: InternalTestItem | undefined; + const profileService = accessor.get(ITestProfileService); + let bestNodes: InternalTestItem[] = []; + let bestRange: IRange | undefined; + + // testsInFile will descend in the test tree. We assume that as we go + // deeper, ranges get more specific. We'll want to run all tests whose + // range is equal to the most specific range we find (see #133519) await showDiscoveringWhile(accessor.get(IProgressService), (async () => { for await (const test of testsInFile(testService.collection, model.uri)) { - if (test.item.range && Range.containsPosition(test.item.range, position)) { - bestNode = test; + if (!test.item.range || !Range.containsPosition(test.item.range, position) || !(profileService.capabilitiesForTest(test) & this.group)) { + continue; + } + + if (bestRange && Range.equalsRange(test.item.range, bestRange)) { + bestNodes.push(test); + } else { + bestRange = test.item.range; + bestNodes = [test]; } } })()); - if (bestNode) { + if (bestNodes.length) { await testService.runTests({ group: this.group, - tests: [bestNode], + tests: bestNodes, }); } } @@ -735,7 +773,7 @@ export class RunAtCursor extends ExecuteTestAtCursor { keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyCode.KEY_C), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyCode.KeyC), }, }, TestRunProfileBitset.Run); } @@ -751,7 +789,7 @@ export class DebugAtCursor extends ExecuteTestAtCursor { keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyMod.CtrlCmd | KeyCode.KEY_C), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyC), }, }, TestRunProfileBitset.Debug); } @@ -806,7 +844,7 @@ export class RunCurrentFile extends ExecuteTestsInCurrentFile { keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyCode.KEY_F), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyCode.KeyF), }, }, TestRunProfileBitset.Run); } @@ -823,7 +861,7 @@ export class DebugCurrentFile extends ExecuteTestsInCurrentFile { keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyMod.CtrlCmd | KeyCode.KEY_F), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyF), }, }, TestRunProfileBitset.Debug); } @@ -896,10 +934,10 @@ abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { ...options, menu: { id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([ + when: ContextKeyExpr.and( hasAnyTestProvider, TestingContextKeys.hasAnyResults.isEqualTo(true), - ]), + ), }, }); } @@ -931,7 +969,7 @@ export class ReRunFailedTests extends RunOrDebugFailedTests { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyCode.KEY_E), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyCode.KeyE), }, }); } @@ -953,7 +991,7 @@ export class DebugFailedTests extends RunOrDebugFailedTests { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyMod.CtrlCmd | KeyCode.KEY_E), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyE), }, }); } @@ -975,7 +1013,7 @@ export class ReRunLastRun extends RunOrDebugLastRun { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyCode.KEY_L), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyCode.KeyL), }, }); } @@ -997,7 +1035,7 @@ export class DebugLastRun extends RunOrDebugLastRun { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyMod.CtrlCmd | KeyCode.KEY_L), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyL), }, }); } @@ -1020,8 +1058,8 @@ export class SearchForTestExtension extends Action2 { } public async run(accessor: ServicesAccessor) { - const viewletService = accessor.get(IViewletService); - const viewlet = (await viewletService.openViewlet(EXTENSIONS_VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + const paneCompositeService = accessor.get(IPaneCompositePartService); + const viewlet = (await paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; viewlet.search('@category:"testing"'); viewlet.focus(); } @@ -1036,7 +1074,7 @@ export class OpenOutputPeek extends Action2 { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyCode.KEY_M), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyCode.KeyM), }, menu: { id: MenuId.CommandPalette, @@ -1059,7 +1097,7 @@ export class ToggleInlineTestOutput extends Action2 { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyMod.CtrlCmd | KeyCode.KEY_I), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyI), }, menu: { id: MenuId.CommandPalette, @@ -1105,6 +1143,7 @@ export const allTestActions = [ ShowMostRecentOutputAction, TestingSortByLocationAction, TestingSortByStatusAction, + TestingSortByDurationAction, TestingViewAsListAction, TestingViewAsTreeAction, ToggleInlineTestOutput, diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 8580319fd1..84dfded437 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -20,20 +20,21 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } fr import { Extensions as ViewContainerExtensions, IViewContainersRegistry, IViewsRegistry, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { REVEAL_IN_EXPLORER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons'; -import { TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations'; -import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; +import { TestingDecorations, TestingDecorationService } from 'vs/workbench/contrib/testing/browser/testingDecorations'; import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; -import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestingOutputPeekController, TestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; +import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { ITestingOutputTerminalService, TestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; -import { ITestingProgressUiService, TestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; +import { ITestingProgressUiService, TestingProgressTrigger, TestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { ITestingDecorationsService } from 'vs/workbench/contrib/testing/common/testingDecorations'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { ITestProfileService, TestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResultService, TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; @@ -53,6 +54,7 @@ registerSingleton(ITestingAutoRun, TestingAutoRun, true); registerSingleton(ITestingOutputTerminalService, TestingOutputTerminalService, true); registerSingleton(ITestingPeekOpener, TestingPeekOpener, true); registerSingleton(ITestingProgressUiService, TestingProgressUiService, true); +registerSingleton(ITestingDecorationsService, TestingDecorationService, true); const viewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: Testing.ViewletId, @@ -108,18 +110,19 @@ registerAction2(OpenMessageInEditorAction); registerAction2(GoToPreviousMessageAction); registerAction2(GoToNextMessageAction); registerAction2(CloseTestPeek); +registerAction2(ToggleTestingPeekHistory); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingPeekOpener, LifecyclePhase.Eventually); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingProgressUiService, LifecyclePhase.Eventually); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingProgressTrigger, LifecyclePhase.Eventually); registerEditorContribution(Testing.OutputPeekContributionId, TestingOutputPeekController); registerEditorContribution(Testing.DecorationsContributionId, TestingDecorations); CommandsRegistry.registerCommand({ - id: 'vscode.revealTestInExplorer', - handler: async (accessor: ServicesAccessor, testId: string, focus?: boolean) => { - accessor.get(ITestExplorerFilterState).reveal.value = testId; + id: '_revealTestInExplorer', + handler: async (accessor: ServicesAccessor, testId: string | ITestItem, focus?: boolean) => { + accessor.get(ITestExplorerFilterState).reveal.value = typeof testId === 'string' ? testId : testId.extId; accessor.get(IViewsService).openView(Testing.ExplorerViewId, focus); } }); @@ -159,11 +162,15 @@ CommandsRegistry.registerCommand({ const fileService = accessor.get(IFileService); const openerService = accessor.get(IOpenerService); - const { range, uri } = test.item; + let { range, uri } = test.item; if (!uri) { return; } + // If an editor has the file open, there are decorations. Try to adjust the + // revealed range to those decorations (#133441). + range = accessor.get(ITestingDecorationsService).getDecoratedRangeForTest(uri, extId) || range; + accessor.get(ITestExplorerFilterState).reveal.value = extId; accessor.get(ITestingPeekOpener).closeAllPeeks(); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index e981ccfbae..2d06fe4a47 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -6,19 +6,22 @@ import * as dom from 'vs/base/browser/dom'; import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; -import { Event } from 'vs/base/common/event'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; -import { setImmediate } from 'vs/base/common/platform'; +import { equals } from 'vs/base/common/arrays'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable, DisposableStore, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDeltaDecoration, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/modelService'; import { editorCodeLensForeground, overviewRulerError, overviewRulerInfo } from 'vs/editor/common/view/editorColorRegistry'; import { localize } from 'vs/nls'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -28,15 +31,16 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IThemeService, registerThemingParticipant, themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; -import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme'; import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { labelForTestInState } from 'vs/workbench/contrib/testing/common/constants'; +import { labelForTestInState, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestDecoration as IPublicTestDecoration, ITestingDecorationsService, TestDecorations } from 'vs/workbench/contrib/testing/common/testingDecorations'; +import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState, maxPriority } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; @@ -56,13 +60,20 @@ function isOriginalInDiffEditor(codeEditorService: ICodeEditorService, codeEdito return false; } -const FONT_FAMILY_VAR = `--testMessageDecorationFontFamily`; +interface ITestDecoration extends IPublicTestDecoration { + id: string; + click(e: IEditorMouseEvent): boolean; +} -export class TestingDecorations extends Disposable implements IEditorContribution { - private currentUri?: URI; - private lastDecorations: ITestDecoration[] = []; - private readonly expectedWidget = new MutableDisposable(); - private readonly actualWidget = new MutableDisposable(); +export class TestingDecorationService extends Disposable implements ITestingDecorationsService { + declare public _serviceBrand: undefined; + + private generation = 0; + private readonly changeEmitter = new Emitter(); + private readonly decorationCache = new ResourceMap<{ + generation: number; + value: TestDecorations; + }>(); /** * List of messages that should be hidden because an editor changed their @@ -72,52 +83,252 @@ export class TestingDecorations extends Disposable implements IEditorContributio * - Message instances are stable for any completed test results for * the duration of the session. */ - private invalidatedMessages = new WeakSet(); + private readonly invalidatedMessages = new WeakSet(); + + /** @inheritdoc */ + public readonly onDidChange = this.changeEmitter.event; constructor( - private readonly editor: ICodeEditor, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @ICodeEditorService codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITestService private readonly testService: ITestService, @ITestResultService private readonly results: ITestResultService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IModelService private readonly modelService: IModelService, ) { super(); - this.attachModel(editor.getModel()?.uri); - this._register(this.editor.onDidChangeModel(e => this.attachModel(e.newModelUrl || undefined))); - this._register(this.editor.onMouseDown(e => { - for (const decoration of this.lastDecorations) { - if (decoration.click(e)) { - e.event.stopPropagation(); - return; - } + codeEditorService.registerDecorationType('test-message-decoration', TestMessageDecoration.decorationId, {}, undefined); + + modelService.onModelRemoved(e => this.decorationCache.delete(e.uri)); + + const debounceInvalidate = this._register(new RunOnceScheduler(() => this.invalidate(), 100)); + + this._register(Event.any( + this.results.onResultsChanged, + this.results.onTestChanged, + this.testService.excluded.onTestExclusionsChanged, + this.testService.showInlineOutput.onDidChange, + this.testService.onDidProcessDiff, + Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(TestingConfigKeys.GutterEnabled)), + )(() => { + if (!debounceInvalidate.isScheduled()) { + debounceInvalidate.schedule(); } })); - this._register(this.editor.onDidChangeModelContent(e => { - if (!this.currentUri) { - return; + } + + /** @inheritdoc */ + public invalidateResultMessage(message: ITestMessage) { + this.invalidatedMessages.add(message); + this.invalidate(); + } + + /** @inheritdoc */ + public syncDecorations(resource: URI): TestDecorations { + const model = this.modelService.getModel(resource); + if (!model) { + return new TestDecorations(); + } + + const cached = this.decorationCache.get(resource); + if (cached?.generation === this.generation) { + return cached.value; + } + + return this.applyDecorations(model); + } + + /** @inheritdoc */ + public getDecoratedRangeForTest(resource: URI, testId: string) { + const model = this.modelService.getModel(resource); + if (!model) { + return undefined; + } + + const decoration = this.syncDecorations(resource).value.find(v => v instanceof RunTestDecoration && v.isForTest(testId)); + if (!decoration) { + return undefined; + } + + return model.getDecorationRange(decoration.id) || undefined; + } + + private invalidate() { + this.generation++; + this.changeEmitter.fire(); + } + + /** + * Applies the current set of test decorations to the given text model. + */ + private applyDecorations(model: ITextModel) { + const gutterEnabled = getTestingConfiguration(this.configurationService, TestingConfigKeys.GutterEnabled); + const uriStr = model.uri.toString(); + const lastDecorations = this.decorationCache.get(model.uri)?.value ?? new TestDecorations(); + const newDecorations = new TestDecorations(); + + model.changeDecorations(accessor => { + const runDecorations = new TestDecorations<{ line: number; id: ''; test: IncrementalTestCollectionItem, resultItem: TestResultItem | undefined }>(); + for (const test of this.testService.collection.all) { + if (!test.item.range || test.item.uri?.toString() !== uriStr) { + continue; + } + + const stateLookup = this.results.getStateById(test.item.extId); + const line = test.item.range.startLineNumber; + runDecorations.push({ line, id: '', test, resultItem: stateLookup?.[1] }); } - let update = false; - for (const change of e.changes) { - for (const deco of this.lastDecorations) { - if (deco instanceof TestMessageDecoration - && deco.location.range.startLineNumber >= change.range.startLineNumber - && deco.location.range.endLineNumber <= change.range.endLineNumber - ) { - this.invalidatedMessages.add(deco.testMessage); - update = true; + for (const [line, tests] of runDecorations.lines()) { + const multi = tests.length > 1; + const existing = lastDecorations.findOnLine(line, d => multi ? d instanceof MultiRunTestDecoration : d instanceof RunSingleTestDecoration) as RunTestDecoration; + if (existing) { + if (existing.replaceOptions(tests, gutterEnabled)) { + accessor.changeDecorationOptions(existing.id, existing.editorDecoration.options); + } + newDecorations.push(existing); + } else { + newDecorations.push(multi + ? this.instantiationService.createInstance(MultiRunTestDecoration, tests, gutterEnabled, model) + : this.instantiationService.createInstance(RunSingleTestDecoration, tests[0].test, tests[0].resultItem, model, gutterEnabled)); + + } + } + + const lastResult = this.results.results[0]; + if (this.testService.showInlineOutput.value && lastResult instanceof LiveTestResult) { + for (const task of lastResult.tasks) { + for (const m of task.otherMessages) { + if (!this.invalidatedMessages.has(m) && m.location?.uri.toString() === uriStr) { + const decoration = lastDecorations.findOnLine(m.location.range.startLineNumber, l => l instanceof TestMessageDecoration && l.testMessage === m) + || this.instantiationService.createInstance(TestMessageDecoration, m, undefined, model); + newDecorations.push(decoration); + } + } + } + + const messageLines = new Set(); + for (const test of lastResult.tests) { + for (let taskId = 0; taskId < test.tasks.length; taskId++) { + const state = test.tasks[taskId]; + for (let i = 0; i < state.messages.length; i++) { + const m = state.messages[i]; + if (this.invalidatedMessages.has(m) || m.location?.uri.toString() !== uriStr) { + continue; + } + + // Only add one message per line number. Overlapping messages + // don't appear well, and the peek will show all of them (#134129) + const line = m.location.range.startLineNumber; + if (messageLines.has(line)) { + continue; + } + messageLines.add(line); + + const previous = lastDecorations.findOnLine(line, l => l instanceof TestMessageDecoration && l.testMessage === m); + if (previous) { + newDecorations.push(previous); + continue; + } + + const messageUri = m.type === TestMessageType.Info ? undefined : buildTestUri({ + type: TestUriType.ResultActualOutput, + messageIndex: i, + taskIndex: taskId, + resultId: lastResult.id, + testExtId: test.item.extId, + }); + + newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, messageUri, model)); + } } } } - if (update) { - this.setDecorations(this.currentUri); + const saveFromRemoval = new Set(); + for (const decoration of newDecorations.value) { + if (decoration.id === '') { + decoration.id = accessor.addDecoration(decoration.editorDecoration.range, decoration.editorDecoration.options); + } else { + saveFromRemoval.add(decoration.id); + } + } + + for (const decoration of lastDecorations.value) { + if (!saveFromRemoval.has(decoration.id)) { + accessor.removeDecoration(decoration.id); + } + } + + this.decorationCache.set(model.uri, { generation: this.generation, value: newDecorations }); + }); + + return newDecorations; + } +} + +export class TestingDecorations extends Disposable implements IEditorContribution { + /** + * Gets the decorations associated with the given code editor. + */ + public static get(editor: ICodeEditor): TestingDecorations { + return editor.getContribution(Testing.DecorationsContributionId); + } + + private currentUri?: URI; + private readonly expectedWidget = new MutableDisposable(); + private readonly actualWidget = new MutableDisposable(); + + constructor( + private readonly editor: ICodeEditor, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @ITestService private readonly testService: ITestService, + @ITestingDecorationsService private readonly decorations: ITestingDecorationsService, + ) { + super(); + + codeEditorService.registerDecorationType('test-message-decoration', TestMessageDecoration.decorationId, {}, undefined, editor); + + this.attachModel(editor.getModel()?.uri); + this._register(decorations.onDidChange(() => { + if (this.currentUri) { + decorations.syncDecorations(this.currentUri); + } + })); + this._register(this.editor.onDidChangeModel(e => this.attachModel(e.newModelUrl || undefined))); + this._register(this.editor.onMouseDown(e => { + if (e.target.position && this.currentUri) { + const modelDecorations = editor.getModel()?.getDecorationsInRange(Range.fromPositions(e.target.position)) ?? []; + for (const { id } of modelDecorations) { + const cache = decorations.syncDecorations(this.currentUri) as TestDecorations; + if (cache.get(id)?.click(e)) { + e.event.stopPropagation(); + return; + } + } + } + })); + this._register(this.editor.onDidChangeModelContent(e => { + const model = editor.getModel(); + if (!this.currentUri || !model) { + return; + } + + const currentDecorations = decorations.syncDecorations(this.currentUri); + for (const change of e.changes) { + const modelDecorations = model.getLinesDecorations(change.range.startLineNumber, change.range.endLineNumber); + for (const { id } of modelDecorations) { + const decoration = currentDecorations.get(id); + if (decoration instanceof TestMessageDecoration) { + decorations.invalidateResultMessage(decoration.testMessage); + } + } } })); const updateFontFamilyVar = () => { - this.editor.getContainerDomNode().style.setProperty(FONT_FAMILY_VAR, editor.getOption(EditorOption.fontFamily)); + this.editor.getContainerDomNode().style.setProperty('--testMessageDecorationFontFamily', editor.getOption(EditorOption.fontFamily)); + this.editor.getContainerDomNode().style.setProperty('--testMessageDecorationFontSize', `${editor.getOption(EditorOption.fontSize)}px`); }; this._register(this.editor.onDidChangeConfiguration((e) => { if (e.hasChanged(EditorOption.fontFamily)) { @@ -125,25 +336,6 @@ export class TestingDecorations extends Disposable implements IEditorContributio } })); updateFontFamilyVar(); - - this._register(this.results.onTestChanged(({ item: result }) => { - if (this.currentUri && result.item.uri && result.item.uri.toString() === this.currentUri.toString()) { - this.setDecorations(this.currentUri); - } - })); - - this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TestingConfigKeys.GutterEnabled)) { - this.setDecorations(this.currentUri); - } - })); - - this._register(Event.any( - this.results.onResultsChanged, - this.testService.excluded.onTestExclusionsChanged, - this.testService.showInlineOutput.onDidChange, - this.testService.onDidProcessDiff, - )(() => this.setDecorations(this.currentUri))); } private attachModel(uri?: URI) { @@ -168,10 +360,11 @@ export class TestingDecorations extends Disposable implements IEditorContributio this.currentUri = uri; if (!uri) { - this.clearDecorations(); return; } + this.decorations.syncDecorations(uri); + (async () => { for await (const _test of testsInFile(this.testService.collection, uri)) { // consume the iterator so that all tests in the file get expanded. Or @@ -182,110 +375,9 @@ export class TestingDecorations extends Disposable implements IEditorContributio } } })(); - - this.setDecorations(uri); - } - - private setDecorations(uri: URI | undefined): void { - if (!uri) { - this.clearDecorations(); - return; - } - - const gutterEnabled = getTestingConfiguration(this.configurationService, TestingConfigKeys.GutterEnabled); - - this.editor.changeDecorations(accessor => { - const newDecorations: ITestDecoration[] = []; - if (gutterEnabled) { - for (const test of this.testService.collection.all) { - if (!test.item.range || test.item.uri?.toString() !== uri.toString()) { - continue; - } - - const stateLookup = this.results.getStateById(test.item.extId); - const line = test.item.range.startLineNumber; - const resultItem = stateLookup?.[1]; - const existing = newDecorations.findIndex(d => d instanceof RunTestDecoration && d.line === line); - if (existing !== -1) { - newDecorations[existing] = (newDecorations[existing] as RunTestDecoration).merge(test, resultItem); - } else { - newDecorations.push(this.instantiationService.createInstance(RunSingleTestDecoration, test, this.editor, stateLookup?.[1])); - } - } - } - - const lastResult = this.results.results[0]; - if (this.testService.showInlineOutput.value && lastResult instanceof LiveTestResult) { - for (const task of lastResult.tasks) { - for (const m of task.otherMessages) { - if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) { - newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor)); - } - } - } - - for (const test of lastResult.tests) { - for (let taskId = 0; taskId < test.tasks.length; taskId++) { - const state = test.tasks[taskId]; - for (let i = 0; i < state.messages.length; i++) { - const m = state.messages[i]; - if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) { - const uri = m.type === TestMessageType.Info ? undefined : buildTestUri({ - type: TestUriType.ResultActualOutput, - messageIndex: i, - taskIndex: taskId, - resultId: lastResult.id, - testExtId: test.item.extId, - }); - - newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor)); - } - } - } - } - } - - accessor - .deltaDecorations(this.lastDecorations.map(d => d.id), newDecorations.map(d => d.editorDecoration)) - .forEach((id, i) => newDecorations[i].id = id); - - this.lastDecorations = newDecorations; - }); - } - - private clearDecorations(): void { - if (!this.lastDecorations.length) { - return; - } - - this.editor.changeDecorations(accessor => { - for (const decoration of this.lastDecorations) { - accessor.removeDecoration(decoration.id); - } - - this.lastDecorations = []; - }); } } -interface ITestDecoration extends IDisposable { - /** - * ID of the decoration after being added to the editor, set after the - * decoration is applied. - */ - id: string; - - readonly editorDecoration: IModelDeltaDecoration; - - /** - * Handles a click event, returns true if it was handled. - */ - click(e: IEditorMouseEvent): boolean; -} - -const hasValidLocation = (editorUri: URI, t: T): t is T & { location: IRichLocation } => - t.location?.uri.toString() === editorUri.toString(); - const firstLineRange = (originalRange: IRange) => ({ startLineNumber: originalRange.startLineNumber, endLineNumber: originalRange.startLineNumber, @@ -293,12 +385,16 @@ const firstLineRange = (originalRange: IRange) => ({ endColumn: 1, }); -const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[], states: readonly (TestResultItem | undefined)[]): IModelDeltaDecoration => { +const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[], states: readonly (TestResultItem | undefined)[], visible: boolean): IModelDeltaDecoration => { const range = tests[0]?.item.range; if (!range) { throw new Error('Test decorations can only be created for tests with a range'); } + if (!visible) { + return { range: firstLineRange(range), options: { isWholeLine: true, description: 'run-test-decoration' } }; + } + let computedState = TestResultState.Unset; let hoverMessageParts: string[] = []; let testIdWithMessages: string | undefined; @@ -307,7 +403,9 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] const test = tests[i]; const resultItem = states[i]; const state = resultItem?.computedState ?? TestResultState.Unset; - hoverMessageParts.push(labelForTestInState(test.item.label, state)); + if (hoverMessageParts.length < 10) { + hoverMessageParts.push(labelForTestInState(test.item.label, state)); + } computedState = maxPriority(computedState, state); retired = retired || !!resultItem?.retired; if (!testIdWithMessages && resultItem?.tasks.some(t => t.messages.length)) { @@ -320,11 +418,7 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] ? (hasMultipleTests ? testingRunAllIcon : testingRunIcon) : testingStatesToIcons.get(computedState)!; - const hoverMessage = new MarkdownString('', true).appendText(hoverMessageParts.join(', ') + '.'); - if (testIdWithMessages) { - const args = encodeURIComponent(JSON.stringify([testIdWithMessages])); - hoverMessage.appendMarkdown(`[${localize('peekTestOutout', 'Peek Test Output')}](command:vscode.peekTestError?${args})`); - } + let hoverMessage: IMarkdownString | undefined; let glyphMarginClassName = ThemeIcon.asClassName(icon) + ' testing-run-glyph'; if (retired) { @@ -336,7 +430,17 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] options: { description: 'run-test-decoration', isWholeLine: true, - hoverMessage, + get hoverMessage() { + if (!hoverMessage) { + const building = hoverMessage = new MarkdownString('', true).appendText(hoverMessageParts.join(', ') + '.'); + if (testIdWithMessages) { + const args = encodeURIComponent(JSON.stringify([testIdWithMessages])); + building.appendMarkdown(`[${localize('peekTestOutout', 'Peek Test Output')}](command:vscode.peekTestError?${args})`); + } + } + + return hoverMessage; + }, glyphMarginClassName, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, } @@ -358,7 +462,7 @@ abstract class TitleLensContentWidget { private viewZoneId?: string; constructor(private readonly editor: ICodeEditor) { - setImmediate(() => { + queueMicrotask(() => { this.applyStyling(); this.editor.addContentWidget(this); }); @@ -436,7 +540,7 @@ class ExpectedLensContentWidget extends TitleLensContentWidget { } protected override getText() { - return localize('expected.title', 'Expected:'); + return localize('expected.title', 'Expected'); } } @@ -447,11 +551,11 @@ class ActualLensContentWidget extends TitleLensContentWidget { } protected override getText() { - return localize('actual.title', 'Actual:'); + return localize('actual.title', 'Actual'); } } -abstract class RunTestDecoration extends Disposable { +abstract class RunTestDecoration { /** @inheritdoc */ public id = ''; @@ -459,9 +563,16 @@ abstract class RunTestDecoration extends Disposable { return this.editorDecoration.range.startLineNumber; } + public editorDecoration: IModelDeltaDecoration; + constructor( - public editorDecoration: IModelDeltaDecoration, - protected readonly editor: ICodeEditor, + protected readonly tests: { + test: IncrementalTestCollectionItem, + resultItem: TestResultItem | undefined, + }[], + private visible: boolean, + protected readonly model: ITextModel, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @ITestService protected readonly testService: ITestService, @IContextMenuService protected readonly contextMenuService: IContextMenuService, @ICommandService protected readonly commandService: ICommandService, @@ -470,13 +581,13 @@ abstract class RunTestDecoration extends Disposable { @IContextKeyService protected readonly contextKeyService: IContextKeyService, @IMenuService protected readonly menuService: IMenuService, ) { - super(); - editorDecoration.options.glyphMarginHoverMessage = new MarkdownString().appendText(this.getGutterLabel()); + this.editorDecoration = createRunTestDecoration(tests.map(t => t.test), tests.map(t => t.resultItem), visible); + this.editorDecoration.options.glyphMarginHoverMessage = new MarkdownString().appendText(this.getGutterLabel()); } /** @inheritdoc */ public click(e: IEditorMouseEvent): boolean { - if (e.target.position?.lineNumber !== this.line || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) { + if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) { return false; } @@ -502,37 +613,61 @@ abstract class RunTestDecoration extends Disposable { } /** - * Adds the test to this decoration. + * Updates the decoration to match the new set of tests. + * @returns true if options were changed, false otherwise */ - public abstract merge(other: IncrementalTestCollectionItem, resultItem: TestResultItem | undefined): RunTestDecoration; + public replaceOptions(newTests: readonly { + test: IncrementalTestCollectionItem, + resultItem: TestResultItem | undefined, + }[], visible: boolean): boolean { + if (visible === this.visible + && equals(this.tests.map(t => t.test.item.extId), newTests.map(t => t.test.item.extId)) + && this.tests.map(t => t.resultItem?.computedState) === newTests.map(t => t.resultItem?.computedState)) { + return false; + } + + this.visible = visible; + this.editorDecoration.options = createRunTestDecoration(newTests.map(t => t.test), newTests.map(t => t.resultItem), visible).options; + return true; + } + + /** + * Gets whether this decoration serves as the run button for the given test ID. + */ + public isForTest(testId: string) { + return this.tests.some(t => t.test.item.extId === testId); + } /** * Called when the decoration is clicked on. */ protected abstract getContextMenuActions(e: IEditorMouseEvent): IReference; - /** - * Default run action. - */ - protected abstract defaultRun(): void; + protected defaultRun() { + return this.testService.runTests({ + tests: this.tests.map(({ test }) => test), + group: TestRunProfileBitset.Run, + }); + } - /** - * Default debug action. - */ - protected abstract defaultDebug(): void; + protected defaultDebug() { + return this.testService.runTests({ + tests: this.tests.map(({ test }) => test), + group: TestRunProfileBitset.Debug, + }); + } private showContextMenu(e: IEditorMouseEvent) { let actions = this.getContextMenuActions(e); - - const model = this.editor.getModel(); - if (model) { + const editor = this.codeEditorService.listCodeEditors().find(e => e.getModel() === this.model); + if (editor) { actions = { dispose: actions.dispose, object: Separator.join( actions.object, - this.editor + editor .getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID) - .getContextMenuActionsAtPosition(this.line, model) + .getContextMenuActionsAtPosition(this.line, this.model) ) }; } @@ -600,7 +735,7 @@ abstract class RunTestDecoration extends Disposable { } testActions.push(new Action('testing.gutter.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined, - () => this.commandService.executeCommand('vscode.revealTestInExplorer', test.item.extId))); + () => this.commandService.executeCommand('_revealTestInExplorer', test.item.extId))); const contributed = this.getContributedTestActions(test, capabilities); return { object: Separator.join(testActions, contributed.object), dispose: contributed.dispose }; @@ -622,27 +757,12 @@ abstract class RunTestDecoration extends Disposable { } class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoration { - constructor( - private readonly tests: { - test: IncrementalTestCollectionItem, - resultItem: TestResultItem | undefined, - }[], - editor: ICodeEditor, - @ITestService testService: ITestService, - @ICommandService commandService: ICommandService, - @IContextMenuService contextMenuService: IContextMenuService, - @IConfigurationService configurationService: IConfigurationService, - @ITestProfileService testProfiles: ITestProfileService, - @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService menuService: IMenuService, - ) { - super(createRunTestDecoration(tests.map(t => t.test), tests.map(t => t.resultItem)), editor, testService, contextMenuService, commandService, configurationService, testProfiles, contextKeyService, menuService); + public get testIds() { + return this.tests.map(t => t.test.item.extId); } - public override merge(test: IncrementalTestCollectionItem, resultItem: TestResultItem | undefined): RunTestDecoration { - this.tests.push({ test, resultItem }); - this.editorDecoration = createRunTestDecoration(this.tests.map(t => t.test), this.tests.map(t => t.resultItem)); - return this; + public get displayedStates() { + return this.tests.map(t => t.resultItem?.computedState); } protected override getContextMenuActions() { @@ -664,27 +784,15 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio return { object: Separator.join(allActions, testSubmenus), dispose: () => disposable.dispose() }; } - - protected override defaultRun() { - return this.testService.runTests({ - tests: this.tests.map(({ test }) => test), - group: TestRunProfileBitset.Run, - }); - } - - protected override defaultDebug() { - return this.testService.runTests({ - tests: this.tests.map(({ test }) => test), - group: TestRunProfileBitset.Run, - }); - } } class RunSingleTestDecoration extends RunTestDecoration implements ITestDecoration { constructor( - private readonly test: IncrementalTestCollectionItem, - editor: ICodeEditor, - private readonly resultItem: TestResultItem | undefined, + test: IncrementalTestCollectionItem, + resultItem: TestResultItem | undefined, + model: ITextModel, + visible: boolean, + @ICodeEditorService codeEditorService: ICodeEditorService, @ITestService testService: ITestService, @ICommandService commandService: ICommandService, @IContextMenuService contextMenuService: IContextMenuService, @@ -693,70 +801,50 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, ) { - super(createRunTestDecoration([test], [resultItem]), editor, testService, contextMenuService, commandService, configurationService, testProfiles, contextKeyService, menuService); - } - - public override merge(test: IncrementalTestCollectionItem, resultItem: TestResultItem | undefined): RunTestDecoration { - return new MultiRunTestDecoration([ - { test: this.test, resultItem: this.resultItem }, - { test, resultItem }, - ], this.editor, this.testService, this.commandService, this.contextMenuService, this.configurationService, this.testProfileService, this.contextKeyService, this.menuService); + super([{ test, resultItem }], visible, model, codeEditorService, testService, contextMenuService, commandService, configurationService, testProfiles, contextKeyService, menuService); } protected override getContextMenuActions(e: IEditorMouseEvent) { - return this.getTestContextMenuActions(this.test, this.resultItem); - } - - protected override defaultRun() { - return this.testService.runTests({ - tests: [this.test], - group: TestRunProfileBitset.Run, - }); - } - - protected override defaultDebug() { - return this.testService.runTests({ - tests: [this.test], - group: TestRunProfileBitset.Debug, - }); + return this.getTestContextMenuActions(this.tests[0].test, this.tests[0].resultItem); } } class TestMessageDecoration implements ITestDecoration { + public static readonly inlineClassName = 'test-message-inline-content'; + public static readonly decorationId = `testmessage-${generateUuid()}`; + public id = ''; public readonly editorDecoration: IModelDeltaDecoration; - private readonly decorationId = `testmessage-${generateUuid()}`; + public readonly location: IRichLocation; + public readonly line: number; + + private readonly contentIdClass = `test-message-inline-content-id${generateUuid()}`; constructor( public readonly testMessage: ITestMessage, private readonly messageUri: URI | undefined, - public readonly location: IRichLocation, - private readonly editor: ICodeEditor, - @ICodeEditorService private readonly editorService: ICodeEditorService, - @IThemeService themeService: IThemeService, + textModel: ITextModel, + @ITestingPeekOpener private readonly peekOpener: ITestingPeekOpener, + @ICodeEditorService editorService: ICodeEditorService, ) { + this.location = testMessage.location!; + this.line = this.location.range.startLineNumber; const severity = testMessage.type; const message = typeof testMessage.message === 'string' ? removeAnsiEscapeCodes(testMessage.message) : testMessage.message; - const colorTheme = themeService.getColorTheme(); - editorService.registerDecorationType('test-message-decoration', this.decorationId, { - after: { - contentText: renderStringAsPlaintext(message), - color: `${colorTheme.getColor(testMessageSeverityColors[severity].decorationForeground)}`, - fontSize: `${editor.getOption(EditorOption.fontSize)}px`, - fontFamily: `var(${FONT_FAMILY_VAR})`, - padding: `0px 12px 0px 24px`, - }, - }, undefined, editor); - const options = editorService.resolveDecorationOptions(this.decorationId, true); + const options = editorService.resolveDecorationOptions(TestMessageDecoration.decorationId, true); options.hoverMessage = typeof message === 'string' ? new MarkdownString().appendText(message) : message; - options.afterContentClassName = `${options.afterContentClassName} testing-inline-message-content`; options.zIndex = 10; // todo: in spite of the z-index, this appears behind gitlens - options.className = `testing-inline-message-margin testing-inline-message-severity-${severity}`; + options.className = `testing-inline-message-severity-${severity}`; options.isWholeLine = true; options.stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; options.collapseOnReplaceEdit = true; + options.after = { + content: ' '.repeat(4) + renderStringAsPlaintext(message), + inlineClassName: `test-message-inline-content test-message-inline-content-s${severity} ${this.contentIdClass}` + }; + options.showIfCollapsed = true; const rulerColor = severity === TestMessageType.Error ? overviewRulerError @@ -766,7 +854,17 @@ class TestMessageDecoration implements ITestDecoration { options.overviewRuler = { color: themeColorFromId(rulerColor), position: OverviewRulerLane.Right }; } - this.editorDecoration = { range: firstLineRange(location.range), options }; + const lineLength = textModel.getLineLength(this.location.range.startLineNumber); + const column = lineLength ? (lineLength + 1) : this.location.range.endColumn; + this.editorDecoration = { + options, + range: { + startLineNumber: this.location.range.startLineNumber, + startColumn: column, + endColumn: column, + endLineNumber: this.location.range.startLineNumber, + } + }; } click(e: IEditorMouseEvent): boolean { @@ -778,16 +876,12 @@ class TestMessageDecoration implements ITestDecoration { return false; } - if (e.target.element?.className.includes(this.decorationId)) { - TestingOutputPeekController.get(this.editor).toggle(this.messageUri); + if (e.target.element?.className.includes(this.contentIdClass)) { + this.peekOpener.peekUri(this.messageUri); } return false; } - - dispose(): void { - this.editorService.removeDecorationType(this.decorationId); - } } registerThemingParticipant((theme, collector) => { @@ -795,4 +889,8 @@ registerThemingParticipant((theme, collector) => { if (codeLensForeground) { collector.addRule(`.testing-diff-lens-widget { color: ${codeLensForeground}; }`); } + + for (const [severity, { decorationForeground }] of Object.entries(testMessageSeverityColors)) { + collector.addRule(`.test-message-inline-content-s${severity} { color: ${theme.getColor(decorationForeground)} !important }`); + } }); diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 8f5d82644e..86b01a11a4 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -10,160 +10,21 @@ import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { Action, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; -import { Emitter, Event } from 'vs/base/common/event'; -import { splitGlobAware } from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; import { localize } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { TestTag } from 'vs/workbench/api/common/extHostTypeConverters'; import { attachSuggestEnabledInputBoxStyler, ContextScopedSuggestEnabledInputWithHistory, SuggestEnabledInputWithHistory, SuggestResultsProvider } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; +import { ITestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; -export interface ITestExplorerFilterState { - _serviceBrand: undefined; - readonly text: IObservableValue; - - /** Test ID the user wants to reveal in the explorer */ - readonly reveal: MutableObservableValue; - - readonly onDidRequestInputFocus: Event; - - /** - * Glob list to filter for based on the {@link text} - */ - readonly globList: readonly { include: boolean; text: string }[]; - - /** - * The user requested to filter for only the specified tags. - */ - readonly onlyTags: ReadonlySet; - - /** - * Focuses the filter input in the test explorer view. - */ - focusInput(): void; - - /** - * Replaces the filter {@link text}. - */ - setText(text: string): void; - - /** - * Sets whether the {@link text} is filtering for a special term. - */ - isFilteringFor(term: TestFilterTerm): boolean; - - /** - * Sets whether the {@link text} includes a special filter term. - */ - toggleFilteringFor(term: TestFilterTerm, shouldFilter?: boolean): void; -} - -export const ITestExplorerFilterState = createDecorator('testingFilterState'); - -const tagRe = /@([^ ,]*)/g; -const testTagRe = /^@(.+?):(.+)$/; -const trimExtraWhitespace = (str: string) => str.replace(/\s\s+/g, ' ').trim(); - -export class TestExplorerFilterState implements ITestExplorerFilterState { - declare _serviceBrand: undefined; - private readonly focusEmitter = new Emitter(); - /** - * Mapping of terms to whether they're included in the text. - */ - private termFilterState: { [K in TestFilterTerm]?: true } = {}; - - /** @inheritdoc */ - public globList: { include: boolean; text: string }[] = []; - - /** @inheritdoc */ - public onlyTags = new Set(); - - /** @inheritdoc */ - public readonly text = new MutableObservableValue(''); - - public readonly reveal = new MutableObservableValue(undefined); - - public readonly onDidRequestInputFocus = this.focusEmitter.event; - - /** @inheritdoc */ - public focusInput() { - this.focusEmitter.fire(); - } - - /** @inheritdoc */ - public setText(text: string) { - if (text === this.text.value) { - return; - } - - this.termFilterState = {}; - this.globList = []; - this.onlyTags.clear(); - - let globText = ''; - let lastIndex = 0; - for (const match of text.matchAll(tagRe)) { - globText += text.slice(lastIndex, match.index); - lastIndex = match.index! + match[0].length; - - const tag = match[0]; - if (allTestFilterTerms.includes(tag as TestFilterTerm)) { - this.termFilterState[tag as TestFilterTerm] = true; - } - - const tagMatch = testTagRe.exec(tag); - if (tagMatch) { - this.onlyTags.add(TestTag.namespace(tagMatch[1], tagMatch[2])); - } - } - - globText += text.slice(lastIndex).trim(); - - if (globText.length) { - for (const filter of splitGlobAware(globText, ',').map(s => s.trim()).filter(s => !!s.length)) { - if (filter.startsWith('!')) { - this.globList.push({ include: false, text: filter.slice(1).toLowerCase() }); - } else { - this.globList.push({ include: true, text: filter.toLowerCase() }); - } - } - } - - this.text.value = text; // purposely afterwards so everything is updated when the change event happen - } - - /** @inheritdoc */ - public isFilteringFor(term: TestFilterTerm) { - return !!this.termFilterState[term]; - } - - /** @inheritdoc */ - public toggleFilteringFor(term: TestFilterTerm, shouldFilter?: boolean) { - const text = this.text.value.trim(); - if (shouldFilter !== false && !this.termFilterState[term]) { - this.setText(text ? `${text} ${term}` : term); - } else if (shouldFilter !== true && this.termFilterState[term]) { - this.setText(trimExtraWhitespace(text.replace(term, ''))); - } - } -} - -export const enum TestFilterTerm { - Failed = '@failed', - Executed = '@executed', - CurrentDoc = '@doc', - Hidden = '@hidden', -} - const testFilterDescriptions: { [K in TestFilterTerm]: string } = { [TestFilterTerm.Failed]: localize('testing.filters.showOnlyFailed', "Show Only Failed Tests"), [TestFilterTerm.Executed]: localize('testing.filters.showOnlyExecuted', "Show Only Executed Tests"), @@ -171,8 +32,6 @@ const testFilterDescriptions: { [K in TestFilterTerm]: string } = { [TestFilterTerm.Hidden]: localize('testing.filters.showExcludedTests', "Show Hidden Tests"), }; -export const allTestFilterTerms = Object.keys(testFilterDescriptions) as readonly TestFilterTerm[]; - export class TestingExplorerFilter extends BaseActionViewItem { private input!: SuggestEnabledInputWithHistory; private wrapper!: HTMLDivElement; @@ -216,9 +75,11 @@ export class TestingExplorerFilter extends BaseActionViewItem { ...Object.entries(testFilterDescriptions).map(([label, detail]) => ({ label, detail })), ...Iterable.map(this.testService.collection.tags.values(), tag => { const { ctrlId, tagId } = TestTag.denamespace(tag.id); + const insertText = `@${ctrlId}:${tagId}`; return ({ label: `@${ctrlId}:${tagId}`, - detail: tag.label ? `${tag.ctrlLabel} › ${tag.label}` : tag.ctrlLabel, + detail: tag.ctrlLabel, + insertText: tagId.includes(' ') ? `@${ctrlId}:"${tagId.replace(/(["\\])/g, '\\$1')}"` : insertText, }); }), ].filter(r => !this.state.text.value.includes(r.label)), @@ -228,7 +89,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { value: this.state.text.value, placeholderText: localize('testExplorerFilter', "Filter (e.g. text, !exclude, @tag)"), }, - history: this.history.get([]), + history: this.history.get([]) })); this._register(attachSuggestEnabledInputBoxStyler(input, this.themeService)); diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index bb543968c2..1ea7d9fc4a 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -51,11 +51,12 @@ import { ByNameTestItemElement, HierarchicalByNameProjection } from 'vs/workbenc import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; -import { ITestExplorerFilterState, TestExplorerFilterState, TestFilterTerm, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; +import { TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { labelForTestInState, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestExplorerFilterState, TestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -222,18 +223,6 @@ export class TestingExplorerView extends ViewPane { } })); - const progress = new MutableDisposable(); - this._register(this.testProgressService.onCountChange(evt => { - if (!evt.isRunning && progress.value) { - progress.clear(); - } else if (evt.isRunning) { - if (!progress.value) { - progress.value = this.instantiationService.createInstance(UnmanagedProgress, { location: this.getProgressLocation(), total: 100 }); - } - progress.value.report({ increment: evt.runSoFar, total: evt.totalWillBeRun }); - } - })); - const listContainer = dom.append(this.container, dom.$('.test-explorer-tree')); this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility); this._register(this.viewModel.onChangeWelcomeVisibility(() => this._onDidChangeViewWelcomeState.fire())); @@ -868,11 +857,13 @@ class TestsFilter implements ITreeFilter { } private testTags(element: TestItemTreeElement): FilterResult { - if (!this.state.onlyTags.size) { + if (!this.state.includeTags.size && !this.state.excludeTags.size) { return FilterResult.Include; } - return element.test.item.tags.some(t => this.state.onlyTags.has(t)) + return (this.state.includeTags.size ? + element.test.item.tags.some(t => this.state.includeTags.has(t)) : + true) && element.test.item.tags.every(t => !this.state.excludeTags.has(t)) ? FilterResult.Include : FilterResult.Inherit; } @@ -898,8 +889,11 @@ class TestsFilter implements ITreeFilter { return FilterResult.Include; } - return hasNodeInOrParentOfUri(this.collection, this.documentUri, element.test.item.extId) - ? FilterResult.Include : FilterResult.Exclude; + if (hasNodeInOrParentOfUri(this.collection, this.documentUri, element.test.item.extId)) { + return FilterResult.Include; + } + + return FilterResult.Inherit; } private testFilterText(element: TestItemTreeElement) { @@ -935,6 +929,11 @@ class TreeSorter implements ITreeSorter { return (a instanceof TestTreeErrorMessage ? -1 : 0) + (b instanceof TestTreeErrorMessage ? 1 : 0); } + const durationDelta = (b.duration || 0) - (a.duration || 0); + if (this.viewModel.viewSorting === TestExplorerViewSorting.ByDuration && durationDelta !== 0) { + return durationDelta; + } + const stateDelta = cmpPriority(a.state, b.state); if (this.viewModel.viewSorting === TestExplorerViewSorting.ByStatus && stateDelta !== 0) { return stateDelta; diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 312d8edca2..8322b3c66e 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -27,7 +27,7 @@ import { clamp } from 'vs/base/common/numbers'; import { count } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; -import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IDiffEditorConstructionOptions, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -37,18 +37,19 @@ import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { getOuterEditor, IPeekViewService, peekViewResultsBackground, peekViewResultsMatchForeground, peekViewResultsSelectionBackground, peekViewResultsSelectionForeground, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView'; +import { getOuterEditor, IPeekViewService, peekViewResultsBackground, peekViewResultsMatchForeground, peekViewResultsSelectionBackground, peekViewResultsSelectionForeground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView'; import { localize } from 'vs/nls'; import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyAndExpr, ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; @@ -57,17 +58,19 @@ import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; -import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; -import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme'; +import { testingPeekBorder, testingPeekHeaderBackground } from 'vs/workbench/contrib/testing/browser/theme'; import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; +import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; -import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; +import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; +import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult, maxCountPriority, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; @@ -177,6 +180,30 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener return true; } + /** @inheritdoc */ + public peekUri(uri: URI, options?: Partial) { + const parsed = parseTestUri(uri); + const result = parsed && this.testResults.getResult(parsed.resultId); + if (!parsed || !result) { + return false; + } + + const message = result.getStateById(parsed.testExtId)?.tasks[parsed.taskIndex].messages[parsed.messageIndex]; + if (!message?.location) { + return false; + } + + this.showPeekFromUri({ + type: TestUriType.ResultMessage, + documentUri: message.location.uri, + taskIndex: parsed.taskIndex, + messageIndex: parsed.messageIndex, + resultId: result.id, + testExtId: parsed.testExtId, + }, { selection: message.location.range, ...options }); + return true; + } + /** @inheritdoc */ public closeAllPeeks() { for (const editor of this.codeEditorService.listCodeEditors()) { @@ -184,6 +211,7 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener } } + /** @inheritdoc */ private async showPeekFromUri(uri: TestUriWithDocument, options?: ITextEditorOptions) { const pane = await this.editorService.openEditor({ resource: uri.documentUri, @@ -375,12 +403,22 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo return this.peek.value; } + /** + * Whether the history part of the peek view should be visible. + */ + public readonly historyVisible = MutableObservableValue.stored(new StoredValue({ + key: 'testHistoryVisibleInPeek', + scope: StorageScope.GLOBAL, + target: StorageTarget.USER, + }, this.storageService), true); + constructor( private readonly editor: ICodeEditor, @IEditorService private readonly editorService: IEditorService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITestResultService private readonly testResults: ITestResultService, + @IStorageService private readonly storageService: IStorageService, @IContextKeyService contextKeyService: IContextKeyService, ) { super(); @@ -431,7 +469,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo const message = dto.messages[dto.messageIndex]; if (!this.peek.value) { - this.peek.value = this.instantiationService.createInstance(TestingOutputPeek, this.editor); + this.peek.value = this.instantiationService.createInstance(TestingOutputPeek, this.editor, this.historyVisible); this.peek.value.onDidClose(() => { this.visible.set(false); this.currentPeekUri = undefined; @@ -589,6 +627,7 @@ class TestingOutputPeek extends PeekViewWidget { constructor( editor: ICodeEditor, + private readonly historyVisible: IObservableValue, @IThemeService themeService: IThemeService, @IPeekViewService peekViewService: IPeekViewService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -596,7 +635,7 @@ class TestingOutputPeek extends PeekViewWidget { @IInstantiationService instantiationService: IInstantiationService, @ITextModelService protected readonly modelService: ITextModelService, ) { - super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService); + super(editor, { showFrame: true, frameWidth: 1, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService); TestingContextKeys.isInPeek.bindTo(contextKeyService); this._disposables.add(themeService.onDidColorThemeChange(this.applyTheme, this)); @@ -607,10 +646,11 @@ class TestingOutputPeek extends PeekViewWidget { private applyTheme(theme: IColorTheme) { const borderColor = theme.getColor(testingPeekBorder) || Color.transparent; + const headerBg = theme.getColor(testingPeekHeaderBackground) || Color.transparent; this.style({ arrowColor: borderColor, frameColor: borderColor, - headerBackgroundColor: theme.getColor(peekViewTitleBackground) || Color.transparent, + headerBackgroundColor: headerBg, primaryHeadingColor: theme.getColor(peekViewTitleForeground), secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground) }); @@ -670,6 +710,12 @@ class TestingOutputPeek extends PeekViewWidget { } }, }, Sizing.Distribute); + + const historyViewIndex = 1; + this.splitView.setViewVisible(historyViewIndex, this.historyVisible.value); + this._disposables.add(this.historyVisible.onDidChange(visible => { + this.splitView.setViewVisible(historyViewIndex, visible); + })); } /** @@ -740,6 +786,7 @@ interface IPeekOutputRenderer extends IDisposable { const commonEditorOptions: IEditorOptions = { scrollBeyondLastLine: false, + links: true, scrollbar: { verticalScrollbarSize: 14, horizontal: 'auto', @@ -753,9 +800,10 @@ const commonEditorOptions: IEditorOptions = { minimap: { enabled: false }, + wordWrap: 'on', }; -const diffEditorOptions: IDiffEditorOptions = { +const diffEditorOptions: IDiffEditorConstructionOptions = { ...commonEditorOptions, enableSplitViewResizing: true, isInEmbeddedEditor: true, @@ -1460,7 +1508,7 @@ class TreeActionsProvider { localize('testing.revealInExplorer', "Reveal in Test Explorer"), Codicon.listTree.classNames, undefined, - () => this.commandService.executeCommand('vscode.revealTestInExplorer', extId), + () => this.commandService.executeCommand('_revealTestInExplorer', extId), )); if (capabilities & TestRunProfileBitset.Run) { @@ -1525,10 +1573,10 @@ registerThemingParticipant((theme, collector) => { } }); -const navWhen = ContextKeyAndExpr.create([ +const navWhen = ContextKeyExpr.and( EditorContextKeys.focus, TestingContextKeys.isPeekVisible, -]); +); /** * Gets the editor where the peek may be shown, bubbling upwards if the given @@ -1558,7 +1606,7 @@ export class GoToNextMessageAction extends EditorAction2 { id: GoToNextMessageAction.ID, f1: true, title: localize('testing.goToNextMessage', "Go to Next Test Failure"), - icon: Codicon.chevronDown, + icon: Codicon.arrowDown, category: CATEGORIES.Test, keybinding: { primary: KeyMod.Alt | KeyCode.F8, @@ -1588,7 +1636,7 @@ export class GoToPreviousMessageAction extends EditorAction2 { id: GoToPreviousMessageAction.ID, f1: true, title: localize('testing.goToPreviousMessage', "Go to Previous Test Failure"), - icon: Codicon.chevronUp, + icon: Codicon.arrowUp, category: CATEGORIES.Test, keybinding: { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F8, @@ -1628,3 +1676,31 @@ export class OpenMessageInEditorAction extends EditorAction2 { TestingOutputPeekController.get(getPeekedEditor(accessor, editor)).openCurrentInEditor(); } } + +export class ToggleTestingPeekHistory extends EditorAction2 { + public static readonly ID = 'testing.toggleTestingPeekHistory'; + constructor() { + super({ + id: ToggleTestingPeekHistory.ID, + f1: true, + title: localize('testing.toggleTestingPeekHistory', "Toggle Test History in Peek"), + icon: Codicon.history, + category: CATEGORIES.Test, + menu: [{ + id: MenuId.TestPeekTitle, + group: 'navigation', + order: 3, + }], + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyH, + when: TestingContextKeys.isPeekVisible.isEqualTo(true), + }, + }); + } + + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor) { + const ctrl = TestingOutputPeekController.get(getPeekedEditor(accessor, editor)); + ctrl.historyVisible.value = !ctrl.historyVisible.value; + } +} diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts b/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts index 79bab001bd..2d1bddfd15 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { DeferredPromise } from 'vs/base/common/async'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { listenStream } from 'vs/base/common/stream'; import { isDefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessPropertyMap, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, ProcessCapability, ProcessPropertyType, TerminalLocation, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { IViewsService } from 'vs/workbench/common/views'; -import { ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; @@ -51,6 +51,7 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi constructor( @ITerminalService private readonly terminalService: ITerminalService, @ITerminalGroupService private readonly terminalGroupService: ITerminalGroupService, + @ITerminalEditorService private readonly terminalEditorService: ITerminalEditorService, @ITestResultService resultService: ITestResultService, @IViewsService private viewsService: IViewsService, ) { @@ -89,7 +90,11 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi const existing = testOutputPtys.find(([, o]) => o.resultId === result?.id); if (existing) { this.terminalService.setActiveInstance(existing[0]); - this.terminalGroupService.showPanel(); + if (existing[0].target === TerminalLocation.Editor) { + this.terminalEditorService.revealActiveEditor(); + } else { + this.terminalGroupService.showPanel(); + } return; } @@ -98,6 +103,7 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi if (ended) { ended[1].clear(); this.showResultsInTerminal(ended[0], ended[1], result); + return; } const output = new TestOutputProcess(); @@ -115,7 +121,11 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi this.outputTerminals.set(terminal, output); output.resetFor(result?.id, getTitle(result)); this.terminalService.setActiveInstance(terminal); - this.terminalGroupService.showPanel(); + if (terminal.target === TerminalLocation.Editor) { + this.terminalEditorService.revealActiveEditor(); + } else { + this.terminalGroupService.showPanel(); + } if (!result) { // seems like it takes a tick for listeners to be registered @@ -147,10 +157,15 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi } class TestOutputProcess extends Disposable implements ITerminalChildProcess { + onProcessOverrideDimensions?: Event | undefined; + onProcessResolvedShellLaunchConfig?: Event | undefined; + onDidChangeHasChildProcesses?: Event | undefined; + onDidChangeProperty = Event.None; private processDataEmitter = this._register(new Emitter()); private titleEmitter = this._register(new Emitter()); private readonly startedDeferred = new DeferredPromise(); - + private _capabilities: ProcessCapability[] = []; + get capabilities(): ProcessCapability[] { return this._capabilities; } /** Whether the associated test has ended (indicating the terminal can be reused) */ public ended = true; /** Result currently being displayed */ @@ -178,14 +193,14 @@ class TestOutputProcess extends Disposable implements ITerminalChildProcess { public readonly onProcessData = this.processDataEmitter.event; public readonly onProcessExit = this._register(new Emitter()).event; - private readonly _onProcessReady = this._register(new Emitter<{ pid: number; cwd: string; }>()); + private readonly _onProcessReady = this._register(new Emitter<{ pid: number; cwd: string; capabilities: ProcessCapability[] }>()); public readonly onProcessReady = this._onProcessReady.event; public readonly onProcessTitleChanged = this.titleEmitter.event; public readonly onProcessShellTypeChanged = this._register(new Emitter()).event; public start(): Promise { this.startedDeferred.complete(); - this._onProcessReady.fire({ pid: -1, cwd: '' }); + this._onProcessReady.fire({ pid: -1, cwd: '', capabilities: [] }); return Promise.resolve(undefined); } public shutdown(): void { @@ -219,5 +234,13 @@ class TestOutputProcess extends Disposable implements ITerminalChildProcess { public getLatency(): Promise { return Promise.resolve(0); } + + public refreshProperty(property: ProcessPropertyType): Promise { + throw new Error(`refreshProperty is not suppported in TestOutputProcesses. property: ${property}`); + } + + public updateProperty(property: ProcessPropertyType, value: any): Promise { + throw new Error(`updateProperty is not suppported in TestOutputProcesses. property: ${property}, value: ${value}`); + } //#endregion } diff --git a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts index 7d83d04545..449fbca50c 100644 --- a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts +++ b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts @@ -10,6 +10,7 @@ import { localize } from 'vs/nls'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ProgressLocation, UnmanagedProgress } from 'vs/platform/progress/common/progress'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; +import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestStateCount } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; @@ -17,27 +18,21 @@ export interface ITestingProgressUiService { readonly _serviceBrand: undefined; readonly onCountChange: Event; readonly onTextChange: Event; + + update(): void; } export const ITestingProgressUiService = createDecorator('testingProgressUiService'); -export class TestingProgressUiService extends Disposable implements ITestingProgressUiService { - declare _serviceBrand: undefined; - - private readonly current = this._register(new MutableDisposable()); - private readonly updateCountsEmitter = new Emitter(); - private readonly updateTextEmitter = new Emitter(); - - public readonly onCountChange = this.updateCountsEmitter.event; - public readonly onTextChange = this.updateTextEmitter.event; - +/** Workbench contribution that triggers updates in the TestingProgressUi service */ +export class TestingProgressTrigger extends Disposable { constructor( - @ITestResultService private readonly resultService: ITestResultService, - @IInstantiationService private readonly instantiaionService: IInstantiationService, + @ITestResultService resultService: ITestResultService, + @ITestingProgressUiService progressService: ITestingProgressUiService, ) { super(); - const scheduler = this._register(new RunOnceScheduler(() => this.updateProgress(), 200)); + const scheduler = this._register(new RunOnceScheduler(() => progressService.update(), 200)); this._register(resultService.onResultsChanged(() => { if (!scheduler.isScheduled()) { @@ -51,8 +46,28 @@ export class TestingProgressUiService extends Disposable implements ITestingProg } })); } +} - private updateProgress() { +export class TestingProgressUiService extends Disposable implements ITestingProgressUiService { + declare _serviceBrand: undefined; + + private readonly windowProg = this._register(new MutableDisposable()); + private readonly testViewProg = this._register(new MutableDisposable()); + private readonly updateCountsEmitter = new Emitter(); + private readonly updateTextEmitter = new Emitter(); + + public readonly onCountChange = this.updateCountsEmitter.event; + public readonly onTextChange = this.updateTextEmitter.event; + + constructor( + @ITestResultService private readonly resultService: ITestResultService, + @IInstantiationService private readonly instantiaionService: IInstantiationService, + ) { + super(); + } + + /** @inheritdoc */ + public update() { const allResults = this.resultService.results; const running = allResults.filter(r => r.completedAt === undefined); if (!running.length) { @@ -65,12 +80,19 @@ export class TestingProgressUiService extends Disposable implements ITestingProg this.updateCountsEmitter.fire(collectTestStateCounts(false)); } - this.current.clear(); + this.windowProg.clear(); + this.testViewProg.clear(); return; } - if (!this.current.value) { - this.current.value = this.instantiaionService.createInstance(UnmanagedProgress, { location: ProgressLocation.Window }); + if (!this.windowProg.value) { + this.windowProg.value = this.instantiaionService.createInstance(UnmanagedProgress, { + location: ProgressLocation.Window, + }); + this.testViewProg.value = this.instantiaionService.createInstance(UnmanagedProgress, { + location: Testing.ViewletId, + total: 100, + }); } const collected = collectTestStateCounts(true, ...running.map(r => r.counts)); @@ -78,7 +100,8 @@ export class TestingProgressUiService extends Disposable implements ITestingProg const message = getTestProgressText(true, collected); this.updateTextEmitter.fire(message); - this.current.value.report({ message }); + this.windowProg.value.report({ message }); + this.testViewProg.value!.report({ increment: collected.runSoFar, total: collected.totalWillBeRun }); } } diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index 7378cff4ea..edd442a154 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -5,7 +5,7 @@ import { Color, RGBA } from 'vs/base/common/color'; import { localize } from 'vs/nls'; -import { editorErrorForeground, editorForeground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorErrorForeground, editorForeground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; @@ -13,19 +13,19 @@ import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/c export const testingColorIconFailed = registerColor('testing.iconFailed', { dark: '#f14c4c', light: '#f14c4c', - hc: '#000000' + hc: '#f14c4c' }, localize('testing.iconFailed', "Color for the 'failed' icon in the test explorer.")); export const testingColorIconErrored = registerColor('testing.iconErrored', { dark: '#f14c4c', light: '#f14c4c', - hc: '#000000' + hc: '#f14c4c' }, localize('testing.iconErrored', "Color for the 'Errored' icon in the test explorer.")); export const testingColorIconPassed = registerColor('testing.iconPassed', { dark: '#73c991', light: '#73c991', - hc: '#000000' + hc: '#73c991' }, localize('testing.iconPassed', "Color for the 'passed' icon in the test explorer.")); export const testingColorRunAction = registerColor('testing.runAction', { @@ -37,7 +37,7 @@ export const testingColorRunAction = registerColor('testing.runAction', { export const testingColorIconQueued = registerColor('testing.iconQueued', { dark: '#cca700', light: '#cca700', - hc: '#000000' + hc: '#cca700' }, localize('testing.iconQueued', "Color for the 'Queued' icon in the test explorer.")); export const testingColorIconUnset = registerColor('testing.iconUnset', { @@ -55,7 +55,13 @@ export const testingColorIconSkipped = registerColor('testing.iconSkipped', { export const testingPeekBorder = registerColor('testing.peekBorder', { dark: editorErrorForeground, light: editorErrorForeground, - hc: editorErrorForeground, + hc: contrastBorder, +}, localize('testing.peekBorder', 'Color of the peek view borders and arrow.')); + +export const testingPeekHeaderBackground = registerColor('testing.peekHeaderBackground', { + dark: transparent(editorErrorForeground, 0.1), + light: transparent(editorErrorForeground, 0.1), + hc: null, }, localize('testing.peekBorder', 'Color of the peek view borders and arrow.')); export const testMessageSeverityColors: { diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 11603936a0..a2dc741743 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -23,6 +23,7 @@ export const enum TestExplorerViewMode { export const enum TestExplorerViewSorting { ByLocation = 'location', ByStatus = 'status', + ByDuration = 'duration', } export const enum TestExplorerStateFilter { diff --git a/src/vs/workbench/contrib/testing/common/getComputedState.ts b/src/vs/workbench/contrib/testing/common/getComputedState.ts index 0b4a044dd9..d8e17338fb 100644 --- a/src/vs/workbench/contrib/testing/common/getComputedState.ts +++ b/src/vs/workbench/contrib/testing/common/getComputedState.ts @@ -36,8 +36,12 @@ export const getComputedState = (accessor: IComputedStateAccessor, node: T let computed = accessor.getCurrentComputedState(node); if (computed === undefined || force) { computed = accessor.getOwnState(node) ?? TestResultState.Unset; + for (const child of accessor.getChildren(node)) { - computed = maxPriority(computed, getComputedState(accessor, child)); + const childComputed = getComputedState(accessor, child); + // If all children are skipped, make the current state skipped too if unset (#131537) + computed = childComputed === TestResultState.Skipped && computed === TestResultState.Unset + ? TestResultState.Skipped : maxPriority(computed, childComputed); } accessor.setComputedState(node, computed); diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index cdfc9328fe..8ce6dce921 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -281,11 +281,10 @@ export class SingleUseTestCollection extends Disposable { if (existing) { existing.refCount++; } else { - this.tags.set(tag.id, { label: tag.label, refCount: 1 }); + this.tags.set(tag.id, { refCount: 1 }); this.pushDiff([TestDiffOpType.AddTag, { id: Convert.TestTag.namespace(this.controllerId, tag.id), ctrlLabel: this.root.label, - label: tag.label, }]); } } diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index e17477e818..7cd069bd92 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -8,6 +8,7 @@ import { MarshalledId } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; +import { ILocationDto } from 'vs/workbench/api/common/extHost.protocol'; export const enum TestResultState { Unset = 0, @@ -109,6 +110,8 @@ export interface ITestErrorMessage { location: IRichLocation | undefined; } +export type SerializedTestErrorMessage = Omit & { location?: ILocationDto }; + export interface ITestOutputMessage { message: string; type: TestMessageType.Info; @@ -116,6 +119,10 @@ export interface ITestOutputMessage { location: IRichLocation | undefined; } +export type SerializedTestOutputMessage = Omit & { location?: ILocationDto }; + +export type SerializedTestMessage = SerializedTestErrorMessage | SerializedTestOutputMessage; + export type ITestMessage = ITestErrorMessage | ITestOutputMessage; export interface ITestTaskState { @@ -132,13 +139,11 @@ export interface ITestRunTask { export interface ITestTag { id: string; - label?: string; } export interface ITestTagDisplayInfo { id: string; ctrlLabel: string; - label?: string; } /** diff --git a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts new file mode 100644 index 0000000000..c96a3309dd --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { splitGlobAware } from 'vs/base/common/glob'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TestTag } from 'vs/workbench/api/common/extHostTypeConverters'; +import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; + +export interface ITestExplorerFilterState { + _serviceBrand: undefined; + + /** Current filter text */ + readonly text: IObservableValue; + + /** Test ID the user wants to reveal in the explorer */ + readonly reveal: MutableObservableValue; + + /** Event that fires when {@link focusInput} is invoked. */ + readonly onDidRequestInputFocus: Event; + + /** + * Glob list to filter for based on the {@link text} + */ + readonly globList: readonly { include: boolean; text: string }[]; + + /** + * The user requested to filter including tags. + */ + readonly includeTags: ReadonlySet; + + /** + * The user requested to filter excluding tags. + */ + readonly excludeTags: ReadonlySet; + + /** + * Focuses the filter input in the test explorer view. + */ + focusInput(): void; + + /** + * Replaces the filter {@link text}. + */ + setText(text: string): void; + + /** + * Sets whether the {@link text} is filtering for a special term. + */ + isFilteringFor(term: TestFilterTerm): boolean; + + /** + * Sets whether the {@link text} includes a special filter term. + */ + toggleFilteringFor(term: TestFilterTerm, shouldFilter?: boolean): void; +} + +export const ITestExplorerFilterState = createDecorator('testingFilterState'); + +const tagRe = /!?@([^ ,:]+)/g; +const trimExtraWhitespace = (str: string) => str.replace(/\s\s+/g, ' ').trim(); + +export class TestExplorerFilterState implements ITestExplorerFilterState { + declare _serviceBrand: undefined; + private readonly focusEmitter = new Emitter(); + /** + * Mapping of terms to whether they're included in the text. + */ + private termFilterState: { [K in TestFilterTerm]?: true } = {}; + + /** @inheritdoc */ + public globList: { include: boolean; text: string }[] = []; + + /** @inheritdoc */ + public includeTags = new Set(); + + /** @inheritdoc */ + public excludeTags = new Set(); + + /** @inheritdoc */ + public readonly text = new MutableObservableValue(''); + + public readonly reveal = new MutableObservableValue(undefined); + + public readonly onDidRequestInputFocus = this.focusEmitter.event; + + /** @inheritdoc */ + public focusInput() { + this.focusEmitter.fire(); + } + + /** @inheritdoc */ + public setText(text: string) { + if (text === this.text.value) { + return; + } + + this.termFilterState = {}; + this.globList = []; + this.includeTags.clear(); + this.excludeTags.clear(); + + let globText = ''; + let lastIndex = 0; + for (const match of text.matchAll(tagRe)) { + let nextIndex = match.index! + match[0].length; + + const tag = match[0]; + if (allTestFilterTerms.includes(tag as TestFilterTerm)) { + this.termFilterState[tag as TestFilterTerm] = true; + } + + // recognize and parse @ctrlId:tagId or quoted like @ctrlId:"tag \\"id" + if (text[nextIndex] === ':') { + nextIndex++; + + let delimiter = text[nextIndex]; + if (delimiter !== `"` && delimiter !== `'`) { + delimiter = ' '; + } else { + nextIndex++; + } + + let tagId = ''; + while (nextIndex < text.length && text[nextIndex] !== delimiter) { + if (text[nextIndex] === '\\') { + tagId += text[nextIndex + 1]; + nextIndex += 2; + } else { + tagId += text[nextIndex]; + nextIndex++; + } + } + + if (match[0].startsWith('!')) { + this.excludeTags.add(TestTag.namespace(match[1], tagId)); + } else { + this.includeTags.add(TestTag.namespace(match[1], tagId)); + } + nextIndex++; + } + + globText += text.slice(lastIndex, match.index); + lastIndex = nextIndex; + } + + globText += text.slice(lastIndex).trim(); + + if (globText.length) { + for (const filter of splitGlobAware(globText, ',').map(s => s.trim()).filter(s => !!s.length)) { + if (filter.startsWith('!')) { + this.globList.push({ include: false, text: filter.slice(1).toLowerCase() }); + } else { + this.globList.push({ include: true, text: filter.toLowerCase() }); + } + } + } + + this.text.value = text; // purposely afterwards so everything is updated when the change event happen + } + + /** @inheritdoc */ + public isFilteringFor(term: TestFilterTerm) { + return !!this.termFilterState[term]; + } + + /** @inheritdoc */ + public toggleFilteringFor(term: TestFilterTerm, shouldFilter?: boolean) { + const text = this.text.value.trim(); + if (shouldFilter !== false && !this.termFilterState[term]) { + this.setText(text ? `${text} ${term}` : term); + } else if (shouldFilter !== true && this.termFilterState[term]) { + this.setText(trimExtraWhitespace(text.replace(term, ''))); + } + } +} + +export const enum TestFilterTerm { + Failed = '@failed', + Executed = '@executed', + CurrentDoc = '@doc', + Hidden = '@hidden', +} + +export const allTestFilterTerms: readonly TestFilterTerm[] = [ + TestFilterTerm.Failed, + TestFilterTerm.Executed, + TestFilterTerm.CurrentDoc, + TestFilterTerm.Hidden, +]; diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 93b6e29950..0d64a0d931 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -7,6 +7,7 @@ import { findFirstInSorted } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; +import { Iterable } from 'vs/base/common/iterator'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -22,6 +23,14 @@ export type ResultChangeEvent = | { inserted: ITestResult } | { removed: ITestResult[] }; +export const allChangedResults = (evt: ResultChangeEvent): Iterable => 'completed' in evt + ? Iterable.single(evt.completed) + : 'started' in evt + ? Iterable.single(evt.started) + : 'inserted' in evt + ? Iterable.single(evt.inserted) + : evt.removed; + export interface ITestResultService { readonly _serviceBrand: undefined; /** diff --git a/src/vs/workbench/contrib/testing/common/testingDecorations.ts b/src/vs/workbench/contrib/testing/common/testingDecorations.ts new file mode 100644 index 0000000000..1a90b12d76 --- /dev/null +++ b/src/vs/workbench/contrib/testing/common/testingDecorations.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { binarySearch } from 'vs/base/common/arrays'; +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; + +export interface ITestingDecorationsService { + _serviceBrand: undefined; + + /** + * Fires when something happened to change decorations in an editor. + * Interested consumers should call {@link syncDecorations} to update them. + */ + onDidChange: Event; + + /** + * Signals the code underlying a test message has changed, and it should + * no longer be decorated in the source. + */ + invalidateResultMessage(message: ITestMessage): void; + + /** + * Ensures decorations in the given document URI are up to date, + * and returns them. + */ + syncDecorations(resource: URI): TestDecorations; + + /** + * Gets the range where a test ID is displayed, in the given URI. + * Returns undefined if there's no such decoration. + */ + getDecoratedRangeForTest(resource: URI, testId: string): Range | undefined; +} + +export interface ITestDecoration { + /** + * ID of the decoration after being added to the editor, set after the + * decoration is applied. + */ + readonly id: string; + + /** + * Original decoration line number. + */ + readonly line: number; + + /** + * Editor decoration instance. + */ + readonly editorDecoration: IModelDeltaDecoration; +} + +export class TestDecorations { + public value: T[] = []; + + private _idMap?: Map; + + /** + * Looks up a decoration by ID. + */ + public get(decorationId: string) { + if (this._idMap) { + return this._idMap.get(decorationId); + } else if (this.value.length > 16) { + this._idMap = new Map(); + for (const value of this.value) { this._idMap.set(value.id, value); } + return this._idMap.get(decorationId); + } else { + return this.value.find(v => v.id === decorationId); + } + } + + /** + * Adds a new value to the decorations. + */ + public push(value: T) { + const searchIndex = binarySearch(this.value, value, (a, b) => a.line - b.line); + this.value.splice(searchIndex < 0 ? ~searchIndex : searchIndex, 0, value); + this._idMap = undefined; + } + + /** + * Finds the value that exists on the given line, if any. + */ + public findOnLine(line: number, predicate: (value: T) => boolean): T | undefined { + const lineStart = binarySearch<{ line: number }>(this.value, { line }, (a, b) => a.line - b.line); + if (lineStart < 0) { + return undefined; + } + + for (let i = lineStart; i < this.value.length && this.value[i].line === line; i++) { + if (predicate(this.value[i])) { + return this.value[i]; + } + } + + return undefined; + } + + /** + * Gets decorations on each line. + */ + public *lines(): Iterable<[number, T[]]> { + if (!this.value.length) { + return; + } + + let startIndex = 0; + let startLine = this.value[0].line; + for (let i = 1; i < this.value.length; i++) { + const v = this.value[i]; + if (v.line !== startLine) { + yield [startLine, this.value.slice(startIndex, i)]; + startLine = v.line; + startIndex = i; + } + } + + yield [startLine, this.value.slice(startIndex)]; + } +} + +export const ITestingDecorationsService = createDecorator('testingDecorationService'); + diff --git a/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts index 28b39d1159..67961204d2 100644 --- a/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts +++ b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; @@ -17,6 +18,12 @@ export interface ITestingPeekOpener { */ tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial): boolean; + /** + * Peeks at the given test message uri. + * @returns a boolean indicating whether a peek was opened + */ + peekUri(uri: URI, options?: Partial): boolean; + /** * Opens the peek. Shows any available message. */ diff --git a/src/vs/workbench/contrib/testing/common/testingStates.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts index 0afd6ccee9..3eb2d080b3 100644 --- a/src/vs/workbench/contrib/testing/common/testingStates.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -16,8 +16,8 @@ export const statePriority: { [K in TestResultState]: number } = { [TestResultState.Running]: 6, [TestResultState.Errored]: 5, [TestResultState.Failed]: 4, - [TestResultState.Queued]: 3, - [TestResultState.Passed]: 2, + [TestResultState.Passed]: 3, + [TestResultState.Queued]: 2, [TestResultState.Unset]: 1, [TestResultState.Skipped]: 0, }; diff --git a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts new file mode 100644 index 0000000000..cbdcc88d4e --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; + + +suite('TestExplorerFilterState', () => { + let t: TestExplorerFilterState; + setup(() => { + t = new TestExplorerFilterState(); + }); + + const assertFilteringFor = (expected: { [T in TestFilterTerm]?: boolean }) => { + for (const [term, expectation] of Object.entries(expected)) { + assert.strictEqual(t.isFilteringFor(term as TestFilterTerm), expectation, `expected filtering for ${term} === ${expectation}`); + } + }; + + const termFiltersOff = { + [TestFilterTerm.Failed]: false, + [TestFilterTerm.Executed]: false, + [TestFilterTerm.CurrentDoc]: false, + [TestFilterTerm.Hidden]: false, + }; + + test('filters simple globs', () => { + t.setText('hello, !world'); + assert.deepStrictEqual(t.globList, [{ text: 'hello', include: true }, { text: 'world', include: false }]); + assert.deepStrictEqual(t.includeTags, new Set()); + assert.deepStrictEqual(t.excludeTags, new Set()); + assertFilteringFor(termFiltersOff); + }); + + test('filters to patterns', () => { + t.setText('@doc'); + assert.deepStrictEqual(t.globList, []); + assert.deepStrictEqual(t.includeTags, new Set()); + assert.deepStrictEqual(t.excludeTags, new Set()); + assertFilteringFor({ + ...termFiltersOff, + [TestFilterTerm.CurrentDoc]: true, + }); + }); + + test('filters to tags', () => { + t.setText('@hello:world !@foo:bar'); + assert.deepStrictEqual(t.globList, []); + assert.deepStrictEqual(t.includeTags, new Set(['hello\0world'])); + assert.deepStrictEqual(t.excludeTags, new Set(['foo\0bar'])); + assertFilteringFor(termFiltersOff); + }); + + test('filters to mixed terms and tags', () => { + t.setText('@hello:world foo, !bar @doc !@foo:bar'); + assert.deepStrictEqual(t.globList, [{ text: 'foo', include: true }, { text: 'bar', include: false }]); + assert.deepStrictEqual(t.includeTags, new Set(['hello\0world'])); + assert.deepStrictEqual(t.excludeTags, new Set(['foo\0bar'])); + assertFilteringFor({ + ...termFiltersOff, + [TestFilterTerm.CurrentDoc]: true, + }); + }); + + test('parses quotes', () => { + t.setText('@hello:"world" @foo:\'bar\' baz'); + assert.deepStrictEqual(t.globList, [{ text: 'baz', include: true }]); + assert.deepStrictEqual([...t.includeTags], ['hello\0world', 'foo\0bar']); + assert.deepStrictEqual(t.excludeTags, new Set()); + }); + + test('parses quotes with escapes', () => { + t.setText('@hello:"world\\"1" foo'); + assert.deepStrictEqual(t.globList, [{ text: 'foo', include: true }]); + assert.deepStrictEqual([...t.includeTags], ['hello\0world"1']); + assert.deepStrictEqual(t.excludeTags, new Set()); + }); +}); diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index f8ee3b8faa..ac94c96f4c 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -12,7 +12,6 @@ import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/c import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IColorRegistry, Extensions as ColorRegistryExtensions } from 'vs/platform/theme/common/colorRegistry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Color } from 'vs/base/common/color'; @@ -21,6 +20,8 @@ import { colorThemeSchemaId } from 'vs/workbench/services/themes/common/colorThe import { onUnexpectedError } from 'vs/base/common/errors'; import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; export class SelectColorThemeAction extends Action { @@ -33,7 +34,7 @@ export class SelectColorThemeAction extends Action { @IQuickInputService private readonly quickInputService: IQuickInputService, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, // @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, {{SQL CARBON EDIT}} no unused - @IViewletService private readonly viewletService: IViewletService + @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService ) { super(id, label); } @@ -59,7 +60,6 @@ export class SelectColorThemeAction extends Action { selectThemeTimeout = window.setTimeout(() => { selectThemeTimeout = undefined; const themeId = theme && theme.id !== undefined ? theme.id : currentTheme.id; - console.log(`setColorTheme apply: ` + applyTheme); this.themeService.setColorTheme(themeId, applyTheme ? 'auto' : 'preview').then(undefined, err => { onUnexpectedError(err); @@ -81,7 +81,7 @@ export class SelectColorThemeAction extends Action { quickpick.onDidAccept(_ => { const theme = quickpick.activeItems[0]; if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry - openExtensionViewlet(this.viewletService, `category:themes ${quickpick.value}`); + openExtensionViewlet(this.paneCompositeService, `category:themes ${quickpick.value}`); } else { selectTheme(theme, true); } @@ -108,7 +108,7 @@ abstract class AbstractIconThemeAction extends Action { label: string, private readonly quickInputService: IQuickInputService, private readonly extensionGalleryService: IExtensionGalleryService, - private readonly viewletService: IViewletService + private readonly paneCompositeService: IPaneCompositePartService ) { super(id, label); @@ -158,7 +158,7 @@ abstract class AbstractIconThemeAction extends Action { quickpick.onDidAccept(_ => { const theme = quickpick.activeItems[0]; if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry - openExtensionViewlet(this.viewletService, `${this.marketplaceTag} ${quickpick.value}`); + openExtensionViewlet(this.paneCompositeService, `${this.marketplaceTag} ${quickpick.value}`); } else { selectTheme(theme, true); } @@ -189,10 +189,10 @@ class SelectFileIconThemeAction extends AbstractIconThemeAction { @IQuickInputService quickInputService: IQuickInputService, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, - @IViewletService viewletService: IViewletService + @IPaneCompositePartService paneCompositeService: IPaneCompositePartService ) { - super(id, label, quickInputService, extensionGalleryService, viewletService); + super(id, label, quickInputService, extensionGalleryService, paneCompositeService); } protected builtInEntry: QuickPickInput = { id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable File Icons') }; @@ -220,10 +220,10 @@ class SelectProductIconThemeAction extends AbstractIconThemeAction { @IQuickInputService quickInputService: IQuickInputService, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, - @IViewletService viewletService: IViewletService + @IPaneCompositePartService paneCompositeService: IPaneCompositePartService ) { - super(id, label, quickInputService, extensionGalleryService, viewletService); + super(id, label, quickInputService, extensionGalleryService, paneCompositeService); } protected builtInEntry: QuickPickInput = { id: DEFAULT_PRODUCT_ICON_THEME_ID, label: localize('defaultProductIconThemeLabel', 'Default') }; @@ -255,8 +255,8 @@ function configurationEntries(extensionGalleryService: IExtensionGalleryService, return []; } -function openExtensionViewlet(viewletService: IViewletService, query: string) { - return viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { +function openExtensionViewlet(paneCompositeService: IPaneCompositePartService, query: string) { + return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true).then(viewlet => { if (viewlet) { (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); viewlet.focus(); @@ -338,7 +338,7 @@ class GenerateColorThemeAction extends Action { const category = localize('preferences', "Preferences"); -const colorThemeDescriptor = SyncActionDescriptor.from(SelectColorThemeAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_T) }); +const colorThemeDescriptor = SyncActionDescriptor.from(SelectColorThemeAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyT) }); Registry.as(Extensions.WorkbenchActions).registerWorkbenchAction(colorThemeDescriptor, 'Preferences: Color Theme', category); const fileIconThemeDescriptor = SyncActionDescriptor.from(SelectFileIconThemeAction); diff --git a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts index 5d67311d0f..204aa16bac 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts @@ -216,8 +216,8 @@ class Snapper { } public captureSyntaxTokens(fileName: string, content: string): Promise { - const modeId = this.modeService.getModeIdByFilepathOrFirstLine(URI.file(fileName)); - return this.textMateService.createGrammar(modeId!).then((grammar) => { + const languageId = this.modeService.getModeIdByFilepathOrFirstLine(URI.file(fileName)); + return this.textMateService.createGrammar(languageId!).then((grammar) => { if (!grammar) { return []; } diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index a99c0b8cef..a624ee3f9c 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -478,7 +478,7 @@ export class TimelinePane extends ViewPane { } private async loadTimeline(reset: boolean, sources?: string[]) { - // If we have no source, we are reseting all sources, so cancel everything in flight and reset caches + // If we have no source, we are resetting all sources, so cancel everything in flight and reset caches if (sources === undefined) { if (reset) { this.clear(true); diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts index 4b314f7a04..c2a92d05c6 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts @@ -206,7 +206,7 @@ registerAction2(class extends EditorAction2 { precondition: ContextKeyExpr.and(_ctxTypeHierarchyVisible, _ctxTypeHierarchyDirection.isEqualTo(TypeHierarchyDirection.Subtypes)), keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KEY_H, + primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KeyH, }, menu: { id: TypeHierarchyTreePeekWidget.TitleMenu, @@ -231,7 +231,7 @@ registerAction2(class extends EditorAction2 { precondition: ContextKeyExpr.and(_ctxTypeHierarchyVisible, _ctxTypeHierarchyDirection.isEqualTo(TypeHierarchyDirection.Supertypes)), keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KEY_H, + primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KeyH, }, menu: { id: TypeHierarchyTreePeekWidget.TitleMenu, diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index eabb7e071c..34d5d670a3 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -23,12 +23,13 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { asText, IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/common/markdownDocumentRenderer'; +import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; export class ReleaseNotesManager { @@ -194,7 +195,7 @@ export class ReleaseNotesManager { } private async addGAParameters(uri: URI, origin: string, experiment = '1'): Promise { - if (this._environmentService.isBuilt && !this._environmentService.isExtensionDevelopment && !this._environmentService.disableTelemetry && !!this._productService.enableTelemetry) { + if (supportsTelemetry(this._productService, this._environmentService)) { if (uri.scheme === 'https' && uri.authority === 'code.visualstudio.com') { const info = await this._telemetryService.getTelemetryInfo(); diff --git a/src/vs/workbench/contrib/update/browser/update.contribution.ts b/src/vs/workbench/contrib/update/browser/update.contribution.ts index ea62dabce0..5cbeb8ef1d 100644 --- a/src/vs/workbench/contrib/update/browser/update.contribution.ts +++ b/src/vs/workbench/contrib/update/browser/update.contribution.ts @@ -37,6 +37,6 @@ if (ShowCurrentReleaseNotesAction.AVAILABE) { id: ShowCurrentReleaseNotesAction.ID, title: localize({ key: 'miReleaseNotes', comment: ['&& denotes a mnemonic'] }, "&&Release Notes") }, - order: 4 + order: 5 }); } diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 8595997e72..8b42ffeb4d 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -553,8 +553,8 @@ export class SwitchProductQualityContribution extends Disposable implements IWor type: 'info', message: nls.localize('relaunchMessage', "Changing the version requires a reload to take effect"), detail: newQuality === 'insider' ? - nls.localize('relaunchDetailInsiders', "Press the reload button to switch to the nightly pre-production version of VSCode.") : - nls.localize('relaunchDetailStable', "Press the reload button to switch to the monthly released stable version of VSCode."), + nls.localize('relaunchDetailInsiders', "Press the reload button to switch to the Insiders version of VS Code.") : + nls.localize('relaunchDetailStable', "Press the reload button to switch to the Stable version of VS Code."), primaryButton: nls.localize('reload', "&&Reload") }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index b5d479efa8..63009499fc 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -20,7 +20,7 @@ import { localize } from 'vs/nls'; import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyEqualsExpr, ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -33,7 +33,8 @@ import { } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IEditorInput, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; @@ -696,7 +697,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }) as DiffEditorInput[]; } - private getAllConflictsEditorInputs(): IEditorInput[] { + private getAllConflictsEditorInputs(): EditorInput[] { return this.editorService.editors.filter(input => { const resource = input instanceof DiffEditorInput ? input.primary.resource : input.resource; return resource && getSyncResourceFromLocalPreview(resource!, this.environmentService) !== undefined; @@ -1161,7 +1162,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo when }, { id: MenuId.ViewContainerTitle, - when: ContextKeyEqualsExpr.create('viewContainer', SYNC_VIEW_CONTAINER_ID), + when: ContextKeyExpr.equals('viewContainer', SYNC_VIEW_CONTAINER_ID), group: 'navigation', order: 2 }] @@ -1185,7 +1186,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), }, { id: MenuId.ViewContainerTitle, - when: ContextKeyEqualsExpr.create('viewContainer', SYNC_VIEW_CONTAINER_ID), + when: ContextKeyExpr.equals('viewContainer', SYNC_VIEW_CONTAINER_ID), group: 'navigation', order: 1 }], @@ -1234,7 +1235,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: 'workbench.userDataSync.actions.help', title: CATEGORIES.Help.value }, - when: ContextKeyEqualsExpr.create('viewContainer', SYNC_VIEW_CONTAINER_ID), + when: ContextKeyExpr.equals('viewContainer', SYNC_VIEW_CONTAINER_ID), group: '1_help', }); } @@ -1267,7 +1268,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo title: localize('workbench.actions.syncData.reset', "Clear Data in Cloud..."), menu: [{ id: MenuId.ViewContainerTitle, - when: ContextKeyEqualsExpr.create('viewContainer', SYNC_VIEW_CONTAINER_ID), + when: ContextKeyExpr.equals('viewContainer', SYNC_VIEW_CONTAINER_ID), group: '0_configure', }], }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts index 3f71d20729..770a119056 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncMergesView.ts @@ -10,7 +10,7 @@ import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IUserDataSyncService, Change, MergeState, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr, ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Emitter, Event } from 'vs/base/common/event'; @@ -18,7 +18,7 @@ import { Disposable, dispose } from 'vs/base/common/lifecycle'; import { Codicon } from 'vs/base/common/codicons'; import { IUserDataSyncWorkbenchService, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, SYNC_MERGES_VIEW_ID } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { isEqual, basename } from 'vs/base/common/resources'; -import { IDecorationsProvider, IDecorationData, IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; +import { IDecorationsProvider, IDecorationData, IDecorationsService } from 'vs/workbench/services/decorations/common/decorations'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { listWarningForeground, listDeemphasizedForeground } from 'vs/platform/theme/common/colorRegistry'; import * as DOM from 'vs/base/browser/dom'; @@ -164,7 +164,7 @@ export class UserDataSyncMergesViewPane extends TreeViewPane { icon: Codicon.cloudDownload, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 1, }, @@ -184,7 +184,7 @@ export class UserDataSyncMergesViewPane extends TreeViewPane { icon: Codicon.cloudUpload, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 2, }, @@ -204,7 +204,7 @@ export class UserDataSyncMergesViewPane extends TreeViewPane { icon: Codicon.merge, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.equals('viewItem', 'sync-resource-preview')), group: 'inline', order: 3, }, @@ -224,7 +224,7 @@ export class UserDataSyncMergesViewPane extends TreeViewPane { icon: Codicon.discard, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.or(ContextKeyExpr.equals('viewItem', 'sync-resource-accepted'), ContextKeyExpr.equals('viewItem', 'sync-resource-conflict'))), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', SYNC_MERGES_VIEW_ID), ContextKeyExpr.or(ContextKeyExpr.equals('viewItem', 'sync-resource-accepted'), ContextKeyExpr.equals('viewItem', 'sync-resource-conflict'))), group: 'inline', order: 3, }, @@ -465,6 +465,10 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio return false; } + if (!this.configurationService.getValue('diffEditor.renderSideBySide')) { + return isEqual(userDataSyncResource.merged, model.uri); + } + return true; } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index f65fdd5d8b..8ef25bdfcb 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -9,7 +9,7 @@ import { isWeb } from 'vs/base/common/platform'; import { isEqual } from 'vs/base/common/resources'; import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IViewsService } from 'vs/workbench/common/views'; import { VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -45,7 +45,7 @@ export class UserDataSyncTrigger extends Disposable implements IWorkbenchContrib } } - private getUserDataEditorInputSource(editorInput: IEditorInput | undefined): string | undefined { + private getUserDataEditorInputSource(editorInput: EditorInput | undefined): string | undefined { if (!editorInput) { return undefined; } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 0b8261bb1f..9730a7f926 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -11,7 +11,7 @@ import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeVie import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle as IResourceHandle, SyncStatus, IUserDataSyncResourceEnablementService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataAutoSyncEnablementService, getLastSyncResourceUri } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { FolderThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -32,6 +32,7 @@ import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/ import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export class UserDataSyncDataViews extends Disposable { @@ -107,7 +108,7 @@ export class UserDataSyncDataViews extends Disposable { icon: Codicon.edit, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', id)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', id)), group: 'inline', }, }); @@ -127,7 +128,7 @@ export class UserDataSyncDataViews extends Disposable { title: localize('workbench.actions.sync.turnOffSyncOnMachine', "Turn off Settings Sync"), menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', id), ContextKeyEqualsExpr.create('viewItem', 'sync-machine')), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', id), ContextKeyExpr.equals('viewItem', 'sync-machine')), }, }); } @@ -182,7 +183,7 @@ export class UserDataSyncDataViews extends Disposable { title: localize('workbench.actions.sync.resolveResourceRef', "Show raw JSON sync data"), menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)) + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)) }, }); } @@ -193,6 +194,31 @@ export class UserDataSyncDataViews extends Disposable { } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.sync.compareWithLocal`, + title: localize('workbench.actions.sync.compareWithLocal', "Compare with Local"), + menu: { + id: MenuId.ViewItemContext, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /sync-associatedResource-.*/i)) + }, + }); + } + async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { + const commandService = accessor.get(ICommandService); + const { resource, comparableResource } = <{ resource: string, comparableResource: string }>JSON.parse(handle.$treeItemHandle); + const remoteResource = URI.parse(resource); + const localResource = URI.parse(comparableResource); + return commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, + remoteResource, + localResource, + localize('remoteToLocalDiff', "{0} ↔ {1}", localize({ key: 'leftResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(remoteResource)), localize({ key: 'rightResourceName', comment: ['local as in file in disk'] }, "{0} (Local)", basename(localResource))), + undefined + ); + } + }); + registerAction2(class extends Action2 { constructor() { super({ @@ -201,7 +227,7 @@ export class UserDataSyncDataViews extends Disposable { icon: Codicon.discard, menu: { id: MenuId.ViewItemContext, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)), group: 'inline', }, }); @@ -254,7 +280,8 @@ export class UserDataSyncDataViews extends Disposable { } interface ISyncResourceHandle extends IResourceHandle { - syncResource: SyncResource + syncResource: SyncResource; + previous?: IResourceHandle; } interface SyncResourceHandleTreeItem extends ITreeItem { @@ -322,17 +349,31 @@ abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProv } protected async getChildrenForSyncResourceTreeItem(element: SyncResourceHandleTreeItem): Promise { - const associatedResources = await this.userDataSyncService.getAssociatedResources((element).syncResourceHandle.syncResource, (element).syncResourceHandle); + const syncResourceHandle = (element).syncResourceHandle; + const associatedResources = await this.userDataSyncService.getAssociatedResources(syncResourceHandle.syncResource, syncResourceHandle); + const previousAssociatedResources = syncResourceHandle.previous ? await this.userDataSyncService.getAssociatedResources(syncResourceHandle.syncResource, syncResourceHandle.previous) : []; return associatedResources.map(({ resource, comparableResource }) => { const handle = JSON.stringify({ resource: resource.toString(), comparableResource: comparableResource.toString() }); - const leftResourceName = localize({ key: 'leftResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(resource)); - const rightResourceName = localize({ key: 'rightResourceName', comment: ['local as in file in disk'] }, "{0} (Local)", basename(comparableResource)); + const previousResource = previousAssociatedResources.find(previous => basename(previous.resource) === basename(resource))?.resource; return { handle, collapsibleState: TreeItemCollapsibleState.None, resourceUri: resource, - command: { id: API_OPEN_DIFF_EDITOR_COMMAND_ID, title: '', arguments: [resource, comparableResource, localize('sideBySideLabels', "{0} ↔ {1}", leftResourceName, rightResourceName), undefined] }, - contextValue: `sync-associatedResource-${(element).syncResourceHandle.syncResource}` + command: previousResource ? { + id: API_OPEN_DIFF_EDITOR_COMMAND_ID, + title: '', + arguments: [ + previousResource, + resource, + localize('sideBySideLabels', "{0} ↔ {1}", `${basename(resource)} (${fromNow(syncResourceHandle.previous!.created, true)})`, `${basename(resource)} (${fromNow(syncResourceHandle.created, true)})`), + undefined + ] + } : { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [resource, undefined, undefined] + }, + contextValue: `sync-associatedResource-${syncResourceHandle.syncResource}` }; }); } @@ -341,7 +382,8 @@ abstract class UserDataSyncActivityViewDataProvider implements ITreeViewDataProv if (this.syncResourceHandlesPromise === undefined) { this.syncResourceHandlesPromise = Promise.all(ALL_SYNC_RESOURCES.map(async syncResource => { const resourceHandles = await this.getResourceHandles(syncResource); - return resourceHandles.map(resourceHandle => ({ ...resourceHandle, syncResource })); + resourceHandles.sort((a, b) => b.created - a.created); + return resourceHandles.map((resourceHandle, index) => ({ ...resourceHandle, syncResource, previous: resourceHandles[index + 1] })); })).then(result => flatten(result).sort((a, b) => b.created - a.created)); } return this.syncResourceHandlesPromise; diff --git a/src/vs/workbench/contrib/watermark/browser/watermark.ts b/src/vs/workbench/contrib/watermark/browser/watermark.ts index 3346c3966f..f26b1dda67 100644 --- a/src/vs/workbench/contrib/watermark/browser/watermark.ts +++ b/src/vs/workbench/contrib/watermark/browser/watermark.ts @@ -27,24 +27,29 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler'; +import { ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +// import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; // {{SQL CARBON EDIT}} import { NewNotebookAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; import * as locConstants from 'sql/base/common/locConstants'; + const $ = dom.$; interface WatermarkEntry { text: string; id: string; mac?: boolean; + when?: ContextKeyExpression; } // {{SQL CARBON EDIT}} const newSqlFile: WatermarkEntry = { text: locConstants.watermarkNewSqlFile, id: NEW_UNTITLED_FILE_COMMAND_ID }; const newNotebook: WatermarkEntry = { text: locConstants.watermarkNewNotebook, id: NewNotebookAction.ID }; -/*const showCommands: WatermarkEntry = { text: nls.localize('watermark.showCommands', "Show All Commands"), id: ShowAllCommandsAction.ID }; +/* +const showCommands: WatermarkEntry = { text: nls.localize('watermark.showCommands', "Show All Commands"), id: ShowAllCommandsAction.ID }; const quickAccess: WatermarkEntry = { text: nls.localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' }; const openFileNonMacOnly: WatermarkEntry = { text: nls.localize('watermark.openFile', "Open File"), id: OpenFileAction.ID, mac: false }; const openFolderNonMacOnly: WatermarkEntry = { text: nls.localize('watermark.openFolder', "Open Folder"), id: OpenFolderAction.ID, mac: false }; @@ -52,9 +57,13 @@ const openFileOrFolderMacOnly: WatermarkEntry = { text: nls.localize('watermark. const openRecent: WatermarkEntry = { text: nls.localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' }; const newUntitledFile: WatermarkEntry = { text: nls.localize('watermark.newUntitledFile', "New Untitled File"), id: NEW_UNTITLED_FILE_COMMAND_ID }; const newUntitledFileMacOnly: WatermarkEntry = Object.assign({ mac: true }, newUntitledFile); -const toggleTerminal: WatermarkEntry = { text: nls.localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: TERMINAL_COMMAND_ID.TOGGLE };*/ +const toggleTerminal: WatermarkEntry = { text: nls.localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: TerminalCommandId.Toggle, when: TerminalContextKeys.processSupported }; +*/ const findInFiles: WatermarkEntry = { text: nls.localize('watermark.findInFiles', "Find in Files"), id: FindInFilesActionId }; -// const startDebugging: WatermarkEntry = { text: nls.localize('watermark.startDebugging', "Start Debugging"), id: StartAction.ID }; {{SQL CARBON EDIT}} no unused +//const toggleTerminal: WatermarkEntry = { text: nls.localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: TerminalCommandId.Toggle, when: TerminalContextKeys.processSupported }; +//const startDebugging: WatermarkEntry = { text: nls.localize('watermark.startDebugging', "Start Debugging"), id: DEBUG_START_COMMAND_ID, when: CONTEXT_DEBUGGERS_AVAILABLE }; +//const toggleFullscreen: WatermarkEntry = { text: nls.localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen', when: TerminalContextKeys.processSupported.toNegated() }; +//const showSettings: WatermarkEntry = { text: nls.localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings', when: TerminalContextKeys.processSupported.toNegated() }; // {{SQL CARBON EDIT}} - Replace noFolderEntries and folderEntries const noFolderEntries = [ @@ -83,6 +92,7 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IThemeService private readonly themeService: IThemeService @@ -124,6 +134,15 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr this.recreate(); } })); + + const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!); + const allKeys = new Set(); + allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key))); + this._register(this.contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(allKeys)) { + this.recreate(); + } + })); } private create(): void { @@ -133,7 +152,8 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr this.watermark = $('.watermark'); const box = dom.append(this.watermark, $('.watermark-box')); const folder = this.workbenchState !== WorkbenchState.EMPTY; - const selected = folder ? folderEntries : noFolderEntries + const selected = (folder ? folderEntries : noFolderEntries) + .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when)) .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb)) .filter(entry => !!CommandsRegistry.getCommand(entry.id)); @@ -200,3 +220,4 @@ Registry.as(ConfigurationExtensions.Configuration) }, } }); + diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 2a30e31bdf..92a67373a9 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -21,6 +21,7 @@ const searchParams = new URL(location.toString()).searchParams; const ID = searchParams.get('id'); const onElectron = searchParams.get('platform') === 'electron'; const expectedWorkerVersion = parseInt(searchParams.get('swVersion')); +const parentOrigin = searchParams.get('parentOrigin'); /** * Use polling to track focus of main webview and iframes within the webview @@ -30,7 +31,7 @@ const expectedWorkerVersion = parseInt(searchParams.get('swVersion')); * @param {() => void} handlers.onBlur */ const trackFocus = ({ onFocus, onBlur }) => { - const interval = 50; + const interval = 250; let isFocused = document.hasFocus(); setInterval(() => { const isCurrentlyFocused = document.hasFocus(); @@ -90,7 +91,7 @@ defaultStyles.textContent = ` max-height: 100%; } - a { + a, a code { color: var(--vscode-textLink-foreground, var(--theme-link)); } @@ -197,7 +198,7 @@ function getVsCodeApiScript(allowMultipleAPIAcquire, state) { } /** @type {Promise} */ -const workerReady = new Promise(async (resolve, reject) => { +const workerReady = new Promise((resolve, reject) => { if (!areServiceWorkersEnabled()) { return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.')); } @@ -233,7 +234,28 @@ const workerReady = new Promise(async (resolve, reject) => { } }; navigator.serviceWorker.addEventListener('message', versionHandler); - assertIsDefined(registration.active).postMessage({ channel: 'version' }); + + const postVersionMessage = () => { + assertIsDefined(navigator.serviceWorker.controller).postMessage({ channel: 'version' }); + }; + + // At this point, either the service worker is ready and + // became our controller, or we need to wait for it. + // Note that navigator.serviceWorker.controller could be a + // controller from a previously loaded service worker. + const currentController = navigator.serviceWorker.controller; + if (currentController && currentController.scriptURL.endsWith(swPath)) { + // service worker already loaded & ready to receive messages + postVersionMessage(); + } else { + // either there's no controlling service worker, or it's an old one: + // wait for it to change before posting the message + const onControllerChange = () => { + navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange); + postVersionMessage(); + }; + navigator.serviceWorker.addEventListener('controllerchange', onControllerChange); + } }, error => { reject(new Error(`Could not register service workers: ${error}.`)); @@ -246,6 +268,11 @@ const hostMessaging = new class HostMessaging { this.handlers = new Map(); window.addEventListener('message', (e) => { + if (e.origin !== parentOrigin) { + console.log(`skipping webview message due to mismatched origins: ${e.origin} ${parentOrigin}`); + return; + } + const channel = e.data.channel; const handlers = this.handlers.get(channel); if (handlers) { @@ -263,7 +290,7 @@ const hostMessaging = new class HostMessaging { * @param {any} data */ postMessage(channel, data) { - window.parent.postMessage({ target: ID, channel, data }, '*'); + window.parent.postMessage({ target: ID, channel, data }, parentOrigin); } /** @@ -424,19 +451,18 @@ const handleInnerClick = (event) => { return; } - const baseElement = event.view.document.getElementsByTagName('base')[0]; + const baseElement = event.view.document.querySelector('base'); for (const pathElement of event.composedPath()) { /** @type {any} */ const node = pathElement; - if (node.tagName === 'A' && node.href) { + if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { if (node.getAttribute('href') === '#') { event.view.scrollTo(0, 0); } else if (node.hash && (node.getAttribute('href') === node.hash || (baseElement && node.href === baseElement.href + node.hash))) { - const scrollTarget = event.view.document.getElementById(node.hash.substr(1, node.hash.length - 1)); - if (scrollTarget) { - scrollTarget.scrollIntoView(); - } + const fragment = node.hash.substr(1, node.hash.length - 1); + const scrollTarget = event.view.document.getElementById(decodeURIComponent(fragment)); + scrollTarget?.scrollIntoView(); } else { hostMessaging.postMessage('did-click-link', node.href.baseVal || node.href); } @@ -460,7 +486,7 @@ const handleAuxClick = for (const pathElement of event.composedPath()) { /** @type {any} */ const node = pathElement; - if (node.tagName === 'A' && node.href) { + if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { event.preventDefault(); return; } @@ -615,6 +641,7 @@ function areServiceWorkersEnabled() { * contents: string; * options: { * readonly allowScripts: boolean; + * readonly allowForms: boolean; * readonly allowMultipleAPIAcquire: boolean; * } * state: any; @@ -640,6 +667,11 @@ function toContentHtml(data) { } }); + // Set default aria role + if (!newDocument.body.hasAttribute('role')) { + newDocument.body.setAttribute('role', 'document'); + } + // Inject default script if (options.allowScripts) { const defaultScript = newDocument.createElement('script'); @@ -719,7 +751,6 @@ onDomReady(() => { let updateId = 0; hostMessaging.onMessage('content', async (_event, /** @type {ContentUpdateData} */ data) => { const currentUpdateId = ++updateId; - try { await workerReady; } catch (e) { @@ -773,7 +804,16 @@ onDomReady(() => { const newFrame = document.createElement('iframe'); newFrame.setAttribute('id', 'pending-frame'); newFrame.setAttribute('frameborder', '0'); - newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin allow-pointer-lock allow-downloads' : 'allow-same-origin allow-pointer-lock'); + + const sandboxRules = new Set(['allow-same-origin', 'allow-pointer-lock']); + if (options.allowScripts) { + sandboxRules.add('allow-scripts'); + sandboxRules.add('allow-downloads'); + } + if (options.allowForms) { + sandboxRules.add('allow-forms'); + } + newFrame.setAttribute('sandbox', Array.from(sandboxRules).join(' ')); if (!isFirefox) { newFrame.setAttribute('allow', options.allowScripts ? 'clipboard-read; clipboard-write;' : ''); } @@ -839,6 +879,7 @@ onDomReady(() => { const newFrame = getPendingFrame(); if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { + const wasFocused = document.hasFocus(); const oldActiveFrame = getActiveFrame(); if (oldActiveFrame) { document.body.removeChild(oldActiveFrame); @@ -853,12 +894,12 @@ onDomReady(() => { contentWindow.addEventListener('scroll', handleInnerScroll); contentWindow.addEventListener('wheel', handleWheel); - if (document.hasFocus()) { + if (wasFocused) { contentWindow.focus(); } pendingMessages.forEach((message) => { - contentWindow.postMessage(message.message, '*', message.transfer); + contentWindow.postMessage(message.message, window.origin, message.transfer); }); pendingMessages = []; } @@ -920,7 +961,7 @@ onDomReady(() => { if (!pending) { const target = getActiveFrame(); if (target) { - assertIsDefined(target.contentWindow).postMessage(data.message, '*', data.transfer); + assertIsDefined(target.contentWindow).postMessage(data.message, window.origin, data.transfer); return; } } diff --git a/src/vs/workbench/contrib/webview/browser/resourceLoading.ts b/src/vs/workbench/contrib/webview/browser/resourceLoading.ts index 6cb7969113..a4bb35e153 100644 --- a/src/vs/workbench/contrib/webview/browser/resourceLoading.ts +++ b/src/vs/workbench/contrib/webview/browser/resourceLoading.ts @@ -100,7 +100,7 @@ function getResourceToLoad( function containsResource(root: URI, resource: URI): boolean { if (root.scheme !== resource.scheme) { - return true; + return false; } let rootPath = root.fsPath + (root.fsPath.endsWith(sep) ? '' : sep); diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index a6ae6d3a85..03e16bf753 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -69,6 +69,7 @@ export interface IWebviewService { export const enum WebviewContentPurpose { NotebookRenderer = 'notebookRenderer', CustomEditor = 'customEditor', + WebviewView = 'webviewView', } export type WebviewStyles = { [key: string]: string | number; }; @@ -86,6 +87,7 @@ export interface WebviewOptions { export interface WebviewContentOptions { readonly allowMultipleAPIAcquire?: boolean; readonly allowScripts?: boolean; + readonly allowForms?: boolean; readonly localResourceRoots?: ReadonlyArray; readonly portMapping?: ReadonlyArray; readonly enableCommandUris?: boolean; @@ -95,6 +97,7 @@ export function areWebviewContentOptionsEqual(a: WebviewContentOptions, b: Webvi return ( a.allowMultipleAPIAcquire === b.allowMultipleAPIAcquire && a.allowScripts === b.allowScripts + && a.allowForms === b.allowForms && equals(a.localResourceRoots, b.localResourceRoots, isEqual) && equals(a.portMapping, b.portMapping, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort) && a.enableCommandUris === b.enableCommandUris @@ -102,7 +105,7 @@ export function areWebviewContentOptionsEqual(a: WebviewContentOptions, b: Webvi } export interface WebviewExtensionDescription { - readonly location: URI; + readonly location?: URI; readonly id: ExtensionIdentifier; } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index d9f64b8ae6..b6d34495c5 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -98,7 +98,17 @@ export class IFrameWebview extends Disposable implements Webview { protected get element(): HTMLIFrameElement | undefined { return this._element; } private _focused: boolean | undefined; - public get isFocused(): boolean { return !!this._focused; } + public get isFocused(): boolean { + if (!this._focused) { + return false; + } + if (document.activeElement && document.activeElement !== this.element) { + // looks like https://github.com/microsoft/vscode/issues/132641 + // where the focus is actually not in the `