From 0bfbdc62ed4fbffe6b77fd06f400532386abc764 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Wed, 1 Apr 2020 00:44:39 -0700 Subject: [PATCH] Merge from vscode 2f984aad710215f4e4684a035bb02f55d1a9e2cc (#9819) --- build/builtin/browser-main.js | 1 - build/gulpfile.editor.js | 2 - build/gulpfile.vscode.js | 4 +- build/gulpfile.vscode.linux.js | 4 - build/lib/watch/watch-win32.js | 4 +- cglicenses.json | 80 +++ extensions/image-preview/src/preview.ts | 2 +- .../src/features/previewManager.ts | 2 +- extensions/shared.webpack.config.js | 2 +- extensions/vscode-account/package.json | 15 +- extensions/vscode-account/src/keychain.ts | 5 +- extensions/vscode-account/src/logger.ts | 18 + extensions/vscode-colorize-tests/package.json | 123 ++-- .../src/colorizerTestMain.ts | 2 +- package.json | 3 +- remote/package.json | 2 +- remote/yarn.lock | 10 +- scripts/code-web.js | 2 +- scripts/code.sh | 3 + src/bootstrap-fork.js | 2 - src/bootstrap-window.js | 4 - src/main.js | 2 +- src/paths.js | 4 +- .../dashboardGridContainer.component.ts | 3 +- .../browser/jobsView.component.ts | 4 +- .../browser/notebooksView.component.ts | 4 +- .../ui/codiconLabel/codicon/codicon.css | 19 +- .../ui/codiconLabel/codicon/codicon.ttf | Bin 57968 -> 58140 bytes src/vs/base/browser/ui/splitview/paneview.ts | 4 +- src/vs/base/common/arrays.ts | 11 - src/vs/base/common/buffer.ts | 45 +- src/vs/base/common/comparers.ts | 9 +- src/vs/base/common/extpath.ts | 6 +- src/vs/base/common/fuzzyScorer.ts | 80 ++- src/vs/base/common/glob.ts | 8 +- src/vs/base/common/labels.ts | 8 +- src/vs/base/common/map.ts | 26 - src/vs/base/common/mime.ts | 8 +- src/vs/base/common/uri.ts | 4 +- src/vs/base/node/pfs.ts | 3 +- .../quickinput/browser/media/quickInput.css | 2 +- src/vs/base/parts/storage/node/storage.ts | 5 +- src/vs/base/test/common/fuzzyScorer.test.ts | 47 +- src/vs/base/test/common/map.test.ts | 15 +- src/vs/base/test/common/uri.test.ts | 3 + src/vs/base/test/node/path.test.ts | 8 +- src/vs/code/electron-main/app.ts | 3 +- src/vs/code/electron-main/window.ts | 33 +- src/vs/css.build.js | 2 +- src/vs/css.js | 2 +- .../browser/config/elementSizeObserver.ts | 81 ++- src/vs/editor/common/config/editorOptions.ts | 11 - .../common/controller/cursorWordOperations.ts | 4 +- src/vs/editor/common/core/stringBuilder.ts | 5 +- src/vs/editor/common/model.ts | 12 +- src/vs/editor/common/model/editStack.ts | 5 +- src/vs/editor/common/model/textModel.ts | 21 +- src/vs/editor/common/model/tokensStore.ts | 327 ++++++++-- .../common/modes/supports/richEditBrackets.ts | 21 +- src/vs/editor/common/services/modelService.ts | 6 + .../common/services/modelServiceImpl.ts | 398 +++-------- .../services/semanticTokensProviderStyling.ts | 261 ++++++++ .../common/viewLayout/viewLineRenderer.ts | 9 +- .../editor/contrib/hover/modesContentHover.ts | 12 +- .../editor/contrib/multicursor/multicursor.ts | 12 +- .../quickAccess/gotoLineQuickAccess.ts | 4 +- .../quickAccess/gotoSymbolQuickAccess.ts | 4 +- .../contrib/suggest/suggestController.ts | 4 - .../suggest/suggestRangeHighlighter.ts | 128 ---- .../viewportSemanticTokens.ts | 106 +++ .../test/wordPartOperations.test.ts | 44 +- src/vs/editor/editor.all.ts | 2 + .../test/common/model/tokensStore.test.ts | 117 +++- .../viewLayout/viewLineRenderer.test.ts | 14 +- .../node/classification/typescript.test.ts | 2 +- src/vs/monaco.d.ts | 4 - src/vs/nls.js | 9 +- .../platform/backup/electron-main/backup.ts | 16 +- .../backup/electron-main/backupMainService.ts | 118 ++-- .../electron-main/backupMainService.test.ts | 42 ++ .../clipboard/browser/clipboardService.ts | 3 +- .../common/configurationService.ts | 1 - .../electron-main/electronMainService.ts | 5 +- .../environment/common/environment.ts | 3 +- src/vs/platform/environment/node/argv.ts | 4 +- .../environment/node/environmentService.ts | 12 + .../files/node/diskFileSystemProvider.ts | 9 +- .../node/watcher/win32/watcherService.ts | 4 +- src/vs/platform/label/common/label.ts | 3 +- .../notification/common/notification.ts | 2 +- .../quickinput/browser/helpQuickAccess.ts | 4 +- .../quickinput/browser/quickAccess.ts | 20 +- .../platform/quickinput/common/quickAccess.ts | 13 +- .../storage/browser/storageService.ts | 5 +- src/vs/platform/storage/node/storageIpc.ts | 16 +- .../common/tokenClassificationRegistry.ts | 29 +- .../undoRedo/common/undoRedoService.ts | 5 +- .../common/abstractSynchronizer.ts | 17 +- .../userDataSync/common/extensionsSync.ts | 41 +- .../userDataSync/common/globalStateMerge.ts | 45 +- .../userDataSync/common/globalStateSync.ts | 68 +- .../userDataSync/common/keybindingsSync.ts | 2 +- .../userDataSync/common/settingsSync.ts | 4 +- .../userDataSync/common/snippetsSync.ts | 53 +- .../userDataSync/common/userDataSync.ts | 6 + .../common/userDataSyncService.ts | 11 +- .../common/userDataSyncStoreService.ts | 4 +- .../test/common/globalStateMerge.test.ts | 71 +- .../test/common/synchronizer.test.ts | 6 +- .../test/common/userDataSyncService.test.ts | 45 +- src/vs/platform/windows/common/windows.ts | 1 + .../platform/windows/electron-main/windows.ts | 3 + .../electron-main/windowsMainService.ts | 9 +- .../platform/workspaces/common/workspaces.ts | 43 +- .../workspacesHistoryMainService.ts | 21 +- .../electron-main/workspacesMainService.ts | 2 +- .../electron-main/workspacesService.ts | 20 +- .../workspacesMainService.test.ts | 90 ++- src/vs/vscode.d.ts | 580 ++++++++++++++++ src/vs/vscode.proposed.d.ts | 617 ++---------------- .../api/browser/mainThreadAuthentication.ts | 29 +- .../api/browser/mainThreadLanguageFeatures.ts | 2 +- .../api/browser/mainThreadNotebook.ts | 13 +- .../api/common/configurationExtensionPoint.ts | 2 +- .../workbench/api/common/extHost.api.impl.ts | 3 - .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostLanguageFeatures.ts | 2 +- .../workbench/api/common/extHostNotebook.ts | 45 +- src/vs/workbench/api/common/extHostTypes.ts | 175 ++++- src/vs/workbench/api/common/extHostWebview.ts | 16 +- .../api/common/shared/semanticTokens.ts | 113 ---- .../api/common/shared/semanticTokensDto.ts | 152 +++++ .../browser/actions/developerActions.ts | 2 +- .../browser/actions/layoutActions.ts | 75 ++- .../workbench/browser/actions/listCommands.ts | 2 +- .../browser/actions/media/actions.css | 50 ++ .../browser/actions/media/screencast.css | 50 -- .../browser/actions/windowActions.ts | 180 +++-- src/vs/workbench/browser/editor.ts | 10 +- .../parts/activitybar/activitybarPart.ts | 2 +- .../parts/editor/editor.contribution.ts | 2 +- .../browser/parts/editor/editorActions.ts | 3 +- .../browser/parts/editor/editorGroupView.ts | 4 +- .../browser/parts/editor/editorPart.ts | 3 +- .../browser/parts/editor/editorQuickAccess.ts | 4 +- .../notifications/notificationsViewer.ts | 3 +- .../browser/parts/panel/media/panelpart.css | 4 +- .../browser/parts/statusbar/statusbarPart.ts | 5 +- src/vs/workbench/browser/parts/views/views.ts | 7 +- .../common/editor/diffEditorModel.ts | 4 +- .../backup/electron-browser/backupTracker.ts | 4 +- .../inspectEditorTokens.ts | 23 +- .../browser/largeFileOptimizations.ts | 7 +- .../codeEditor/browser/semanticTokensHelp.ts | 10 +- .../customEditor/browser/customEditorInput.ts | 47 +- .../customEditor/browser/customEditors.ts | 32 +- .../contrib/debug/browser/breakpointsView.ts | 14 +- .../browser/extensionHostDebugService.ts | 5 +- .../extensions/browser/extensionEditor.ts | 76 ++- .../extensions/browser/extensionsActions.ts | 332 ++++++---- .../extensions/browser/extensionsList.ts | 39 +- .../extensions/browser/extensionsViewer.ts | 122 +++- .../extensions/browser/extensionsViews.ts | 9 +- .../extensions/browser/extensionsWidgets.ts | 36 +- .../extensions/browser/media/extension.css | 173 +++++ .../browser/media/extensionActions.css | 80 +-- .../browser/media/extensionEditor.css | 99 ++- .../browser/media/extensionsViewlet.css | 230 +------ .../browser/media/extensionsWidgets.css | 21 + .../extensionsActions.test.ts | 66 +- .../editors/textFileSaveErrorHandler.ts | 6 +- .../files/browser/files.contribution.ts | 8 +- .../files/browser/views/openEditorsView.ts | 5 +- .../browser/localizations.contribution.ts | 1 + .../contrib/notebook/browser/constants.ts | 13 +- .../browser/contrib/notebookActions.ts | 146 ++++- .../contrib/notebook/browser/notebook.css | 38 +- .../notebook/browser/notebookBrowser.ts | 54 +- .../notebook/browser/notebookEditor.ts | 128 +++- .../notebook/browser/notebookService.ts | 13 +- .../view/renderers/backLayerWebView.ts | 7 +- .../browser/view/renderers/cellRenderer.ts | 192 ++++-- .../browser/view/renderers/codeCell.ts | 66 +- .../browser/view/renderers/markdownCell.ts | 8 +- .../browser/viewModel/baseCellViewModel.ts | 36 +- .../browser/viewModel/codeCellViewModel.ts | 29 +- .../browser/viewModel/notebookViewModel.ts | 11 + .../common/model/notebookTextModel.ts | 9 +- .../contrib/notebook/common/notebookCommon.ts | 12 +- .../notebook/test/notebookViewModel.test.ts | 6 +- .../notebook/test/testNotebookEditor.ts | 12 + .../contrib/outline/browser/outlinePane.ts | 23 +- .../quickaccess/browser/viewQuickAccess.ts | 4 +- .../search/browser/anythingQuickAccess.ts | 14 +- .../search/browser/media/searchview.css | 2 +- .../searchEditor/browser/searchEditorInput.ts | 3 +- .../tasks/browser/abstractTaskService.ts | 14 +- .../contrib/tasks/browser/taskQuickPick.ts | 39 +- .../contrib/tasks/common/taskService.ts | 2 +- .../terminal/browser/terminalConfigHelper.ts | 7 +- .../terminal/browser/terminalService.ts | 18 +- .../test/browser/terminalConfigHelper.test.ts | 35 +- .../themes/browser/themes.contribution.ts | 228 ++++--- .../timeline/browser/timeline.contribution.ts | 7 - .../contrib/update/browser/update.ts | 7 +- .../userDataSync/browser/userDataSync.ts | 27 +- .../browser/webviewWorkbenchService.ts | 54 +- .../actions/media/actions.css | 8 + .../electron-browser/actions/windowActions.ts | 10 +- src/vs/workbench/electron-browser/window.ts | 20 +- .../backup/common/backupFileService.ts | 4 +- .../browser/configurationService.ts | 2 +- .../services/editor/browser/editorService.ts | 11 +- .../environment/browser/environmentService.ts | 15 +- .../common/abstractExtensionService.ts | 4 +- .../extensions/common/proxyIdentifier.ts | 1 - .../node/extensionHostProcessSetup.ts | 2 +- .../host/browser/browserHostService.ts | 3 +- .../services/label/common/labelService.ts | 6 +- .../common/notificationService.ts | 14 +- .../services/search/node/fileSearch.ts | 2 +- .../services/search/node/rawSearchService.ts | 2 +- .../common/textFileSaveParticipant.ts | 5 +- .../electron-browser/nativeTextFileService.ts | 16 +- .../themes/browser/productIconThemeData.ts | 2 +- .../themes/browser/workbenchThemeService.ts | 4 +- .../services/themes/common/colorThemeData.ts | 57 +- .../themes/common/themeConfiguration.ts | 14 +- .../themes/common/themeExtensionPoints.ts | 9 +- .../tokenClassificationExtensionPoint.ts | 134 ++-- .../themes/common/workbenchThemeService.ts | 42 +- .../tokenStyleResolving.test.ts | 16 +- .../views/browser/viewDescriptorService.ts | 41 +- .../workingCopyFileOperationParticipant.ts | 5 +- .../workingCopy/common/workingCopyService.ts | 4 +- .../abstractWorkspaceEditingService.ts | 6 +- .../workspaces/browser/workspacesService.ts | 11 +- .../workspaceEditingService.ts | 2 - .../test/browser/api/extHostTypes.test.ts | 60 ++ .../test/common/api/semanticTokensDto.test.ts | 110 ++++ .../textsearch.perf.integrationTest.ts | 2 +- .../electron-browser/workbenchTestServices.ts | 8 +- test/automation/src/extensions.ts | 2 +- test/integration/browser/src/index.ts | 14 +- test/smoke/README.md | 1 - test/unit/browser/index.js | 2 +- yarn.lock | 15 +- 247 files changed, 5402 insertions(+), 3311 deletions(-) create mode 100644 src/vs/editor/common/services/semanticTokensProviderStyling.ts delete mode 100644 src/vs/editor/contrib/suggest/suggestRangeHighlighter.ts create mode 100644 src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts delete mode 100644 src/vs/workbench/api/common/shared/semanticTokens.ts create mode 100644 src/vs/workbench/api/common/shared/semanticTokensDto.ts delete mode 100644 src/vs/workbench/browser/actions/media/screencast.css create mode 100644 src/vs/workbench/contrib/extensions/browser/media/extension.css create mode 100644 src/vs/workbench/electron-browser/actions/media/actions.css create mode 100644 src/vs/workbench/test/common/api/semanticTokensDto.test.ts diff --git a/build/builtin/browser-main.js b/build/builtin/browser-main.js index fb4a4e28f2..9d52b75fb6 100644 --- a/build/builtin/browser-main.js +++ b/build/builtin/browser-main.js @@ -6,7 +6,6 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); -// @ts-ignore review const { remote } = require('electron'); const dialog = remote.dialog; diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 3ca8dbb731..948627332f 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -455,10 +455,8 @@ function createTscCompileTask(watch) { // e.g. src/vs/base/common/strings.ts(663,5): error TS2322: Type '1234' is not assignable to type 'string'. let fullpath = path.join(root, match[1]); let message = match[3]; - // @ts-ignore reporter(fullpath + message); } else { - // @ts-ignore reporter(str); } } diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index af59df1239..5c96a4a4b2 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -36,10 +36,8 @@ const { compileBuildTask } = require('./gulpfile.compile'); const { compileExtensionsBuildTask } = require('./gulpfile.extensions'); const productionDependencies = deps.getProductionDependencies(path.dirname(__dirname)); - const baseModules = Object.keys(process.binding('natives')).filter(n => !/^_|\//.test(n)); -// {{SQL CARBON EDIT}} -const nodeModules = [ +const nodeModules = [ // {{SQL CARBON EDIT}} 'electron', 'original-fs', 'rxjs/Observable', diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 1e3f338da3..57719d9fc9 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -92,9 +92,7 @@ function prepareDebPackage(arch) { const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@ARCHITECTURE@@', debArch)) - // @ts-ignore JSON checking: quality is optional .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) - // @ts-ignore JSON checking: updateUrl is optional .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(rename('DEBIAN/postinst')); @@ -169,9 +167,7 @@ function prepareRpmPackage(arch) { .pipe(replace('@@RELEASE@@', linuxPackageRevision)) .pipe(replace('@@ARCHITECTURE@@', rpmArch)) .pipe(replace('@@LICENSE@@', product.licenseName)) - // @ts-ignore JSON checking: quality is optional .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) - // @ts-ignore JSON checking: updateUrl is optional .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(replace('@@DEPENDENCIES@@', rpmDependencies[rpmArch].join(', '))) .pipe(rename('SPECS/' + product.applicationName + '.spec')); diff --git a/build/lib/watch/watch-win32.js b/build/lib/watch/watch-win32.js index 4ceb09c166..acaa50b8c3 100644 --- a/build/lib/watch/watch-win32.js +++ b/build/lib/watch/watch-win32.js @@ -25,7 +25,6 @@ function watch(root) { var child = cp.spawn(watcherPath, [root]); child.stdout.on('data', function (data) { - // @ts-ignore var lines = data.toString('utf8').split('\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); @@ -47,7 +46,6 @@ function watch(root) { path: changePathFull, base: root }); - //@ts-ignore file.event = toChangeType(changeType); result.emit('data', file); } @@ -106,4 +104,4 @@ module.exports = function (pattern, options) { }); })) .pipe(rebase); -}; \ No newline at end of file +}; diff --git a/cglicenses.json b/cglicenses.json index 76e6fe49f0..0d588440ae 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -89,5 +89,85 @@ "prependLicenseText": [ "Copyright (c) Microsoft Corporation. All rights reserved." ] + }, + { + // Reason: The license at https://github.com/reem/rust-unreachable/blob/master/LICENSE-MIT + // cannot be found by the OSS tool automatically. + "name": "reem/rust-unreachable", + "fullLicenseText": [ + "Copyright (c) 2015 The rust-unreachable Developers", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ] + }, + { + // Reason: The license at https://github.com/reem/rust-void/blob/master/LICENSE-MIT + // cannot be found by the OSS tool automatically. + "name": "reem/rust-void", + "fullLicenseText": [ + "Copyright (c) 2015 The rust-void Developers", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ] + }, + { + // Reason: The license at https://github.com/mrhooray/crc-rs/blob/master/LICENSE-MIT + // cannot be found by the OSS tool automatically. + "name": "mrhooray/crc-rs", + "fullLicenseText": [ + "MIT License", + "", + "Copyright (c) 2017 crc-rs Developers", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ] } ] diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index 8a5f30c98e..9686329f2b 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -28,7 +28,7 @@ export class PreviewManager implements vscode.CustomEditorProvider { ) { } public async openCustomDocument(uri: vscode.Uri) { - return new vscode.CustomDocument(PreviewManager.viewType, uri); + return new vscode.CustomDocument(uri); } public async resolveCustomEditor( diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index af7051f357..6970c98ec0 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -151,7 +151,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview } public async openCustomDocument(uri: vscode.Uri) { - return new vscode.CustomDocument(this.customEditorViewType, uri); + return new vscode.CustomDocument(uri); } public async resolveCustomTextEditor( diff --git a/extensions/shared.webpack.config.js b/extensions/shared.webpack.config.js index 8f55c823ed..f1546f9e15 100644 --- a/extensions/shared.webpack.config.js +++ b/extensions/shared.webpack.config.js @@ -69,7 +69,7 @@ module.exports = function withDefaults(/**@type WebpackConfig*/extConfig) { // yes, really source maps devtool: 'source-map', plugins: [ - // @ts-ignore + // @ts-expect-error new CopyWebpackPlugin([ { from: 'src', to: '.', ignore: ['**/test/**', '*.ts'] } ]), diff --git a/extensions/vscode-account/package.json b/extensions/vscode-account/package.json index cd579aef85..3a5407738c 100644 --- a/extensions/vscode-account/package.json +++ b/extensions/vscode-account/package.json @@ -27,7 +27,20 @@ "title": "%signOut%", "category": "%displayName%" } - ] + ], + "configuration": { + "title": "Microsoft Account", + "properties": { + "microsoftAccount.logLevel": { + "type": "string", + "enum": [ + "info", + "trace" + ], + "default": "info" + } + } + } }, "scripts": { "vscode:prepublish": "npm run compile", diff --git a/extensions/vscode-account/src/keychain.ts b/extensions/vscode-account/src/keychain.ts index b5314e5de3..73ab288f5f 100644 --- a/extensions/vscode-account/src/keychain.ts +++ b/extensions/vscode-account/src/keychain.ts @@ -45,6 +45,7 @@ export class Keychain { async setToken(token: string): Promise { try { + Logger.trace('Writing to keychain', token); return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token); } catch (e) { // Ignore @@ -59,7 +60,9 @@ export class Keychain { async getToken(): Promise { try { - return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); + const result = await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); + Logger.trace('Reading from keychain', result); + return result; } catch (e) { // Ignore Logger.error(`Getting token failed: ${e}`); diff --git a/extensions/vscode-account/src/logger.ts b/extensions/vscode-account/src/logger.ts index ec0699eab3..c5dd1235bd 100644 --- a/extensions/vscode-account/src/logger.ts +++ b/extensions/vscode-account/src/logger.ts @@ -7,11 +7,23 @@ import * as vscode from 'vscode'; type LogLevel = 'Trace' | 'Info' | 'Error'; +enum Level { + Trace = 'trace', + Info = 'Info' +} + class Log { private output: vscode.OutputChannel; + private level: Level; constructor() { this.output = vscode.window.createOutputChannel('Account'); + this.level = vscode.workspace.getConfiguration('microsoftAccount').get('logLevel') || Level.Info; + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('microsoftAccount.logLevel')) { + this.level = vscode.workspace.getConfiguration('microsoftAccount').get('logLevel') || Level.Info; + } + }); } private data2String(data: any): string { @@ -32,6 +44,12 @@ class Log { this.logLevel('Error', message, data); } + public trace(message: string, data?: any): void { + if (this.level === Level.Trace) { + this.logLevel('Trace', message, data); + } + } + public logLevel(level: LogLevel, message: string, data?: any): void { this.output.appendLine(`[${level} - ${this.now()}] ${message}`); if (data) { diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index 0f3815b2b6..c976dbc8d0 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -1,68 +1,59 @@ { - "name": "vscode-colorize-tests", - "description": "Colorize tests for VS Code", - "version": "0.0.1", - "publisher": "vscode", - "license": "MIT", - "private": true, - "activationEvents": [ - "onLanguage:json" - ], - "main": "./out/colorizerTestMain", - "enableProposedApi": true, - "engines": { - "vscode": "*" - }, - "scripts": { - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json" - }, - "dependencies": { - "jsonc-parser": "2.2.1" - }, - "devDependencies": { - "@types/node": "^12.11.7", - "mocha-junit-reporter": "^1.17.0", - "mocha-multi-reporters": "^1.1.7", - "vscode": "1.1.5" - }, - "contributes": { - "semanticTokenTypes": [ - { - "id": "testToken", - "description": "A test token" - } - ], - "semanticTokenModifiers": [ - { - "id": "testModifier", - "description": "A test modifier" - } - ], - "semanticTokenStyleDefaults": [ - { - "selector": "testToken", - "scope": [ "entity.name.function.special" ] - }, - { - "selector": "*.testModifier", - "light": { - "fontStyle": "bold" - }, - "dark": { - "fontStyle": "bold" - }, - "highContrast": { - "fontStyle": "bold" - } - } - ], - "productIconThemes": [ - { - "id": "Test Product Icons", - "label": "The Test Product Icon Theme", - "path": "./producticons/test-product-icon-theme.json", - "_watch": true - } - ] - } + "name": "vscode-colorize-tests", + "description": "Colorize tests for VS Code", + "version": "0.0.1", + "publisher": "vscode", + "license": "MIT", + "private": true, + "activationEvents": [ + "onLanguage:json" + ], + "main": "./out/colorizerTestMain", + "enableProposedApi": true, + "engines": { + "vscode": "*" + }, + "scripts": { + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json" + }, + "dependencies": { + "jsonc-parser": "2.2.1" + }, + "devDependencies": { + "@types/node": "^12.11.7", + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7", + "vscode": "1.1.5" + }, + "contributes": { + "semanticTokenTypes": [ + { + "id": "testToken", + "description": "A test token" + } + ], + "semanticTokenModifiers": [ + { + "id": "testModifier", + "description": "A test modifier" + } + ], + "semanticTokenScopes": [ + { + "scopes": { + "testToken": [ + "entity.name.function.special" + ] + } + } + ], + "productIconThemes": [ + { + "id": "Test Product Icons", + "label": "The Test Product Icon Theme", + "path": "./producticons/test-product-icon-theme.json", + "_watch": true + } + ] + } } diff --git a/extensions/vscode-colorize-tests/src/colorizerTestMain.ts b/extensions/vscode-colorize-tests/src/colorizerTestMain.ts index 079c77e8e8..c12e4af29e 100644 --- a/extensions/vscode-colorize-tests/src/colorizerTestMain.ts +++ b/extensions/vscode-colorize-tests/src/colorizerTestMain.ts @@ -56,7 +56,7 @@ export function activate(context: vscode.ExtensionContext): any { }; jsoncParser.visit(document.getText(), visitor); - return new vscode.SemanticTokens(builder.build()); + return builder.build(); } }; diff --git a/package.json b/package.json index 79df1d81f0..1008d08c25 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "jquery": "3.4.0", "jschardet": "2.1.1", "keytar": "^4.11.0", + "minimist": "^1.2.5", "native-is-elevated": "0.4.1", "native-keymap": "2.1.1", "native-watchdog": "1.3.0", @@ -72,7 +73,6 @@ "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", "v8-inspect-profiler": "^0.0.20", - "vscode-minimist": "^1.2.2", "vscode-nsfw": "1.2.8", "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.8", @@ -98,6 +98,7 @@ "@types/http-proxy-agent": "^2.0.1", "@types/iconv-lite": "0.0.1", "@types/keytar": "^4.4.0", + "@types/minimist": "^1.2.0", "@types/mocha": "2.2.39", "@types/node": "^12.11.7", "@types/plotly.js": "^1.44.9", diff --git a/remote/package.json b/remote/package.json index f1976985a7..25220035ee 100644 --- a/remote/package.json +++ b/remote/package.json @@ -23,6 +23,7 @@ "iconv-lite": "0.5.0", "jquery": "3.4.0", "jschardet": "2.1.1", + "minimist": "^1.2.5", "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", "node-pty": "^0.10.0-beta2", @@ -33,7 +34,6 @@ "semver-umd": "^5.5.5", "slickgrid": "github:anthonydresser/SlickGrid#2.3.32", "spdlog": "^0.11.1", - "vscode-minimist": "^1.2.2", "vscode-nsfw": "1.2.8", "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.8", diff --git a/remote/yarn.lock b/remote/yarn.lock index faf4f46b9f..761cdd74b3 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -454,6 +454,11 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -695,11 +700,6 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -vscode-minimist@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/vscode-minimist/-/vscode-minimist-1.2.2.tgz#65403f44f0c6010d259b2271d36eb5c6f4ad8aab" - integrity sha512-DXMNG2QgrXn1jOP12LzjVfvxVkzxv/0Qa27JrMBj/XP2esj+fJ/wP2T4YUH5derj73Lc96dC8F25WyfDUbTpxQ== - vscode-nsfw@1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-1.2.8.tgz#1bf452e72ff1304934de63692870d039a2d972af" diff --git a/scripts/code-web.js b/scripts/code-web.js index 23bd232903..b7441ea015 100755 --- a/scripts/code-web.js +++ b/scripts/code-web.js @@ -13,7 +13,7 @@ const fs = require('fs'); const path = require('path'); const util = require('util'); const opn = require('opn'); -const minimist = require('vscode-minimist'); +const minimist = require('minimist'); const APP_ROOT = path.dirname(__dirname); const EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions'); diff --git a/scripts/code.sh b/scripts/code.sh index 0afe96bfcb..3bc09f54f4 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -55,6 +55,9 @@ function code() { function code-wsl() { + HOST_IP=$(powershell.exe -Command "& {(Get-NetIPAddress | Where-Object {\$_.InterfaceAlias -like '*WSL*' -and \$_.AddressFamily -eq 'IPv4'}).IPAddress | Write-Host -NoNewline}") + export DISPLAY="$HOST_IP:0" + # in a wsl shell ELECTRON="$ROOT/.build/electron/Code - OSS.exe" if [ -f "$ELECTRON" ]; then diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 422168128a..c92f1c79ce 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -142,13 +142,11 @@ function pipeLoggingToParent() { function handleExceptions() { // Handle uncaught exceptions - // @ts-ignore process.on('uncaughtException', function (err) { console.error('Uncaught Exception: ', err); }); // Handle unhandled promise rejections - // @ts-ignore process.on('unhandledRejection', function (reason) { console.error('Unhandled Promise Rejection: ', reason); }); diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index bcd6c4a5c9..4c0f153f69 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -25,7 +25,6 @@ exports.assign = function assign(destination, source) { */ exports.load = function (modulePaths, resultCallback, options) { - // @ts-ignore const webFrame = require('electron').webFrame; const path = require('path'); @@ -49,7 +48,6 @@ exports.load = function (modulePaths, resultCallback, options) { } // Error handler - // @ts-ignore process.on('uncaughtException', function (error) { onUnexpectedError(error, enableDeveloperTools); }); @@ -184,7 +182,6 @@ function parseURLQueryArgs() { */ function registerDeveloperKeybindings(disallowReloadKeybinding) { - // @ts-ignore const ipc = require('electron').ipcRenderer; const extractKey = function (e) { @@ -223,7 +220,6 @@ function registerDeveloperKeybindings(disallowReloadKeybinding) { function onUnexpectedError(error, enableDeveloperTools) { - // @ts-ignore const ipc = require('electron').ipcRenderer; if (enableDeveloperTools) { diff --git a/src/main.js b/src/main.js index e572053219..287e693b36 100644 --- a/src/main.js +++ b/src/main.js @@ -323,7 +323,7 @@ function getUserDataPath(cliArgs) { * @returns {ParsedArgs} */ function parseCLIArgs() { - const minimist = require('vscode-minimist'); + const minimist = require('minimist'); return minimist(process.argv, { string: [ diff --git a/src/paths.js b/src/paths.js index b49f0db52e..7453a528c6 100644 --- a/src/paths.js +++ b/src/paths.js @@ -6,7 +6,7 @@ //@ts-check 'use strict'; -// @ts-ignore +// @ts-expect-error // const pkg = require('../package.json'); const path = require('path'); const os = require('os'); @@ -34,4 +34,4 @@ function getDefaultUserDataPath(platform) { } exports.getAppDataPath = getAppDataPath; -exports.getDefaultUserDataPath = getDefaultUserDataPath; \ No newline at end of file +exports.getDefaultUserDataPath = getDefaultUserDataPath; diff --git a/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.ts b/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.ts index ad091d6357..fae4fe5f49 100644 --- a/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.ts @@ -17,7 +17,6 @@ import { TabChild } from 'sql/base/browser/ui/panel/tab.component'; import { Event, Emitter } from 'vs/base/common/event'; import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive'; import { values } from 'vs/base/common/collections'; -import { fill } from 'vs/base/common/arrays'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; export interface GridCellConfig { @@ -243,7 +242,7 @@ export class DashboardGridContainer extends DashboardTab implements OnDestroy { private createIndexes(indexes: number[]) { const max = Math.max(...indexes) + 1; - return fill(max, 0).map((x, i) => i); + return new Array(max).fill(0).map((x, i) => i); } ngOnDestroy() { diff --git a/src/sql/workbench/contrib/jobManagement/browser/jobsView.component.ts b/src/sql/workbench/contrib/jobManagement/browser/jobsView.component.ts index c5de79e7b0..321782e1a2 100644 --- a/src/sql/workbench/contrib/jobManagement/browser/jobsView.component.ts +++ b/src/sql/workbench/contrib/jobManagement/browser/jobsView.component.ts @@ -32,7 +32,7 @@ import { tableBackground, cellBackground, cellBorderColor } from 'sql/platform/t import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { attachButtonStyler } from 'sql/platform/theme/common/styler'; -import { find, fill } from 'vs/base/common/arrays'; +import { find } from 'vs/base/common/arrays'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -672,7 +672,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe // if the durations are all 0 secs, show minimal chart // instead of nothing if (zeroDurationJobCount === jobHistories.length) { - return fill(jobHistories.length, '5px'); + return new Array(jobHistories.length).fill('5px'); } else { return chartHeights; } diff --git a/src/sql/workbench/contrib/jobManagement/browser/notebooksView.component.ts b/src/sql/workbench/contrib/jobManagement/browser/notebooksView.component.ts index 4e531b47fb..cc64fc9d8b 100644 --- a/src/sql/workbench/contrib/jobManagement/browser/notebooksView.component.ts +++ b/src/sql/workbench/contrib/jobManagement/browser/notebooksView.component.ts @@ -33,7 +33,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { attachButtonStyler } from 'sql/platform/theme/common/styler'; import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; -import { find, fill } from 'vs/base/common/arrays'; +import { find } from 'vs/base/common/arrays'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; @@ -731,7 +731,7 @@ export class NotebooksViewComponent extends JobManagementView implements OnInit, // if the durations are all 0 secs, show minimal chart // instead of nothing if (zeroDurationJobCount === jobHistories.length) { - return fill(jobHistories.length, '5px'); + return new Array(jobHistories.length).fill('5px'); } else { return chartHeights; } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index ced2bd342f..5d6b170186 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?70edf2c63384951357ed8517386759dd") format("truetype"); + src: url("./codicon.ttf?a76e99e42eab7c1a55601640b708d820") format("truetype"); } .codicon[class*='codicon-'] { @@ -416,11 +416,12 @@ .codicon-feedback:before { content: "\eb96" } .codicon-group-by-ref-type:before { content: "\eb97" } .codicon-ungroup-by-ref-type:before { content: "\eb98" } -.codicon-account:before { content: "\f101" } -.codicon-bell-dot:before { content: "\f102" } -.codicon-debug-alt-2:before { content: "\f103" } -.codicon-debug-alt:before { content: "\f104" } -.codicon-debug-console:before { content: "\f105" } -.codicon-library:before { content: "\f106" } -.codicon-output:before { content: "\f107" } -.codicon-run-all:before { content: "\f108" } +.codicon-account:before { content: "\eb99" } +.codicon-bell-dot:before { content: "\eb9a" } +.codicon-debug-console:before { content: "\eb9b" } +.codicon-library:before { content: "\eb9c" } +.codicon-output:before { content: "\eb9d" } +.codicon-run-all:before { content: "\eb9e" } +.codicon-sync-ignored:before { content: "\eb9f" } +.codicon-debug-alt-2:before { content: "\f101" } +.codicon-debug-alt:before { content: "\f102" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index 7f910daafb298eaa4bbc844cf5cabb274732260a..d4358ace622faafdb62ed3b4408be0dd02db012a 100644 GIT binary patch delta 1261 zcmYL|drVtZ9LK-EdrK>Ab-0-o7>`03<<-{)bb`^COgG8KtF&b#>Kb5Ul|qF&LEY*K z$@T}ys+TNtqT>&YZZ0OKL^DG&=7TsTO30@1k@(1T8#fY}7zu>w@9zBJZ9e^+bMEQs zJ@@-Ne|{>D&B#ex{A++l00i12q392d)-M44EU;n12gY%3v?aO-R$md5B06?i~?Dw0qJlgG!T{inBn`G zJjvPb{$zo8l>kKQLKHAgGDS0$bDxqegC%H0(q-G+R(Rlzm_ z@(OekKN~XmBLfR37Dg7XsP$@6J-Z|+83s?oVZJM496#Yl3}cRW@d^IHB-T(#+pvgb z{Dm-zaRnia;sP9GK>+uW2QjqcJ6ys=d_)B}i6b~pTT)5GhvdzoY|5cr%A zh_+HO*{PJusGKUuK~8d!o3>LWRZ%s0X$L*198E8^l<)>N;4!?3Y22XAs6j1nTCZGA zzoSf3nsUjgPrs_Y3m&aizB2kKQzz_h2L$9`4+_Z1enCJk_KN~?vtJU>cI<+BggXpX zvX2U=iX9hFHTz`&dD&dT8nlC*5O73gnQQS9?!6|^s!cs3;MkzPF5m#6@&VA`D51U~ z;83AzeFit!X9XNImUt}VPX zv}va4*XEtg6TUXznD0H`v~S*by`{e8RLi2j&_57BpgC|NFdNJahJ(rAWN^8)r1gC3 z?7oS8D{YS>wYH^rJkE!M&%tkU6!*gh&W7v{bNO1l7K6?1wKygHCX3!F*$sAYJ{jzJ zGFPmD0&f48lM!8=E8$MK_R7KJTh~1P%Wrq2?ePq`hTKo+8xyDRWS2{~%4Mm-YkKlR zXPx`lu)Dr_q(NSP*XhvhpV+_GouFZ}yKYaS-Y5+^*6W-p>s0z?s!5g2_eqjen;N@% Z+0K|15JH-G(5_v(^nq+vS=DV9i>gcAf`=x^ z^q~)p@J51!kqBwrBZ$y6E+I`^A}(FT#5D;1<2*Q%-~7&*nRDjMeE*&s+Q;kKL088E zfSmyRp-3>=-uG%Uka!aqd2v@%S%EEkd<4H+l> zULh|WiM3t5EIsfgknpm(H57Edo7V+o><4VINU$wx^WlcvcgjOKEx|}*?(wm6b^`b0 z1-6uE>-tzjeytzqw*$i)5B0ic*MQc7KylOEAIsBbenG1C#(Kw-qoI@k{pW}FUVAHJ zJ8affX)=d3dNxnfZuIecf_^7)ROQ!y%fa>$+P_|*I0ewYBEfcDCJmQE0|xgGb`IW( zFN`0Icbj&7bW&MeSng^#f~Oe39`tbxuHl>fm7l0!89v}MKA{P2+(HmtIDsOj!;fJs zMGPT4#wDD`1?J!YwqYN~8eK`}SnXs6GnvJ4%;tFJaspk<<3zfd&jL>76c)0G#Vlbt zr?P^TtYS5%>AR8(Mof|qNW*l@z12L;bo5A>?o04^f>BLIwXy_e4Yg}#H$s|6gMfDC0?VTBpz>8Fk2i^aJ;xh z!CY~xf>JT@sDdu>S_SjO>wpgH#ECLkub^8TQ!rn=LBRs?Mg^rz(num: number, value: T, arr: T[] = []): T[] { - for (let i = 0; i < num; i++) { - arr[i] = value; - } - - return arr; -} - export function index(array: ReadonlyArray, indexer: (t: T) => string): { [key: string]: T; }; export function index(array: ReadonlyArray, indexer: (t: T) => string, merger?: (t: T, r: R) => R): { [key: string]: R; }; export function index(array: ReadonlyArray, indexer: (t: T) => string, merger: (t: T, r: R) => R = t => t as any): { [key: string]: R; } { diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index e412009eeb..05df43204e 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -94,8 +94,14 @@ export class VSBuffer { return new VSBuffer(this.buffer.subarray(start!/*bad lib.d.ts*/, end)); } - set(array: VSBuffer, offset?: number): void { - this.buffer.set(array.buffer, offset); + set(array: VSBuffer, offset?: number): void; + set(array: Uint8Array, offset?: number): void; + set(array: VSBuffer | Uint8Array, offset?: number): void { + if (array instanceof VSBuffer) { + this.buffer.set(array.buffer, offset); + } else { + this.buffer.set(array, offset); + } } readUInt32BE(offset: number): number { @@ -106,6 +112,14 @@ export class VSBuffer { writeUInt32BE(this.buffer, value, offset); } + readUInt32LE(offset: number): number { + return readUInt32LE(this.buffer, offset); + } + + writeUInt32LE(value: number, offset: number): void { + writeUInt32LE(this.buffer, value, offset); + } + readUInt8(offset: number): number { return readUInt8(this.buffer, offset); } @@ -117,15 +131,15 @@ export class VSBuffer { export function readUInt16LE(source: Uint8Array, offset: number): number { return ( - source[offset] - + source[offset + 1] * 2 ** 8 + ((source[offset + 0] << 0) >>> 0) | + ((source[offset + 1] << 8) >>> 0) ); } export function writeUInt16LE(destination: Uint8Array, value: number, offset: number): void { - destination[offset] = value; + destination[offset + 0] = (value & 0b11111111); value = value >>> 8; - destination[offset + 1] = value; + destination[offset + 1] = (value & 0b11111111); } export function readUInt32BE(source: Uint8Array, offset: number): number { @@ -147,6 +161,25 @@ export function writeUInt32BE(destination: Uint8Array, value: number, offset: nu destination[offset] = value; } +export function readUInt32LE(source: Uint8Array, offset: number): number { + return ( + ((source[offset + 0] << 0) >>> 0) | + ((source[offset + 1] << 8) >>> 0) | + ((source[offset + 2] << 16) >>> 0) | + ((source[offset + 3] << 24) >>> 0) + ); +} + +export function writeUInt32LE(destination: Uint8Array, value: number, offset: number): void { + destination[offset + 0] = (value & 0b11111111); + value = value >>> 8; + destination[offset + 1] = (value & 0b11111111); + value = value >>> 8; + destination[offset + 2] = (value & 0b11111111); + value = value >>> 8; + destination[offset + 3] = (value & 0b11111111); +} + export function readUInt8(source: Uint8Array, offset: number): number { return source[offset]; } diff --git a/src/vs/base/common/comparers.ts b/src/vs/base/common/comparers.ts index 377d61e3ed..367eb79508 100644 --- a/src/vs/base/common/comparers.ts +++ b/src/vs/base/common/comparers.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as strings from 'vs/base/common/strings'; import { sep } from 'vs/base/common/path'; import { IdleValue } from 'vs/base/common/async'; @@ -133,8 +132,8 @@ export function compareAnything(one: string, other: string, lookFor: string): nu } // Sort suffix matches over non suffix matches - const elementASuffixMatch = strings.endsWith(elementAName, lookFor); - const elementBSuffixMatch = strings.endsWith(elementBName, lookFor); + const elementASuffixMatch = elementAName.endsWith(lookFor); + const elementBSuffixMatch = elementBName.endsWith(lookFor); if (elementASuffixMatch !== elementBSuffixMatch) { return elementASuffixMatch ? -1 : 1; } @@ -154,8 +153,8 @@ export function compareByPrefix(one: string, other: string, lookFor: string): nu const elementBName = other.toLowerCase(); // Sort prefix matches over non prefix matches - const elementAPrefixMatch = strings.startsWith(elementAName, lookFor); - const elementBPrefixMatch = strings.startsWith(elementBName, lookFor); + const elementAPrefixMatch = elementAName.startsWith(lookFor); + const elementBPrefixMatch = elementBName.startsWith(lookFor); if (elementAPrefixMatch !== elementBPrefixMatch) { return elementAPrefixMatch ? -1 : 1; } diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index a64ad4df57..f4da5c246e 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isWindows } from 'vs/base/common/platform'; -import { startsWithIgnoreCase, equalsIgnoreCase, endsWith, rtrim } from 'vs/base/common/strings'; +import { startsWithIgnoreCase, equalsIgnoreCase, rtrim } from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; import { sep, posix, isAbsolute, join, normalize } from 'vs/base/common/path'; @@ -235,7 +235,7 @@ export function isWindowsDriveLetter(char0: number): boolean { export function sanitizeFilePath(candidate: string, cwd: string): string { // Special case: allow to open a drive letter without trailing backslash - if (isWindows && endsWith(candidate, ':')) { + if (isWindows && candidate.endsWith(':')) { candidate += sep; } @@ -252,7 +252,7 @@ export function sanitizeFilePath(candidate: string, cwd: string): string { candidate = rtrim(candidate, sep); // Special case: allow to open drive root ('C:\') - if (endsWith(candidate, ':')) { + if (candidate.endsWith(':')) { candidate += sep; } diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index 74485f0bee..290e05506f 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -25,15 +25,15 @@ export function score(target: string, query: IPreparedQuery, fuzzy: boolean): Sc return scoreMultiple(target, query.values, fuzzy); } - return scoreSingle(target, query.value, query.valueLowercase, fuzzy); + return scoreSingle(target, query.normalized, query.normalizedLowercase, fuzzy); } function scoreMultiple(target: string, query: IPreparedQueryPiece[], fuzzy: boolean): Score { let totalScore = NO_MATCH; const totalPositions: number[] = []; - for (const { value, valueLowercase } of query) { - const [scoreValue, positions] = scoreSingle(target, value, valueLowercase, fuzzy); + for (const { normalized, normalizedLowercase } of query) { + const [scoreValue, positions] = scoreSingle(target, normalized, normalizedLowercase, fuzzy); if (scoreValue === NO_MATCH) { // if a single query value does not match, return with // no score entirely, we require all queries to match @@ -338,11 +338,26 @@ const LABEL_CAMELCASE_SCORE = 1 << 16; const LABEL_SCORE_THRESHOLD = 1 << 15; export interface IPreparedQueryPiece { + + /** + * The original query as provided as input. + */ original: string; originalLowercase: string; - value: string; - valueLowercase: string; + /** + * Original normalized to platform separators: + * - Windows: \ + * - Posix: / + */ + pathNormalized: string; + + /** + * In addition to the normalized path, will have + * whitespace and wildcards removed. + */ + normalized: string; + normalizedLowercase: string; } export interface IPreparedQuery extends IPreparedQueryPiece { @@ -364,17 +379,21 @@ export function prepareQuery(original: string): IPreparedQuery { } const originalLowercase = original.toLowerCase(); - const value = prepareQueryValue(original); - const valueLowercase = value.toLowerCase(); - const containsPathSeparator = value.indexOf(sep) >= 0; + const { pathNormalized, normalized, normalizedLowercase } = normalizeQuery(original); + const containsPathSeparator = pathNormalized.indexOf(sep) >= 0; let values: IPreparedQueryPiece[] | undefined = undefined; const originalSplit = original.split(MULTIPL_QUERY_VALUES_SEPARATOR); if (originalSplit.length > 1) { for (const originalPiece of originalSplit) { - const valuePiece = prepareQueryValue(originalPiece); - if (valuePiece) { + const { + pathNormalized: pathNormalizedPiece, + normalized: normalizedPiece, + normalizedLowercase: normalizedLowercasePiece + } = normalizeQuery(originalPiece); + + if (normalizedPiece) { if (!values) { values = []; } @@ -382,29 +401,36 @@ export function prepareQuery(original: string): IPreparedQuery { values.push({ original: originalPiece, originalLowercase: originalPiece.toLowerCase(), - value: valuePiece, - valueLowercase: valuePiece.toLowerCase() + pathNormalized: pathNormalizedPiece, + normalized: normalizedPiece, + normalizedLowercase: normalizedLowercasePiece }); } } } - return { original, originalLowercase, value, valueLowercase, values, containsPathSeparator }; + return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator }; } -function prepareQueryValue(original: string): string { - let value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace +function normalizeQuery(original: string): { pathNormalized: string, normalized: string, normalizedLowercase: string } { + let pathNormalized: string; if (isWindows) { - value = value.replace(/\//g, sep); // Help Windows users to search for paths when using slash + pathNormalized = original.replace(/\//g, sep); // Help Windows users to search for paths when using slash } else { - value = value.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash + pathNormalized = original.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash } - return value; + const normalized = stripWildcards(pathNormalized).replace(/\s/g, ''); + + return { + pathNormalized, + normalized, + normalizedLowercase: normalized.toLowerCase() + }; } export function scoreItem(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: ScorerCache): IItemScore { - if (!item || !query.value) { + if (!item || !query.normalized) { return NO_ITEM_SCORE; // we need an item and query to score on at least } @@ -417,9 +443,9 @@ export function scoreItem(item: T, query: IPreparedQuery, fuzzy: boolean, acc let cacheHash: string; if (description) { - cacheHash = `${label}${description}${query.value}${fuzzy}`; + cacheHash = `${label}${description}${query.normalized}${fuzzy}`; } else { - cacheHash = `${label}${query.value}${fuzzy}`; + cacheHash = `${label}${query.normalized}${fuzzy}`; } const cached = cache[cacheHash]; @@ -455,7 +481,7 @@ function createMatches(offsets: undefined | number[]): IMatch[] { function doScoreItem(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore { // 1.) treat identity matches on full path highest - if (path && (isLinux ? query.original === path : equalsIgnoreCase(query.original, path))) { + if (path && (isLinux ? query.pathNormalized === path : equalsIgnoreCase(query.pathNormalized, path))) { return { score: PATH_IDENTITY_SCORE, labelMatch: [{ start: 0, end: label.length }], descriptionMatch: description ? [{ start: 0, end: description.length }] : undefined }; } @@ -464,13 +490,13 @@ function doScoreItem(label: string, description: string | undefined, path: strin if (preferLabelMatches) { // 2.) treat prefix matches on the label second highest - const prefixLabelMatch = matchesPrefix(query.value, label); + const prefixLabelMatch = matchesPrefix(query.normalized, label); if (prefixLabelMatch) { return { score: LABEL_PREFIX_SCORE, labelMatch: prefixLabelMatch }; } // 3.) treat camelcase matches on the label third highest - const camelcaseLabelMatch = matchesCamelCase(query.value, label); + const camelcaseLabelMatch = matchesCamelCase(query.normalized, label); if (camelcaseLabelMatch) { return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch }; } @@ -702,17 +728,17 @@ function fallbackCompare(itemA: T, itemB: T, query: IPreparedQuery, accessor: // compare by label if (labelA !== labelB) { - return compareAnything(labelA, labelB, query.value); + return compareAnything(labelA, labelB, query.normalized); } // compare by description if (descriptionA && descriptionB && descriptionA !== descriptionB) { - return compareAnything(descriptionA, descriptionB, query.value); + return compareAnything(descriptionA, descriptionB, query.normalized); } // compare by path if (pathA && pathB && pathA !== pathB) { - return compareAnything(pathA, pathB, query.value); + return compareAnything(pathA, pathB, query.normalized); } // equal diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index b4a4b8ed7b..464a7fdc92 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -302,7 +302,7 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P if (T1.test(pattern)) { // common pattern: **/*.txt just need endsWith check const base = pattern.substr(4); // '**/*'.length === 4 parsedPattern = function (path, basename) { - return typeof path === 'string' && strings.endsWith(path, base) ? pattern : null; + return typeof path === 'string' && path.endsWith(base) ? pattern : null; }; } else if (match = T2.exec(trimForExclusions(pattern, options))) { // common pattern: **/some.txt just need basename check parsedPattern = trivia2(match[1], pattern); @@ -339,7 +339,7 @@ function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | } function trimForExclusions(pattern: string, options: IGlobOptions): string { - return options.trimForExclusions && strings.endsWith(pattern, '/**') ? pattern.substr(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later + return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substr(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later } // common pattern: **/some.txt just need basename check @@ -353,7 +353,7 @@ function trivia2(base: string, originalPattern: string): ParsedStringPattern { if (basename) { return basename === base ? originalPattern : null; } - return path === base || strings.endsWith(path, slashBase) || strings.endsWith(path, backslashBase) ? originalPattern : null; + return path === base || path.endsWith(slashBase) || path.endsWith(backslashBase) ? originalPattern : null; }; const basenames = [base]; parsedPattern.basenames = basenames; @@ -398,7 +398,7 @@ function trivia4and5(path: string, pattern: string, matchPathEnds: boolean): Par const nativePath = paths.sep !== paths.posix.sep ? path.replace(ALL_FORWARD_SLASHES, paths.sep) : path; const nativePathEnd = paths.sep + nativePath; const parsedPattern: ParsedStringPattern = matchPathEnds ? function (path, basename) { - return typeof path === 'string' && (path === nativePath || strings.endsWith(path, nativePathEnd)) ? pattern : null; + return typeof path === 'string' && (path === nativePath || path.endsWith(nativePathEnd)) ? pattern : null; } : function (path, basename) { return typeof path === 'string' && path === nativePath ? pattern : null; }; diff --git a/src/vs/base/common/labels.ts b/src/vs/base/common/labels.ts index 05e6ab819c..56ca73d53c 100644 --- a/src/vs/base/common/labels.ts +++ b/src/vs/base/common/labels.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { posix, normalize, win32, sep } from 'vs/base/common/path'; -import { endsWith, startsWithIgnoreCase, rtrim, startsWith } from 'vs/base/common/strings'; +import { startsWithIgnoreCase, rtrim } from 'vs/base/common/strings'; import { Schemas } from 'vs/base/common/network'; import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform'; import { isEqual, basename, relativePath } from 'vs/base/common/resources'; @@ -117,7 +117,7 @@ export function tildify(path: string, userHome: string): string { } // Linux: case sensitive, macOS: case insensitive - if (isLinux ? startsWith(path, normalizedUserHome) : startsWithIgnoreCase(path, normalizedUserHome)) { + if (isLinux ? path.startsWith(normalizedUserHome) : startsWithIgnoreCase(path, normalizedUserHome)) { path = `~/${path.substr(normalizedUserHome.length)}`; } @@ -210,7 +210,7 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[] // Adding separator as prefix for subpath, such that 'endsWith(src, trgt)' considers subpath as directory name instead of plain string. // prefix is not added when either subpath is root directory or path[otherPathIndex] does not have multiple directories. const subpathWithSep: string = (start > 0 && paths[otherPathIndex].indexOf(pathSeparator) > -1) ? pathSeparator + subpath : subpath; - const isOtherPathEnding: boolean = endsWith(paths[otherPathIndex], subpathWithSep); + const isOtherPathEnding: boolean = paths[otherPathIndex].endsWith(subpathWithSep); match = !isSubpathEnding || isOtherPathEnding; } @@ -221,7 +221,7 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[] let result = ''; // preserve disk drive or root prefix - if (endsWith(segments[0], ':') || prefix !== '') { + if (segments[0].endsWith(':') || prefix !== '') { if (start === 1) { // extend subpath to include disk drive prefix start = 0; diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 18a08f0c41..d226c1a55c 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -56,32 +56,6 @@ export function setToString(set: Set): string { return `Set(${set.size}) {${entries.join(', ')}}`; } -/** - * @deprecated ES6: use `...Map.entries()` - */ -export function mapToSerializable(map: Map): [string, string][] { - const serializable: [string, string][] = []; - - map.forEach((value, key) => { - serializable.push([key, value]); - }); - - return serializable; -} - -/** - * @deprecated ES6: use `new Map([[key1, value1],[key2, value2]])` - */ -export function serializableToMap(serializable: [string, string][]): Map { - const items = new Map(); - - for (const [key, value] of serializable) { - items.set(key, value); - } - - return items; -} - export interface IKeyIterator { reset(key: string): this; next(): this; diff --git a/src/vs/base/common/mime.ts b/src/vs/base/common/mime.ts index a86aee3a97..14065f4401 100644 --- a/src/vs/base/common/mime.ts +++ b/src/vs/base/common/mime.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { basename, posix, extname } from 'vs/base/common/path'; -import { endsWith, startsWithUTF8BOM, startsWith } from 'vs/base/common/strings'; +import { startsWithUTF8BOM } from 'vs/base/common/strings'; import { coalesce } from 'vs/base/common/arrays'; import { match } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; @@ -185,7 +185,7 @@ function guessMimeTypeByPath(path: string, filename: string, associations: IText // Longest extension match if (association.extension) { if (!extensionMatch || association.extension.length > extensionMatch.extension!.length) { - if (endsWith(filename, association.extensionLowercase!)) { + if (filename.endsWith(association.extensionLowercase!)) { extensionMatch = association; } } @@ -259,11 +259,11 @@ export function suggestFilename(mode: string | undefined, prefix: string): strin .map(assoc => assoc.extension); const extensionsWithDotFirst = coalesce(extensions) - .filter(assoc => startsWith(assoc, '.')); + .filter(assoc => assoc.startsWith('.')); if (extensionsWithDotFirst.length > 0) { const candidateExtension = extensionsWithDotFirst[0]; - if (endsWith(prefix, candidateExtension)) { + if (prefix.endsWith(candidateExtension)) { // do not add the prefix if it already exists // https://github.com/microsoft/vscode/issues/83603 return prefix; diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 0cc0f0081a..2dc91be53f 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -588,11 +588,11 @@ function _makeFsPath(uri: URI, keepDriveLetterCasing: boolean): string { && (uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z || uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z) && uri.path.charCodeAt(2) === CharCode.Colon ) { - // windows drive letter: file:///c:/far/boo if (!keepDriveLetterCasing) { + // windows drive letter: file:///c:/far/boo value = uri.path[1].toLowerCase() + uri.path.substr(2); } else { - value = uri.path.substr(1, 2); + value = uri.path.substr(1); } } else { // other path diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index faa10d672d..ca751c2201 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -9,7 +9,6 @@ import * as fs from 'fs'; import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; -import { endsWith } from 'vs/base/common/strings'; import { promisify } from 'util'; import { isRootOrDriveLetter } from 'vs/base/common/extpath'; import { generateUuid } from 'vs/base/common/uuid'; @@ -492,7 +491,7 @@ export async function move(source: string, target: string): Promise { // // 2.) The user tries to rename a file/folder that ends with a dot. This is not // really possible to move then, at least on UNC devices. - if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || endsWith(source, '.')) { + if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) { await copy(source, target); await rimraf(source, RimRafMode.MOVE); await updateMtime(target); diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index fe8b61e2a9..dda4da8884 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -246,8 +246,8 @@ .quick-input-list .quick-input-list-entry-action-bar .action-label.codicon { margin: 0; - width: 19px; height: 100%; + padding: 0 2px; vertical-align: middle; } diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index aaf22e10c3..02c76ec63a 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -9,7 +9,6 @@ import { timeout } from 'vs/base/common/async'; import { mapToString, setToString } from 'vs/base/common/map'; import { basename } from 'vs/base/common/path'; import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs'; -import { fill } from 'vs/base/common/arrays'; import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; interface IDatabaseConnection { @@ -97,7 +96,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { }); keysValuesChunks.forEach(keysValuesChunk => { - this.prepare(connection, `INSERT INTO ItemTable VALUES ${fill(keysValuesChunk.length / 2, '(?,?)').join(',')}`, stmt => stmt.run(keysValuesChunk), () => { + this.prepare(connection, `INSERT INTO ItemTable VALUES ${new Array(keysValuesChunk.length / 2).fill('(?,?)').join(',')}`, stmt => stmt.run(keysValuesChunk), () => { const keys: string[] = []; let length = 0; toInsert.forEach((value, key) => { @@ -132,7 +131,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { }); keysChunks.forEach(keysChunk => { - this.prepare(connection, `DELETE FROM ItemTable WHERE key IN (${fill(keysChunk.length, '?').join(',')})`, stmt => stmt.run(keysChunk), () => { + this.prepare(connection, `DELETE FROM ItemTable WHERE key IN (${new Array(keysChunk.length).fill('?').join(',')})`, stmt => stmt.run(keysChunk), () => { const keys: string[] = []; toDelete.forEach(key => { keys.push(key); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 0ed3dca5c4..945e4d9d24 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -857,41 +857,58 @@ suite('Fuzzy Scorer', () => { }); test('prepareQuery', () => { - assert.equal(scorer.prepareQuery(' f*a ').value, 'fa'); + assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); assert.equal(scorer.prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); - assert.equal(scorer.prepareQuery('model Tester.ts').value, 'modelTester.ts'); - assert.equal(scorer.prepareQuery('Model Tester.ts').valueLowercase, 'modeltester.ts'); + assert.equal(scorer.prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); + assert.equal(scorer.prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); assert.equal(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false); assert.equal(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); // with spaces let query = scorer.prepareQuery('He*llo World'); assert.equal(query.original, 'He*llo World'); - assert.equal(query.value, 'HelloWorld'); - assert.equal(query.valueLowercase, 'HelloWorld'.toLowerCase()); + assert.equal(query.normalized, 'HelloWorld'); + assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); assert.equal(query.values?.length, 2); assert.equal(query.values?.[0].original, 'He*llo'); - assert.equal(query.values?.[0].value, 'Hello'); - assert.equal(query.values?.[0].valueLowercase, 'Hello'.toLowerCase()); + assert.equal(query.values?.[0].normalized, 'Hello'); + assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); assert.equal(query.values?.[1].original, 'World'); - assert.equal(query.values?.[1].value, 'World'); - assert.equal(query.values?.[1].valueLowercase, 'World'.toLowerCase()); + assert.equal(query.values?.[1].normalized, 'World'); + assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); // with spaces that are empty query = scorer.prepareQuery(' Hello World '); assert.equal(query.original, ' Hello World '); assert.equal(query.originalLowercase, ' Hello World '.toLowerCase()); - assert.equal(query.value, 'HelloWorld'); - assert.equal(query.valueLowercase, 'HelloWorld'.toLowerCase()); + assert.equal(query.normalized, 'HelloWorld'); + assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); assert.equal(query.values?.length, 2); assert.equal(query.values?.[0].original, 'Hello'); assert.equal(query.values?.[0].originalLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[0].value, 'Hello'); - assert.equal(query.values?.[0].valueLowercase, 'Hello'.toLowerCase()); + assert.equal(query.values?.[0].normalized, 'Hello'); + assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); assert.equal(query.values?.[1].original, 'World'); assert.equal(query.values?.[1].originalLowercase, 'World'.toLowerCase()); - assert.equal(query.values?.[1].value, 'World'); - assert.equal(query.values?.[1].valueLowercase, 'World'.toLowerCase()); + assert.equal(query.values?.[1].normalized, 'World'); + assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); + + // Path related + if (isWindows) { + assert.equal(scorer.prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path'); + assert.equal(scorer.prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path'); + assert.equal(scorer.prepareQuery('C:\\some\\path').containsPathSeparator, true); + assert.equal(scorer.prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path'); + assert.equal(scorer.prepareQuery('C:/some/path').normalized, 'C:\\some\\path'); + assert.equal(scorer.prepareQuery('C:/some/path').containsPathSeparator, true); + } else { + assert.equal(scorer.prepareQuery('/some/path').pathNormalized, '/some/path'); + assert.equal(scorer.prepareQuery('/some/path').normalized, '/some/path'); + assert.equal(scorer.prepareQuery('/some/path').containsPathSeparator, true); + assert.equal(scorer.prepareQuery('\\some\\path').pathNormalized, '/some/path'); + assert.equal(scorer.prepareQuery('\\some\\path').normalized, '/some/path'); + assert.equal(scorer.prepareQuery('\\some\\path').containsPathSeparator, true); + } }); }); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index fbc0bf5d2c..a53e145974 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache, mapToSerializable, serializableToMap } from 'vs/base/common/map'; +import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache } from 'vs/base/common/map'; import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; @@ -629,17 +629,4 @@ suite('Map', () => { // assert.equal(map.get(windowsFile), 'true'); // assert.equal(map.get(uncFile), 'true'); // }); - - test('mapToSerializable / serializableToMap', function () { - const map = new Map(); - map.set('1', 'foo'); - map.set('2', null!); - map.set('3', 'bar'); - - const map2 = serializableToMap(mapToSerializable(map)); - assert.equal(map2.size, map.size); - assert.equal(map2.get('1'), map.get('1')); - assert.equal(map2.get('2'), map.get('2')); - assert.equal(map2.get('3'), map.get('3')); - }); }); diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index 1e220c2095..a20b13af55 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -563,5 +563,8 @@ suite('URI', () => { assertJoined(('file://ser/foo/'), '../../bazz', 'file://ser/foo/bazz', false); assertJoined(('file://ser/foo'), '../../bazz', 'file://ser/foo/bazz', false); + + //https://github.com/microsoft/vscode/issues/93831 + assertJoined('file:///c:/foo/bar', './other/foo.img', 'file:///c:/foo/bar/other/foo.img', false); }); }); diff --git a/src/vs/base/test/node/path.test.ts b/src/vs/base/test/node/path.test.ts index 1b25fbe018..bf8065d3cc 100644 --- a/src/vs/base/test/node/path.test.ts +++ b/src/vs/base/test/node/path.test.ts @@ -401,9 +401,9 @@ suite('Paths (Node Implementation)', () => { ]; resolveTests.forEach((test) => { const resolve = test[0]; - //@ts-ignore + //@ts-expect-error test[1].forEach((test) => { - //@ts-ignore + //@ts-expect-error const actual = resolve.apply(null, test[0]); let actualAlt; const os = resolve === path.win32.resolve ? 'win32' : 'posix'; @@ -579,9 +579,9 @@ suite('Paths (Node Implementation)', () => { ]; relativeTests.forEach((test) => { const relative = test[0]; - //@ts-ignore + //@ts-expect-error test[1].forEach((test) => { - //@ts-ignore + //@ts-expect-error const actual = relative(test[0], test[1]); const expected = test[2]; const os = relative === path.win32.relative ? 'win32' : 'posix'; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 560bf444a5..f986dfd08a 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -61,7 +61,6 @@ import { Schemas } from 'vs/base/common/network'; import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap'; import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService'; import { GlobalStorageDatabaseChannel } from 'vs/platform/storage/node/storageIpc'; -import { startsWith } from 'vs/base/common/strings'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; @@ -178,7 +177,7 @@ export class CodeApplication extends Disposable { const srcUri = URI.parse(source).fsPath.toLowerCase(); const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase(); - return startsWith(srcUri, rootUri + sep); + return srcUri.startsWith(rootUri + sep); }; // Ensure defaults diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 796712af27..ab86c4d913 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -6,7 +6,7 @@ import * as path from 'vs/base/common/path'; import * as objects from 'vs/base/common/objects'; import * as nls from 'vs/nls'; -import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme } from 'electron'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; @@ -26,7 +26,6 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import * as perf from 'vs/base/common/performance'; import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { endsWith } from 'vs/base/common/strings'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; @@ -69,13 +68,13 @@ export class CodeWindow extends Disposable implements ICodeWindow { private static readonly MAX_URL_LENGTH = 2 * 1024 * 1024; // https://cs.chromium.org/chromium/src/url/url_constants.cc?l=32 private readonly _onClose = this._register(new Emitter()); - readonly onClose: CommonEvent = this._onClose.event; + readonly onClose = this._onClose.event; private readonly _onDestroy = this._register(new Emitter()); - readonly onDestroy: CommonEvent = this._onDestroy.event; + readonly onDestroy = this._onDestroy.event; private readonly _onLoad = this._register(new Emitter()); - readonly onLoad: CommonEvent = this._onLoad.event; + readonly onLoad = this._onLoad.event; private hiddenTitleBarStyle: boolean | undefined; private showTimeoutHandle: NodeJS.Timeout | undefined; @@ -83,7 +82,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { private _readyState: ReadyState; private windowState: IWindowState; private currentMenuBarVisibility: MenuBarVisibility | undefined; + private representedFilename: string | undefined; + private documentEdited: boolean | undefined; private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[]; @@ -271,6 +272,22 @@ export class CodeWindow extends Disposable implements ICodeWindow { return this.representedFilename; } + setDocumentEdited(edited: boolean): void { + if (isMacintosh) { + this._win.setDocumentEdited(edited); + } + + this.documentEdited = edited; + } + + isDocumentEdited(): boolean { + if (isMacintosh) { + return this._win.isDocumentEdited(); + } + + return !!this.documentEdited; + } + focus(): void { if (!this._win) { return; @@ -349,7 +366,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => { if (details.url.indexOf('.svg') > 0) { const uri = URI.parse(details.url); - if (uri && !uri.scheme.match(/file/i) && endsWith(uri.path, '.svg')) { + if (uri && !uri.scheme.match(/file/i) && uri.path.endsWith('.svg')) { return callback({ cancel: true }); } } @@ -586,9 +603,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Clear Document Edited if needed - if (isMacintosh && this._win.isDocumentEdited()) { + if (this.isDocumentEdited()) { if (!isReload || !this.backupMainService.isHotExitEnabled()) { - this._win.setDocumentEdited(false); + this.setDocumentEdited(false); } } diff --git a/src/vs/css.build.js b/src/vs/css.build.js index 880ffc57b1..d0b3465487 100644 --- a/src/vs/css.build.js +++ b/src/vs/css.build.js @@ -53,7 +53,7 @@ var CSSBuildLoaderPlugin; BrowserCSSLoader.prototype._insertLinkNode = function (linkNode) { this._pendingLoads++; var head = document.head || document.getElementsByTagName('head')[0]; - var other = head.getElementsByTagName('link') || document.head.getElementsByTagName('script'); + var other = head.getElementsByTagName('link') || head.getElementsByTagName('script'); if (other.length > 0) { head.insertBefore(linkNode, other[other.length - 1]); } diff --git a/src/vs/css.js b/src/vs/css.js index 5c29d2e5b1..d4ed3636a2 100644 --- a/src/vs/css.js +++ b/src/vs/css.js @@ -51,7 +51,7 @@ var CSSLoaderPlugin; BrowserCSSLoader.prototype._insertLinkNode = function (linkNode) { this._pendingLoads++; var head = document.head || document.getElementsByTagName('head')[0]; - var other = head.getElementsByTagName('link') || document.head.getElementsByTagName('script'); + var other = head.getElementsByTagName('link') || head.getElementsByTagName('script'); if (other.length > 0) { head.insertBefore(linkNode, other[other.length - 1]); } diff --git a/src/vs/editor/browser/config/elementSizeObserver.ts b/src/vs/editor/browser/config/elementSizeObserver.ts index d285162fd4..ca4dea738d 100644 --- a/src/vs/editor/browser/config/elementSizeObserver.ts +++ b/src/vs/editor/browser/config/elementSizeObserver.ts @@ -3,9 +3,34 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IDimension } from 'vs/editor/common/editorCommon'; -import * as dom from 'vs/base/browser/dom'; + +interface ResizeObserver { + observe(target: Element): void; + unobserve(target: Element): void; + disconnect(): void; +} + +interface ResizeObserverSize { + inlineSize: number; + blockSize: number; +} + +interface ResizeObserverEntry { + readonly target: Element; + readonly contentRect: DOMRectReadOnly; + readonly borderBoxSize: ResizeObserverSize; + readonly contentBoxSize: ResizeObserverSize; +} + +type ResizeObserverCallback = (entries: ReadonlyArray, observer: ResizeObserver) => void; + +declare const ResizeObserver: { + prototype: ResizeObserver; + new(callback: ResizeObserverCallback): ResizeObserver; +}; + export class ElementSizeObserver extends Disposable { @@ -13,8 +38,8 @@ export class ElementSizeObserver extends Disposable { private readonly changeCallback: () => void; private width: number; private height: number; - private mutationObserver: MutationObserver | null; - private windowSizeListener: IDisposable | null; + private resizeObserver: ResizeObserver | null; + private measureReferenceDomElementToken: number; constructor(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined, changeCallback: () => void) { super(); @@ -22,8 +47,8 @@ export class ElementSizeObserver extends Disposable { this.changeCallback = changeCallback; this.width = -1; this.height = -1; - this.mutationObserver = null; - this.windowSizeListener = null; + this.resizeObserver = null; + this.measureReferenceDomElementToken = -1; this.measureReferenceDomElement(false, dimension); } @@ -41,25 +66,33 @@ export class ElementSizeObserver extends Disposable { } public startObserving(): void { - if (!this.mutationObserver && this.referenceDomElement) { - this.mutationObserver = new MutationObserver(() => this._onDidMutate()); - this.mutationObserver.observe(this.referenceDomElement, { - attributes: true, - }); - } - if (!this.windowSizeListener) { - this.windowSizeListener = dom.addDisposableListener(window, 'resize', () => this._onDidResizeWindow()); + if (typeof ResizeObserver !== 'undefined') { + if (!this.resizeObserver && this.referenceDomElement) { + this.resizeObserver = new ResizeObserver((entries) => { + if (entries && entries[0] && entries[0].contentRect) { + this.observe({ width: entries[0].contentRect.width, height: entries[0].contentRect.height }); + } else { + this.observe(); + } + }); + this.resizeObserver.observe(this.referenceDomElement); + } + } else { + if (this.measureReferenceDomElementToken === -1) { + // setInterval type defaults to NodeJS.Timeout instead of number, so specify it as a number + this.measureReferenceDomElementToken = setInterval(() => this.observe(), 100); + } } } public stopObserving(): void { - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - this.mutationObserver = null; + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; } - if (this.windowSizeListener) { - this.windowSizeListener.dispose(); - this.windowSizeListener = null; + if (this.measureReferenceDomElementToken !== -1) { + clearInterval(this.measureReferenceDomElementToken); + this.measureReferenceDomElementToken = -1; } } @@ -67,14 +100,6 @@ export class ElementSizeObserver extends Disposable { this.measureReferenceDomElement(true, dimension); } - private _onDidMutate(): void { - this.measureReferenceDomElement(true); - } - - private _onDidResizeWindow(): void { - this.measureReferenceDomElement(true); - } - private measureReferenceDomElement(callChangeCallback: boolean, dimension?: IDimension): void { let observedWidth = 0; let observedHeight = 0; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 5b7413ed77..889ab9de9a 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2722,10 +2722,6 @@ export interface ISuggestOptions { * Overwrite word ends on accept. Default to false. */ insertMode?: 'insert' | 'replace'; - /** - * Show a highlight when suggestion replaces or keep text after the cursor. Defaults to false. - */ - insertHighlight?: boolean; /** * Enable graceful matching. Defaults to true. */ @@ -2876,7 +2872,6 @@ class EditorSuggest extends BaseEditorOption IStringBuilder; export let decodeUTF16LE: (source: Uint8Array, offset: number, len: number) => string; -if (typeof TextDecoder !== 'undefined') { +if (hasTextDecoder) { createStringBuilder = (capacity) => new StringBuilder(capacity); decodeUTF16LE = standardDecodeUTF16LE; } else { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index cb8f0f5cb5..14aba0c709 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -827,7 +827,17 @@ export interface ITextModel { /** * @internal */ - setSemanticTokens(tokens: MultilineTokens2[] | null): void; + setSemanticTokens(tokens: MultilineTokens2[] | null, isComplete: boolean): void; + + /** + * @internal + */ + setPartialSemanticTokens(range: Range, tokens: MultilineTokens2[] | null): void; + + /** + * @internal + */ + hasSemanticTokens(): boolean; /** * Flush all tokenization state. diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index daf740d1ff..63eef8f45e 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -10,10 +10,13 @@ import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation import { TextModel } from 'vs/editor/common/model/textModel'; import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; -import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange'; import * as buffer from 'vs/base/common/buffer'; +function uriGetComparisonKey(resource: URI): string { + return resource.toString(); +} + class SingleModelEditStackData { public static create(model: ITextModel, beforeCursorState: Selection[] | null): SingleModelEditStackData { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index f7632eb374..ee87bcf586 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1793,8 +1793,8 @@ export class TextModel extends Disposable implements model.ITextModel { } } - public setSemanticTokens(tokens: MultilineTokens2[] | null): void { - this._tokens2.set(tokens); + public setSemanticTokens(tokens: MultilineTokens2[] | null, isComplete: boolean): void { + this._tokens2.set(tokens, isComplete); this._emitModelTokensChangedEvent({ tokenizationSupportChanged: false, @@ -1803,6 +1803,23 @@ export class TextModel extends Disposable implements model.ITextModel { }); } + public hasSemanticTokens(): boolean { + return this._tokens2.isComplete(); + } + + public setPartialSemanticTokens(range: Range, tokens: MultilineTokens2[]): void { + if (this.hasSemanticTokens()) { + return; + } + const changedRange = this._tokens2.setPartial(range, tokens); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: true, + ranges: [{ fromLineNumber: changedRange.startLineNumber, toLineNumber: changedRange.endLineNumber }] + }); + } + public tokenizeViewport(startLineNumber: number, endLineNumber: number): void { startLineNumber = Math.max(1, startLineNumber); endLineNumber = Math.min(this._buffer.getLineCount(), endLineNumber); diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index e9afd106e0..92131cf7bf 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -6,7 +6,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 } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer'; import { CharCode } from 'vs/base/common/charCode'; @@ -124,20 +124,7 @@ export class MultilineTokensBuilder { } } -export interface IEncodedTokens { - getTokenCount(): number; - getDeltaLine(tokenIndex: number): number; - getMaxDeltaLine(): number; - getStartCharacter(tokenIndex: number): number; - getEndCharacter(tokenIndex: number): number; - getMetadata(tokenIndex: number): number; - - clear(): void; - acceptDeleteRange(horizontalShiftForFirstLineTokens: number, startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void; - acceptInsertText(deltaLine: number, character: number, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void; -} - -export class SparseEncodedTokens implements IEncodedTokens { +export class SparseEncodedTokens { /** * The encoding of tokens is: * 4*i deltaLine (from `startLineNumber`) @@ -145,7 +132,7 @@ export class SparseEncodedTokens implements IEncodedTokens { * 4*i+2 endCharacter (from the line start) * 4*i+3 metadata */ - private _tokens: Uint32Array; + private readonly _tokens: Uint32Array; private _tokenCount: number; constructor(tokens: Uint32Array) { @@ -153,38 +140,167 @@ export class SparseEncodedTokens implements IEncodedTokens { this._tokenCount = tokens.length / 4; } + public toString(startLineNumber: number): string { + let pieces: string[] = []; + for (let i = 0; i < this._tokenCount; i++) { + pieces.push(`(${this._getDeltaLine(i) + startLineNumber},${this._getStartCharacter(i)}-${this._getEndCharacter(i)})`); + } + return `[${pieces.join(',')}]`; + } + public getMaxDeltaLine(): number { - const tokenCount = this.getTokenCount(); + const tokenCount = this._getTokenCount(); if (tokenCount === 0) { return -1; } - return this.getDeltaLine(tokenCount - 1); + return this._getDeltaLine(tokenCount - 1); } - public getTokenCount(): number { + public getRange(): Range | null { + const tokenCount = this._getTokenCount(); + if (tokenCount === 0) { + return null; + } + const startChar = this._getStartCharacter(0); + const maxDeltaLine = this._getDeltaLine(tokenCount - 1); + const endChar = this._getEndCharacter(tokenCount - 1); + return new Range(0, startChar + 1, maxDeltaLine, endChar + 1); + } + + private _getTokenCount(): number { return this._tokenCount; } - public getDeltaLine(tokenIndex: number): number { + private _getDeltaLine(tokenIndex: number): number { return this._tokens[4 * tokenIndex]; } - public getStartCharacter(tokenIndex: number): number { + private _getStartCharacter(tokenIndex: number): number { return this._tokens[4 * tokenIndex + 1]; } - public getEndCharacter(tokenIndex: number): number { + private _getEndCharacter(tokenIndex: number): number { return this._tokens[4 * tokenIndex + 2]; } - public getMetadata(tokenIndex: number): number { - return this._tokens[4 * tokenIndex + 3]; + public isEmpty(): boolean { + return (this._getTokenCount() === 0); + } + + public getLineTokens(deltaLine: number): LineTokens2 | null { + let low = 0; + let high = this._getTokenCount() - 1; + + while (low < high) { + const mid = low + Math.floor((high - low) / 2); + const midDeltaLine = this._getDeltaLine(mid); + + if (midDeltaLine < deltaLine) { + low = mid + 1; + } else if (midDeltaLine > deltaLine) { + high = mid - 1; + } else { + let min = mid; + while (min > low && this._getDeltaLine(min - 1) === deltaLine) { + min--; + } + let max = mid; + while (max < high && this._getDeltaLine(max + 1) === deltaLine) { + max++; + } + return new LineTokens2(this._tokens.subarray(4 * min, 4 * max + 4)); + } + } + + if (this._getDeltaLine(low) === deltaLine) { + return new LineTokens2(this._tokens.subarray(4 * low, 4 * low + 4)); + } + + return null; } public clear(): void { this._tokenCount = 0; } + public removeTokens(startDeltaLine: number, startChar: number, endDeltaLine: number, endChar: number): number { + const tokens = this._tokens; + const tokenCount = this._tokenCount; + let newTokenCount = 0; + let hasDeletedTokens = false; + let firstDeltaLine = 0; + for (let i = 0; i < tokenCount; i++) { + const srcOffset = 4 * i; + const tokenDeltaLine = tokens[srcOffset]; + const tokenStartCharacter = tokens[srcOffset + 1]; + const tokenEndCharacter = tokens[srcOffset + 2]; + const tokenMetadata = tokens[srcOffset + 3]; + + if ( + (tokenDeltaLine > startDeltaLine || (tokenDeltaLine === startDeltaLine && tokenEndCharacter >= startChar)) + && (tokenDeltaLine < endDeltaLine || (tokenDeltaLine === endDeltaLine && tokenStartCharacter <= endChar)) + ) { + hasDeletedTokens = true; + } else { + if (newTokenCount === 0) { + firstDeltaLine = tokenDeltaLine; + } + if (hasDeletedTokens) { + // must move the token to the left + const destOffset = 4 * newTokenCount; + tokens[destOffset] = tokenDeltaLine - firstDeltaLine; + tokens[destOffset + 1] = tokenStartCharacter; + tokens[destOffset + 2] = tokenEndCharacter; + tokens[destOffset + 3] = tokenMetadata; + } + newTokenCount++; + } + } + + this._tokenCount = newTokenCount; + + return firstDeltaLine; + } + + public split(startDeltaLine: number, startChar: number, endDeltaLine: number, endChar: number): [SparseEncodedTokens, SparseEncodedTokens, number] { + const tokens = this._tokens; + const tokenCount = this._tokenCount; + let aTokens: number[] = []; + let bTokens: number[] = []; + let destTokens: number[] = aTokens; + let destOffset = 0; + let destFirstDeltaLine: number = 0; + for (let i = 0; i < tokenCount; i++) { + const srcOffset = 4 * i; + const tokenDeltaLine = tokens[srcOffset]; + const tokenStartCharacter = tokens[srcOffset + 1]; + const tokenEndCharacter = tokens[srcOffset + 2]; + const tokenMetadata = tokens[srcOffset + 3]; + + if ((tokenDeltaLine > startDeltaLine || (tokenDeltaLine === startDeltaLine && tokenEndCharacter >= startChar))) { + if ((tokenDeltaLine < endDeltaLine || (tokenDeltaLine === endDeltaLine && tokenStartCharacter <= endChar))) { + // this token is touching the range + continue; + } else { + // this token is after the range + if (destTokens !== bTokens) { + // this token is the first token after the range + destTokens = bTokens; + destOffset = 0; + destFirstDeltaLine = tokenDeltaLine; + } + } + } + + destTokens[destOffset++] = tokenDeltaLine - destFirstDeltaLine; + destTokens[destOffset++] = tokenStartCharacter; + destTokens[destOffset++] = tokenEndCharacter; + destTokens[destOffset++] = tokenMetadata; + } + + return [new SparseEncodedTokens(new Uint32Array(aTokens)), new SparseEncodedTokens(new Uint32Array(bTokens)), destFirstDeltaLine]; + } + public acceptDeleteRange(horizontalShiftForFirstLineTokens: number, startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void { // This is a bit complex, here are the cases I used to think about this: // @@ -414,30 +530,26 @@ export class SparseEncodedTokens implements IEncodedTokens { export class LineTokens2 { - private readonly _actual: IEncodedTokens; - private readonly _startTokenIndex: number; - private readonly _endTokenIndex: number; + private readonly _tokens: Uint32Array; - constructor(actual: IEncodedTokens, startTokenIndex: number, endTokenIndex: number) { - this._actual = actual; - this._startTokenIndex = startTokenIndex; - this._endTokenIndex = endTokenIndex; + constructor(tokens: Uint32Array) { + this._tokens = tokens; } public getCount(): number { - return this._endTokenIndex - this._startTokenIndex + 1; + return this._tokens.length / 4; } public getStartCharacter(tokenIndex: number): number { - return this._actual.getStartCharacter(this._startTokenIndex + tokenIndex); + return this._tokens[4 * tokenIndex + 1]; } public getEndCharacter(tokenIndex: number): number { - return this._actual.getEndCharacter(this._startTokenIndex + tokenIndex); + return this._tokens[4 * tokenIndex + 2]; } public getMetadata(tokenIndex: number): number { - return this._actual.getMetadata(this._startTokenIndex + tokenIndex); + return this._tokens[4 * tokenIndex + 3]; } } @@ -445,59 +557,58 @@ export class MultilineTokens2 { public startLineNumber: number; public endLineNumber: number; - public tokens: IEncodedTokens; + public tokens: SparseEncodedTokens; - constructor(startLineNumber: number, tokens: IEncodedTokens) { + constructor(startLineNumber: number, tokens: SparseEncodedTokens) { this.startLineNumber = startLineNumber; this.tokens = tokens; this.endLineNumber = this.startLineNumber + this.tokens.getMaxDeltaLine(); } + public toString(): string { + return this.tokens.toString(this.startLineNumber); + } + private _updateEndLineNumber(): void { this.endLineNumber = this.startLineNumber + this.tokens.getMaxDeltaLine(); } + public isEmpty(): boolean { + return this.tokens.isEmpty(); + } + public getLineTokens(lineNumber: number): LineTokens2 | null { if (this.startLineNumber <= lineNumber && lineNumber <= this.endLineNumber) { - const findResult = MultilineTokens2._findTokensWithLine(this.tokens, lineNumber - this.startLineNumber); - if (findResult) { - const [startTokenIndex, endTokenIndex] = findResult; - return new LineTokens2(this.tokens, startTokenIndex, endTokenIndex); - } + return this.tokens.getLineTokens(lineNumber - this.startLineNumber); } return null; } - private static _findTokensWithLine(tokens: IEncodedTokens, deltaLine: number): [number, number] | null { - let low = 0; - let high = tokens.getTokenCount() - 1; - - while (low < high) { - const mid = low + Math.floor((high - low) / 2); - const midDeltaLine = tokens.getDeltaLine(mid); - - if (midDeltaLine < deltaLine) { - low = mid + 1; - } else if (midDeltaLine > deltaLine) { - high = mid - 1; - } else { - let min = mid; - while (min > low && tokens.getDeltaLine(min - 1) === deltaLine) { - min--; - } - let max = mid; - while (max < high && tokens.getDeltaLine(max + 1) === deltaLine) { - max++; - } - return [min, max]; - } + public getRange(): Range | null { + const deltaRange = this.tokens.getRange(); + if (!deltaRange) { + return deltaRange; } + return new Range(this.startLineNumber + deltaRange.startLineNumber, deltaRange.startColumn, this.startLineNumber + deltaRange.endLineNumber, deltaRange.endColumn); + } - if (tokens.getDeltaLine(low) === deltaLine) { - return [low, low]; - } + public removeTokens(range: Range): void { + const startLineIndex = range.startLineNumber - this.startLineNumber; + const endLineIndex = range.endLineNumber - this.startLineNumber; - return null; + this.startLineNumber += this.tokens.removeTokens(startLineIndex, range.startColumn - 1, endLineIndex, range.endColumn - 1); + this._updateEndLineNumber(); + } + + public split(range: Range): [MultilineTokens2, MultilineTokens2] { + // split tokens to two: + // a) all the tokens before `range` + // b) all the tokens after `range` + const startLineIndex = range.startLineNumber - this.startLineNumber; + const endLineIndex = range.endLineNumber - this.startLineNumber; + + const [a, b, bDeltaLine] = this.tokens.split(startLineIndex, range.startColumn - 1, endLineIndex, range.endColumn - 1); + return [new MultilineTokens2(this.startLineNumber, a), new MultilineTokens2(this.startLineNumber + bDeltaLine, b)]; } public applyEdit(range: IRange, text: string): void { @@ -761,17 +872,91 @@ function toUint32Array(arr: Uint32Array | ArrayBuffer): Uint32Array { export class TokensStore2 { private _pieces: MultilineTokens2[]; + private _isComplete: boolean; constructor() { this._pieces = []; + this._isComplete = false; } public flush(): void { this._pieces = []; + this._isComplete = false; } - public set(pieces: MultilineTokens2[] | null) { + public set(pieces: MultilineTokens2[] | null, isComplete: boolean): void { this._pieces = pieces || []; + this._isComplete = isComplete; + } + + public setPartial(_range: Range, pieces: MultilineTokens2[]): Range { + if (pieces.length === 0) { + return _range; + } + const _firstRange = pieces[0].getRange(); + const _lastRange = pieces[pieces.length - 1].getRange(); + if (!_firstRange || !_lastRange) { + return _range; + } + const range = _range.plusRange(_firstRange).plusRange(_lastRange); + let insertPosition: { index: number; } | null = null; + for (let i = 0, len = this._pieces.length; i < len; i++) { + const piece = this._pieces[i]; + if (piece.endLineNumber < range.startLineNumber) { + // this piece is before the range + continue; + } + + if (piece.startLineNumber > range.endLineNumber) { + // this piece is after the range, so mark the spot before this piece + // as a good insertion position and stop looping + insertPosition = insertPosition || { index: i }; + break; + } + + // this piece might intersect with the range + piece.removeTokens(range); + + if (piece.isEmpty()) { + // remove the piece if it became empty + this._pieces.splice(i, 1); + i--; + len--; + continue; + } + + if (piece.endLineNumber < range.startLineNumber) { + // after removal, this piece is before the range + continue; + } + + if (piece.startLineNumber > range.endLineNumber) { + // after removal, this piece is after the range + insertPosition = insertPosition || { index: i }; + continue; + } + + // after removal, this piece contains the range + const [a, b] = piece.split(range); + this._pieces.splice(i, 1, a, b); + i++; + len++; + + insertPosition = insertPosition || { index: i }; + } + + insertPosition = insertPosition || { index: this._pieces.length }; + + this._pieces = arrays.arrayInsert(this._pieces, insertPosition.index, pieces); + + // console.log(`I HAVE ${this._pieces.length} pieces`); + // console.log(`${this._pieces.map(p => p.toString()).join(', ')}`); + + return range; + } + + public isComplete(): boolean { + return this._isComplete; } public addSemanticTokens(lineNumber: number, aTokens: LineTokens): LineTokens { @@ -782,7 +967,7 @@ export class TokensStore2 { } const pieceIndex = TokensStore2._findFirstPieceWithLine(pieces, lineNumber); - const bTokens = this._pieces[pieceIndex].getLineTokens(lineNumber); + const bTokens = pieces[pieceIndex].getLineTokens(lineNumber); if (!bTokens) { return aTokens; diff --git a/src/vs/editor/common/modes/supports/richEditBrackets.ts b/src/vs/editor/common/modes/supports/richEditBrackets.ts index c13665b3c2..a62457a32e 100644 --- a/src/vs/editor/common/modes/supports/richEditBrackets.ts +++ b/src/vs/editor/common/modes/supports/richEditBrackets.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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'; @@ -264,14 +265,24 @@ function createBracketOrRegExp(pieces: string[]): RegExp { return strings.createRegExp(regexStr, true); } -let toReversedString = (function () { +const toReversedString = (function () { function reverse(str: string): string { - let reversedStr = ''; - for (let i = str.length - 1; i >= 0; i--) { - reversedStr += str.charAt(i); + if (stringBuilder.hasTextDecoder) { + // create a Uint16Array and then use a TextDecoder to create a string + const arr = new Uint16Array(str.length); + let offset = 0; + for (let i = str.length - 1; i >= 0; i--) { + arr[offset++] = str.charCodeAt(i); + } + return stringBuilder.getPlatformTextDecoder().decode(arr); + } else { + let result: string[] = [], resultLen = 0; + for (let i = str.length - 1; i >= 0; i--) { + result[resultLen++] = str.charAt(i); + } + return result.join(''); } - return reversedStr; } let lastInput: string | null = null; diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index d2d09a6f31..fed177b8b6 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -8,9 +8,13 @@ import { URI } from 'vs/base/common/uri'; import { ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; import { ILanguageSelection } from 'vs/editor/common/services/modeService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/modes'; +import { SemanticTokensProviderStyling } from 'vs/editor/common/services/semanticTokensProviderStyling'; export const IModelService = createDecorator('modelService'); +export type DocumentTokensProvider = DocumentSemanticTokensProvider | DocumentRangeSemanticTokensProvider; + export interface IModelService { _serviceBrand: undefined; @@ -28,6 +32,8 @@ export interface IModelService { getModel(resource: URI): ITextModel | null; + getSemanticTokensProviderStyling(provider: DocumentTokensProvider): SemanticTokensProviderStyling; + onModelAdded: Event; onModelRemoved: Event; diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 5338f9d530..3d1372e38c 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -12,26 +12,26 @@ import { URI } from 'vs/base/common/uri'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; -import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions, IValidEditOperation } from 'vs/editor/common/model'; +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, SemanticTokensLegend, SemanticTokens, SemanticTokensEdits, TokenMetadata, FontStyle, MetadataConsts } from 'vs/editor/common/modes'; +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 { IModelService } from 'vs/editor/common/services/modelService'; +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'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { SparseEncodedTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { ILogService } from 'vs/platform/log/common/log'; import { IUndoRedoService, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo'; import { StringSHA1 } from 'vs/base/common/hash'; import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement } from 'vs/editor/common/model/editStack'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Schemas } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; +import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; export const MAINTAIN_UNDO_REDO_STACK = true; @@ -171,6 +171,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { */ private readonly _models: { [modelId: string]: ModelData; }; private readonly _disposedModels: Map; + private readonly _semanticStyling: SemanticStyling; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -184,11 +185,12 @@ export class ModelServiceImpl extends Disposable implements IModelService { this._modelCreationOptionsByLanguageAndResource = Object.create(null); this._models = {}; this._disposedModels = new Map(); + this._semanticStyling = this._register(new SemanticStyling(this._themeService, this._logService)); - this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions())); + this._register(this._configurationService.onDidChangeConfiguration(() => this._updateModelOptions())); this._updateModelOptions(); - this._register(new SemanticColoringFeature(this, this._themeService, this._configurationService, this._logService)); + this._register(new SemanticColoringFeature(this, this._themeService, this._configurationService, this._semanticStyling)); } private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions { @@ -380,7 +382,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { model.pushEditOperations( [], ModelServiceImpl._computeEdits(model, textBuffer), - (inverseEditOperations: IValidEditOperation[]) => [] + () => [] ); model.pushStackElement(); } @@ -542,6 +544,10 @@ export class ModelServiceImpl extends Disposable implements IModelService { return modelData.model; } + public getSemanticTokensProviderStyling(provider: DocumentTokensProvider): SemanticTokensProviderStyling { + return this._semanticStyling.get(provider); + } + // --- end IModelService private _onWillDispose(model: ITextModel): void { @@ -575,13 +581,13 @@ class SemanticColoringFeature extends Disposable { private static readonly SETTING_ID = 'editor.semanticHighlighting'; - private _watchers: Record; - private _semanticStyling: SemanticStyling; + private readonly _watchers: Record; + private readonly _semanticStyling: SemanticStyling; - constructor(modelService: IModelService, themeService: IThemeService, configurationService: IConfigurationService, logService: ILogService) { + constructor(modelService: IModelService, themeService: IThemeService, configurationService: IConfigurationService, semanticStyling: SemanticStyling) { super(); this._watchers = Object.create(null); - this._semanticStyling = this._register(new SemanticStyling(themeService, logService)); + this._semanticStyling = semanticStyling; const isSemanticColoringEnabled = (model: ITextModel) => { if (!themeService.getColorTheme().semanticHighlighting) { @@ -633,204 +639,27 @@ class SemanticColoringFeature extends Disposable { class SemanticStyling extends Disposable { - private _caches: WeakMap; + private _caches: WeakMap; constructor( private readonly _themeService: IThemeService, private readonly _logService: ILogService ) { super(); - this._caches = new WeakMap(); + this._caches = new WeakMap(); this._register(this._themeService.onDidColorThemeChange(() => { - this._caches = new WeakMap(); + this._caches = new WeakMap(); })); } - public get(provider: DocumentSemanticTokensProvider): SemanticColoringProviderStyling { + public get(provider: DocumentTokensProvider): SemanticTokensProviderStyling { if (!this._caches.has(provider)) { - this._caches.set(provider, new SemanticColoringProviderStyling(provider.getLegend(), this._themeService, this._logService)); + this._caches.set(provider, new SemanticTokensProviderStyling(provider.getLegend(), this._themeService, this._logService)); } return this._caches.get(provider)!; } } -const enum Constants { - NO_STYLING = 0b01111111111111111111111111111111 -} - -class HashTableEntry { - public readonly tokenTypeIndex: number; - public readonly tokenModifierSet: number; - public readonly languageId: number; - public readonly metadata: number; - public next: HashTableEntry | null; - - constructor(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number) { - this.tokenTypeIndex = tokenTypeIndex; - this.tokenModifierSet = tokenModifierSet; - this.languageId = languageId; - this.metadata = metadata; - this.next = null; - } -} - -class HashTable { - - private static _SIZES = [3, 7, 13, 31, 61, 127, 251, 509, 1021, 2039, 4093, 8191, 16381, 32749, 65521, 131071, 262139, 524287, 1048573, 2097143]; - - private _elementsCount: number; - private _currentLengthIndex: number; - private _currentLength: number; - private _growCount: number; - private _elements: (HashTableEntry | null)[]; - - constructor() { - this._elementsCount = 0; - this._currentLengthIndex = 0; - this._currentLength = HashTable._SIZES[this._currentLengthIndex]; - this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0); - this._elements = []; - HashTable._nullOutEntries(this._elements, this._currentLength); - } - - private static _nullOutEntries(entries: (HashTableEntry | null)[], length: number): void { - for (let i = 0; i < length; i++) { - entries[i] = null; - } - } - - private _hashFunc(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): number { - const hash = (n1: number, n2: number) => (((n1 << 5) - n1) + n2) | 0; // n1 * 31 + n2, keep as int32 - return hash(hash(tokenTypeIndex, tokenModifierSet), languageId) % this._currentLength; - } - - public get(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): HashTableEntry | null { - const hash = this._hashFunc(tokenTypeIndex, tokenModifierSet, languageId); - - let p = this._elements[hash]; - while (p) { - if (p.tokenTypeIndex === tokenTypeIndex && p.tokenModifierSet === tokenModifierSet && p.languageId === languageId) { - return p; - } - p = p.next; - } - - return null; - } - - public add(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number): void { - this._elementsCount++; - if (this._growCount !== 0 && this._elementsCount >= this._growCount) { - // expand! - const oldElements = this._elements; - - this._currentLengthIndex++; - this._currentLength = HashTable._SIZES[this._currentLengthIndex]; - this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0); - this._elements = []; - HashTable._nullOutEntries(this._elements, this._currentLength); - - for (const first of oldElements) { - let p = first; - while (p) { - const oldNext = p.next; - p.next = null; - this._add(p); - p = oldNext; - } - } - } - this._add(new HashTableEntry(tokenTypeIndex, tokenModifierSet, languageId, metadata)); - } - - private _add(element: HashTableEntry): void { - const hash = this._hashFunc(element.tokenTypeIndex, element.tokenModifierSet, element.languageId); - element.next = this._elements[hash]; - this._elements[hash] = element; - } -} - -class SemanticColoringProviderStyling { - - private readonly _hashTable: HashTable; - - constructor( - private readonly _legend: SemanticTokensLegend, - private readonly _themeService: IThemeService, - private readonly _logService: ILogService - ) { - this._hashTable = new HashTable(); - } - - public getMetadata(tokenTypeIndex: number, tokenModifierSet: number, languageId: LanguageIdentifier): number { - const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet, languageId.id); - let metadata: number; - if (entry) { - metadata = entry.metadata; - } else { - const tokenType = this._legend.tokenTypes[tokenTypeIndex]; - const tokenModifiers: string[] = []; - let modifierSet = tokenModifierSet; - for (let modifierIndex = 0; modifierSet > 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) { - if (modifierSet & 1) { - tokenModifiers.push(this._legend.tokenModifiers[modifierIndex]); - } - modifierSet = modifierSet >> 1; - } - - const tokenStyle = this._themeService.getColorTheme().getTokenStyleMetadata(tokenType, tokenModifiers, languageId.language); - if (typeof tokenStyle === 'undefined') { - metadata = Constants.NO_STYLING; - } else { - metadata = 0; - if (typeof tokenStyle.italic !== 'undefined') { - const italicBit = (tokenStyle.italic ? FontStyle.Italic : 0) << MetadataConsts.FONT_STYLE_OFFSET; - metadata |= italicBit | MetadataConsts.SEMANTIC_USE_ITALIC; - } - if (typeof tokenStyle.bold !== 'undefined') { - const boldBit = (tokenStyle.bold ? FontStyle.Bold : 0) << MetadataConsts.FONT_STYLE_OFFSET; - metadata |= boldBit | MetadataConsts.SEMANTIC_USE_BOLD; - } - if (typeof tokenStyle.underline !== 'undefined') { - const underlineBit = (tokenStyle.underline ? FontStyle.Underline : 0) << MetadataConsts.FONT_STYLE_OFFSET; - metadata |= underlineBit | MetadataConsts.SEMANTIC_USE_UNDERLINE; - } - if (tokenStyle.foreground) { - const foregroundBits = (tokenStyle.foreground) << MetadataConsts.FOREGROUND_OFFSET; - metadata |= foregroundBits | MetadataConsts.SEMANTIC_USE_FOREGROUND; - } - if (metadata === 0) { - // Nothing! - metadata = Constants.NO_STYLING; - } - } - this._hashTable.add(tokenTypeIndex, tokenModifierSet, languageId.id, metadata); - } - if (this._logService.getLevel() === LogLevel.Trace) { - const type = this._legend.tokenTypes[tokenTypeIndex]; - const modifiers = tokenModifierSet ? ' ' + this._legend.tokenModifiers.filter((_, i) => tokenModifierSet & (1 << i)).join(' ') : ''; - this._logService.trace(`tokenStyleMetadata ${entry ? '[CACHED] ' : ''}${type}${modifiers}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); - } - return metadata; - } - - -} - -const enum SemanticColoringConstants { - /** - * Let's aim at having 8KB buffers if possible... - * So that would be 8192 / (5 * 4) = 409.6 tokens per area - */ - DesiredTokensPerArea = 400, - - /** - * Try to keep the total number of areas under 1024 if possible, - * simply compensate by having more tokens per area... - */ - DesiredMaxAreas = 1024, -} - class SemanticTokensResponse { constructor( private readonly _provider: DocumentSemanticTokensProvider, @@ -848,10 +677,10 @@ class ModelSemanticColoring extends Disposable { private _isDisposed: boolean; private readonly _model: ITextModel; private readonly _semanticStyling: SemanticStyling; - private readonly _fetchSemanticTokens: RunOnceScheduler; - private _currentResponse: SemanticTokensResponse | null; - private _currentRequestCancellationTokenSource: CancellationTokenSource | null; - private _providersChangeListeners: IDisposable[]; + private readonly _fetchDocumentSemanticTokens: RunOnceScheduler; + private _currentDocumentResponse: SemanticTokensResponse | null; + private _currentDocumentRequestCancellationTokenSource: CancellationTokenSource | null; + private _documentProvidersChangeListeners: IDisposable[]; constructor(model: ITextModel, themeService: IThemeService, stylingProvider: SemanticStyling) { super(); @@ -859,57 +688,57 @@ class ModelSemanticColoring extends Disposable { this._isDisposed = false; this._model = model; this._semanticStyling = stylingProvider; - this._fetchSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchSemanticTokensNow(), 300)); - this._currentResponse = null; - this._currentRequestCancellationTokenSource = null; - this._providersChangeListeners = []; + this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), 300)); + this._currentDocumentResponse = null; + this._currentDocumentRequestCancellationTokenSource = null; + this._documentProvidersChangeListeners = []; - this._register(this._model.onDidChangeContent(e => { - if (!this._fetchSemanticTokens.isScheduled()) { - this._fetchSemanticTokens.schedule(); + this._register(this._model.onDidChangeContent(() => { + if (!this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(); } })); - const bindChangeListeners = () => { - dispose(this._providersChangeListeners); - this._providersChangeListeners = []; + const bindDocumentChangeListeners = () => { + dispose(this._documentProvidersChangeListeners); + this._documentProvidersChangeListeners = []; for (const provider of DocumentSemanticTokensProviderRegistry.all(model)) { if (typeof provider.onDidChange === 'function') { - this._providersChangeListeners.push(provider.onDidChange(() => this._fetchSemanticTokens.schedule(0))); + this._documentProvidersChangeListeners.push(provider.onDidChange(() => this._fetchDocumentSemanticTokens.schedule(0))); } } }; - bindChangeListeners(); - this._register(DocumentSemanticTokensProviderRegistry.onDidChange(e => { - bindChangeListeners(); - this._fetchSemanticTokens.schedule(); + bindDocumentChangeListeners(); + this._register(DocumentSemanticTokensProviderRegistry.onDidChange(() => { + bindDocumentChangeListeners(); + this._fetchDocumentSemanticTokens.schedule(); })); this._register(themeService.onDidColorThemeChange(_ => { // clear out existing tokens - this._setSemanticTokens(null, null, null, []); - this._fetchSemanticTokens.schedule(); + this._setDocumentSemanticTokens(null, null, null, []); + this._fetchDocumentSemanticTokens.schedule(); })); - this._fetchSemanticTokens.schedule(0); + this._fetchDocumentSemanticTokens.schedule(0); } public dispose(): void { - if (this._currentResponse) { - this._currentResponse.dispose(); - this._currentResponse = null; + if (this._currentDocumentResponse) { + this._currentDocumentResponse.dispose(); + this._currentDocumentResponse = null; } - if (this._currentRequestCancellationTokenSource) { - this._currentRequestCancellationTokenSource.cancel(); - this._currentRequestCancellationTokenSource = null; + if (this._currentDocumentRequestCancellationTokenSource) { + this._currentDocumentRequestCancellationTokenSource.cancel(); + this._currentDocumentRequestCancellationTokenSource = null; } - this._setSemanticTokens(null, null, null, []); + this._setDocumentSemanticTokens(null, null, null, []); this._isDisposed = true; super.dispose(); } - private _fetchSemanticTokensNow(): void { - if (this._currentRequestCancellationTokenSource) { + private _fetchDocumentSemanticTokensNow(): void { + if (this._currentDocumentRequestCancellationTokenSource) { // there is already a request running, let it finish... return; } @@ -917,7 +746,7 @@ class ModelSemanticColoring extends Disposable { if (!provider) { return; } - this._currentRequestCancellationTokenSource = new CancellationTokenSource(); + this._currentDocumentRequestCancellationTokenSource = new CancellationTokenSource(); const pendingChanges: IModelContentChangedEvent[] = []; const contentChangeListener = this._model.onDidChangeContent((e) => { @@ -926,13 +755,13 @@ class ModelSemanticColoring extends Disposable { const styling = this._semanticStyling.get(provider); - const lastResultId = this._currentResponse ? this._currentResponse.resultId || null : null; - const request = Promise.resolve(provider.provideDocumentSemanticTokens(this._model, lastResultId, this._currentRequestCancellationTokenSource.token)); + const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; + const request = Promise.resolve(provider.provideDocumentSemanticTokens(this._model, lastResultId, this._currentDocumentRequestCancellationTokenSource.token)); request.then((res) => { - this._currentRequestCancellationTokenSource = null; + this._currentDocumentRequestCancellationTokenSource = null; contentChangeListener.dispose(); - this._setSemanticTokens(provider, res || null, styling, pendingChanges); + this._setDocumentSemanticTokens(provider, res || null, styling, pendingChanges); }, (err) => { if (!err || typeof err.message !== 'string' || err.message.indexOf('busy') === -1) { errors.onUnexpectedError(err); @@ -940,13 +769,13 @@ class ModelSemanticColoring extends Disposable { // Semantic tokens eats up all errors and considers errors to mean that the result is temporarily not available // The API does not have a special error kind to express this... - this._currentRequestCancellationTokenSource = null; + this._currentDocumentRequestCancellationTokenSource = null; contentChangeListener.dispose(); if (pendingChanges.length > 0) { // More changes occurred while the request was running - if (!this._fetchSemanticTokens.isScheduled()) { - this._fetchSemanticTokens.schedule(); + if (!this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(); } } }); @@ -966,11 +795,11 @@ class ModelSemanticColoring extends Disposable { } } - private _setSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticColoringProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { - const currentResponse = this._currentResponse; - if (this._currentResponse) { - this._currentResponse.dispose(); - this._currentResponse = null; + private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { + const currentResponse = this._currentDocumentResponse; + if (this._currentDocumentResponse) { + this._currentDocumentResponse.dispose(); + this._currentDocumentResponse = null; } if (this._isDisposed) { // disposed! @@ -979,15 +808,19 @@ class ModelSemanticColoring extends Disposable { } return; } - if (!provider || !tokens || !styling) { - this._model.setSemanticTokens(null); + if (!provider || !styling) { + this._model.setSemanticTokens(null, false); + return; + } + if (!tokens) { + this._model.setSemanticTokens(null, true); return; } if (ModelSemanticColoring._isSemanticTokensEdits(tokens)) { if (!currentResponse) { // not possible! - this._model.setSemanticTokens(null); + this._model.setSemanticTokens(null, true); return; } if (tokens.edits.length === 0) { @@ -1037,80 +870,9 @@ class ModelSemanticColoring extends Disposable { if (ModelSemanticColoring._isSemanticTokens(tokens)) { - this._currentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); + this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); - const srcData = tokens.data; - const tokenCount = (tokens.data.length / 5) | 0; - const tokensPerArea = Math.max(Math.ceil(tokenCount / SemanticColoringConstants.DesiredMaxAreas), SemanticColoringConstants.DesiredTokensPerArea); - - const result: MultilineTokens2[] = []; - - const languageId = this._model.getLanguageIdentifier(); - - let tokenIndex = 0; - let lastLineNumber = 1; - let lastStartCharacter = 0; - while (tokenIndex < tokenCount) { - const tokenStartIndex = tokenIndex; - let tokenEndIndex = Math.min(tokenStartIndex + tokensPerArea, tokenCount); - - // Keep tokens on the same line in the same area... - if (tokenEndIndex < tokenCount) { - - let smallTokenEndIndex = tokenEndIndex; - while (smallTokenEndIndex - 1 > tokenStartIndex && srcData[5 * smallTokenEndIndex] === 0) { - smallTokenEndIndex--; - } - - if (smallTokenEndIndex - 1 === tokenStartIndex) { - // there are so many tokens on this line that our area would be empty, we must now go right - let bigTokenEndIndex = tokenEndIndex; - while (bigTokenEndIndex + 1 < tokenCount && srcData[5 * bigTokenEndIndex] === 0) { - bigTokenEndIndex++; - } - tokenEndIndex = bigTokenEndIndex; - } else { - tokenEndIndex = smallTokenEndIndex; - } - } - - let destData = new Uint32Array((tokenEndIndex - tokenStartIndex) * 4); - let destOffset = 0; - let areaLine = 0; - while (tokenIndex < tokenEndIndex) { - const srcOffset = 5 * tokenIndex; - const deltaLine = srcData[srcOffset]; - const deltaCharacter = srcData[srcOffset + 1]; - const lineNumber = lastLineNumber + deltaLine; - const startCharacter = (deltaLine === 0 ? lastStartCharacter + deltaCharacter : deltaCharacter); - const length = srcData[srcOffset + 2]; - const tokenTypeIndex = srcData[srcOffset + 3]; - const tokenModifierSet = srcData[srcOffset + 4]; - const metadata = styling.getMetadata(tokenTypeIndex, tokenModifierSet, languageId); - - if (metadata !== Constants.NO_STYLING) { - if (areaLine === 0) { - areaLine = lineNumber; - } - destData[destOffset] = lineNumber - areaLine; - destData[destOffset + 1] = startCharacter; - destData[destOffset + 2] = startCharacter + length; - destData[destOffset + 3] = metadata; - destOffset += 4; - } - - lastLineNumber = lineNumber; - lastStartCharacter = startCharacter; - tokenIndex++; - } - - if (destOffset !== destData.length) { - destData = destData.subarray(0, destOffset); - } - - const tokens = new MultilineTokens2(areaLine, new SparseEncodedTokens(destData)); - result.push(tokens); - } + const result = toMultilineTokens2(tokens, styling, this._model.getLanguageIdentifier()); // Adjust incoming semantic tokens if (pendingChanges.length > 0) { @@ -1126,16 +888,16 @@ class ModelSemanticColoring extends Disposable { } } - if (!this._fetchSemanticTokens.isScheduled()) { - this._fetchSemanticTokens.schedule(); + if (!this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(); } } - this._model.setSemanticTokens(result); + this._model.setSemanticTokens(result, true); return; } - this._model.setSemanticTokens(null); + this._model.setSemanticTokens(null, true); } private _getSemanticColoringProvider(): DocumentSemanticTokensProvider | null { diff --git a/src/vs/editor/common/services/semanticTokensProviderStyling.ts b/src/vs/editor/common/services/semanticTokensProviderStyling.ts new file mode 100644 index 0000000000..7acd0dd240 --- /dev/null +++ b/src/vs/editor/common/services/semanticTokensProviderStyling.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * 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 { 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'; + +export const enum SemanticTokensProviderStylingConstants { + NO_STYLING = 0b01111111111111111111111111111111 +} + +export class SemanticTokensProviderStyling { + + private readonly _hashTable: HashTable; + + constructor( + private readonly _legend: SemanticTokensLegend, + private readonly _themeService: IThemeService, + private readonly _logService: ILogService + ) { + this._hashTable = new HashTable(); + } + + public getMetadata(tokenTypeIndex: number, tokenModifierSet: number, languageId: LanguageIdentifier): number { + const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet, languageId.id); + let metadata: number; + if (entry) { + metadata = entry.metadata; + } else { + const tokenType = this._legend.tokenTypes[tokenTypeIndex]; + const tokenModifiers: string[] = []; + let modifierSet = tokenModifierSet; + for (let modifierIndex = 0; modifierSet > 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) { + if (modifierSet & 1) { + tokenModifiers.push(this._legend.tokenModifiers[modifierIndex]); + } + modifierSet = modifierSet >> 1; + } + + const tokenStyle = this._themeService.getColorTheme().getTokenStyleMetadata(tokenType, tokenModifiers, languageId.language); + if (typeof tokenStyle === 'undefined') { + metadata = SemanticTokensProviderStylingConstants.NO_STYLING; + } else { + metadata = 0; + if (typeof tokenStyle.italic !== 'undefined') { + const italicBit = (tokenStyle.italic ? FontStyle.Italic : 0) << MetadataConsts.FONT_STYLE_OFFSET; + metadata |= italicBit | MetadataConsts.SEMANTIC_USE_ITALIC; + } + if (typeof tokenStyle.bold !== 'undefined') { + const boldBit = (tokenStyle.bold ? FontStyle.Bold : 0) << MetadataConsts.FONT_STYLE_OFFSET; + metadata |= boldBit | MetadataConsts.SEMANTIC_USE_BOLD; + } + if (typeof tokenStyle.underline !== 'undefined') { + const underlineBit = (tokenStyle.underline ? FontStyle.Underline : 0) << MetadataConsts.FONT_STYLE_OFFSET; + metadata |= underlineBit | MetadataConsts.SEMANTIC_USE_UNDERLINE; + } + if (tokenStyle.foreground) { + const foregroundBits = (tokenStyle.foreground) << MetadataConsts.FOREGROUND_OFFSET; + metadata |= foregroundBits | MetadataConsts.SEMANTIC_USE_FOREGROUND; + } + if (metadata === 0) { + // Nothing! + metadata = SemanticTokensProviderStylingConstants.NO_STYLING; + } + } + this._hashTable.add(tokenTypeIndex, tokenModifierSet, languageId.id, metadata); + } + if (this._logService.getLevel() === LogLevel.Trace) { + const type = this._legend.tokenTypes[tokenTypeIndex]; + const modifiers = tokenModifierSet ? ' ' + this._legend.tokenModifiers.filter((_, i) => tokenModifierSet & (1 << i)).join(' ') : ''; + this._logService.trace(`tokenStyleMetadata ${entry ? '[CACHED] ' : ''}${type}${modifiers}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); + } + return metadata; + } +} + +const enum SemanticColoringConstants { + /** + * Let's aim at having 8KB buffers if possible... + * So that would be 8192 / (5 * 4) = 409.6 tokens per area + */ + DesiredTokensPerArea = 400, + + /** + * Try to keep the total number of areas under 1024 if possible, + * simply compensate by having more tokens per area... + */ + DesiredMaxAreas = 1024, +} + +export function toMultilineTokens2(tokens: SemanticTokens, styling: SemanticTokensProviderStyling, languageId: LanguageIdentifier): MultilineTokens2[] { + const srcData = tokens.data; + const tokenCount = (tokens.data.length / 5) | 0; + const tokensPerArea = Math.max(Math.ceil(tokenCount / SemanticColoringConstants.DesiredMaxAreas), SemanticColoringConstants.DesiredTokensPerArea); + const result: MultilineTokens2[] = []; + + let tokenIndex = 0; + let lastLineNumber = 1; + let lastStartCharacter = 0; + while (tokenIndex < tokenCount) { + const tokenStartIndex = tokenIndex; + let tokenEndIndex = Math.min(tokenStartIndex + tokensPerArea, tokenCount); + + // Keep tokens on the same line in the same area... + if (tokenEndIndex < tokenCount) { + + let smallTokenEndIndex = tokenEndIndex; + while (smallTokenEndIndex - 1 > tokenStartIndex && srcData[5 * smallTokenEndIndex] === 0) { + smallTokenEndIndex--; + } + + if (smallTokenEndIndex - 1 === tokenStartIndex) { + // there are so many tokens on this line that our area would be empty, we must now go right + let bigTokenEndIndex = tokenEndIndex; + while (bigTokenEndIndex + 1 < tokenCount && srcData[5 * bigTokenEndIndex] === 0) { + bigTokenEndIndex++; + } + tokenEndIndex = bigTokenEndIndex; + } else { + tokenEndIndex = smallTokenEndIndex; + } + } + + let destData = new Uint32Array((tokenEndIndex - tokenStartIndex) * 4); + let destOffset = 0; + let areaLine = 0; + while (tokenIndex < tokenEndIndex) { + const srcOffset = 5 * tokenIndex; + const deltaLine = srcData[srcOffset]; + const deltaCharacter = srcData[srcOffset + 1]; + const lineNumber = lastLineNumber + deltaLine; + const startCharacter = (deltaLine === 0 ? lastStartCharacter + deltaCharacter : deltaCharacter); + const length = srcData[srcOffset + 2]; + const tokenTypeIndex = srcData[srcOffset + 3]; + const tokenModifierSet = srcData[srcOffset + 4]; + const metadata = styling.getMetadata(tokenTypeIndex, tokenModifierSet, languageId); + + if (metadata !== SemanticTokensProviderStylingConstants.NO_STYLING) { + if (areaLine === 0) { + areaLine = lineNumber; + } + destData[destOffset] = lineNumber - areaLine; + destData[destOffset + 1] = startCharacter; + destData[destOffset + 2] = startCharacter + length; + destData[destOffset + 3] = metadata; + destOffset += 4; + } + + lastLineNumber = lineNumber; + lastStartCharacter = startCharacter; + tokenIndex++; + } + + if (destOffset !== destData.length) { + destData = destData.subarray(0, destOffset); + } + + const tokens = new MultilineTokens2(areaLine, new SparseEncodedTokens(destData)); + result.push(tokens); + } + + return result; +} + +class HashTableEntry { + public readonly tokenTypeIndex: number; + public readonly tokenModifierSet: number; + public readonly languageId: number; + public readonly metadata: number; + public next: HashTableEntry | null; + + constructor(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number) { + this.tokenTypeIndex = tokenTypeIndex; + this.tokenModifierSet = tokenModifierSet; + this.languageId = languageId; + this.metadata = metadata; + this.next = null; + } +} + +class HashTable { + + private static _SIZES = [3, 7, 13, 31, 61, 127, 251, 509, 1021, 2039, 4093, 8191, 16381, 32749, 65521, 131071, 262139, 524287, 1048573, 2097143]; + + private _elementsCount: number; + private _currentLengthIndex: number; + private _currentLength: number; + private _growCount: number; + private _elements: (HashTableEntry | null)[]; + + constructor() { + this._elementsCount = 0; + this._currentLengthIndex = 0; + this._currentLength = HashTable._SIZES[this._currentLengthIndex]; + this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0); + this._elements = []; + HashTable._nullOutEntries(this._elements, this._currentLength); + } + + private static _nullOutEntries(entries: (HashTableEntry | null)[], length: number): void { + for (let i = 0; i < length; i++) { + entries[i] = null; + } + } + + private _hash2(n1: number, n2: number): number { + return (((n1 << 5) - n1) + n2) | 0; // n1 * 31 + n2, keep as int32 + } + + private _hashFunc(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): number { + return this._hash2(this._hash2(tokenTypeIndex, tokenModifierSet), languageId) % this._currentLength; + } + + public get(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): HashTableEntry | null { + const hash = this._hashFunc(tokenTypeIndex, tokenModifierSet, languageId); + + let p = this._elements[hash]; + while (p) { + if (p.tokenTypeIndex === tokenTypeIndex && p.tokenModifierSet === tokenModifierSet && p.languageId === languageId) { + return p; + } + p = p.next; + } + + return null; + } + + public add(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number): void { + this._elementsCount++; + if (this._growCount !== 0 && this._elementsCount >= this._growCount) { + // expand! + const oldElements = this._elements; + + this._currentLengthIndex++; + this._currentLength = HashTable._SIZES[this._currentLengthIndex]; + this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0); + this._elements = []; + HashTable._nullOutEntries(this._elements, this._currentLength); + + for (const first of oldElements) { + let p = first; + while (p) { + const oldNext = p.next; + p.next = null; + this._add(p); + p = oldNext; + } + } + } + this._add(new HashTableEntry(tokenTypeIndex, tokenModifierSet, languageId, metadata)); + } + + private _add(element: HashTableEntry): void { + const hash = this._hashFunc(element.tokenTypeIndex, element.tokenModifierSet, element.languageId); + element.next = this._elements[hash]; + this._elements[hash] = element; + } +} diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index aecc2483eb..7815a55f46 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -813,7 +813,11 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render let prevPartContentCnt = 0; let partAbsoluteOffset = 0; - sb.appendASCIIString(''); + if (containsRTL) { + sb.appendASCIIString(''); + } else { + sb.appendASCIIString(''); + } for (let partIndex = 0, tokensLen = parts.length; partIndex < tokensLen; partIndex++) { partAbsoluteOffset += prevPartContentCnt; @@ -890,9 +894,6 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render let partContentCnt = 0; - if (containsRTL) { - sb.appendASCIIString(' dir="ltr"'); - } sb.appendASCII(CharCode.GreaterThan); for (; charIndex < partEndIndex; charIndex++) { diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 2185b04e82..59d8e36863 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -563,8 +563,11 @@ export class ModesContentHoverWidget extends ContentHoverWidget { const disposables = new DisposableStore(); const actionsElement = dom.append(hoverElement, $('div.actions')); if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) { + const peekProblemLabel = nls.localize('peek problem', "Peek Problem"); + const peekProblemKeybinding = this._keybindingService.lookupKeybinding(NextMarkerAction.ID); + const peekProblemKeybindingLabel = peekProblemKeybinding && peekProblemKeybinding.getLabel(); disposables.add(this.renderAction(actionsElement, { - label: nls.localize('peek problem', "Peek Problem"), + label: peekProblemKeybindingLabel ? nls.localize('titleAndKb', "{0} ({1})", peekProblemLabel, peekProblemKeybindingLabel) : peekProblemLabel, commandId: NextMarkerAction.ID, run: () => { this.hide(); @@ -581,7 +584,6 @@ export class ModesContentHoverWidget extends ContentHoverWidget { quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."); disposables.add(toDisposable(() => quickfixPlaceholderElement.remove())); - const codeActionsPromise = this.getCodeActions(markerHover.marker); disposables.add(toDisposable(() => codeActionsPromise.cancel())); codeActionsPromise.then(actions => { @@ -602,8 +604,12 @@ export class ModesContentHoverWidget extends ContentHoverWidget { } })); + const quickFixLabel = nls.localize('quick fixes', "Quick Fix..."); + const quickFixKeybinding = this._keybindingService.lookupKeybinding(QuickFixAction.Id); + const quickFixKeybindingLabel = quickFixKeybinding && quickFixKeybinding.getLabel(); + disposables.add(this.renderAction(actionsElement, { - label: nls.localize('quick fixes', "Quick Fix..."), + label: quickFixKeybindingLabel ? nls.localize('titleAndKb', "{0} ({1})", quickFixLabel, quickFixKeybindingLabel) : quickFixLabel, commandId: QuickFixAction.Id, run: (target) => { showing = true; diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index 33973134c8..4ee15a6de6 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -786,11 +786,13 @@ class SelectionHighlighterState { public readonly searchText: string; public readonly matchCase: boolean; public readonly wordSeparators: string | null; + public readonly modelVersionId: number; - constructor(searchText: string, matchCase: boolean, wordSeparators: string | null) { + constructor(searchText: string, matchCase: boolean, wordSeparators: string | null, modelVersionId: number) { this.searchText = searchText; this.matchCase = matchCase; this.wordSeparators = wordSeparators; + this.modelVersionId = modelVersionId; } /** @@ -807,6 +809,7 @@ class SelectionHighlighterState { a.searchText === b.searchText && a.matchCase === b.matchCase && a.wordSeparators === b.wordSeparators + && a.modelVersionId === b.modelVersionId ); } } @@ -857,6 +860,11 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut this._register(editor.onDidChangeModel((e) => { this._setState(null); })); + this._register(editor.onDidChangeModelContent((e) => { + if (this._isEnabled) { + this.updateSoon.schedule(); + } + })); this._register(CommonFindController.get(editor).getState().onFindReplaceStateChange((e) => { this._update(); })); @@ -939,7 +947,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut } } - return new SelectionHighlighterState(r.searchText, r.matchCase, r.wholeWord ? editor.getOption(EditorOption.wordSeparators) : null); + return new SelectionHighlighterState(r.searchText, r.matchCase, r.wholeWord ? editor.getOption(EditorOption.wordSeparators) : null, editor.getModel().getVersionId()); } private _setState(state: SelectionHighlighterState | null): void { diff --git a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts index 0f2c45fd57..181dfcb33a 100644 --- a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts @@ -134,10 +134,10 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor const position = editor.getPosition() || { lineNumber: 1, column: 1 }; const lineCount = this.lineCount(editor); if (lineCount > 1) { - return localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Column: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount); + return localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Character: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount); } - return localize('gotoLineLabelEmpty', "Current Line: {0}, Column: {1}. Type a line number to navigate to.", position.lineNumber, position.column); + return localize('gotoLineLabelEmpty', "Current Line: {0}, Character: {1}. Type a line number to navigate to.", position.lineNumber, position.column); } private isValidLineNumber(editor: IEditor, lineNumber: number | undefined): boolean { diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index b402c4476e..17f433800f 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -41,7 +41,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit } protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { - const label = localize('cannotRunGotoSymbolWithoutEditor', "Open a text editor first to go to a symbol."); + const label = localize('cannotRunGotoSymbolWithoutEditor', "To go to a symbol, first open a text editor with symbol information."); picker.items = [{ label, index: 0, kind: SymbolKind.String }]; picker.ariaLabel = label; @@ -70,7 +70,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit const disposables = new DisposableStore(); // Generic pick for not having any symbol information - const label = localize('cannotRunGotoSymbolWithoutSymbolProvider', "Open a text editor with symbol information first to go to a symbol."); + const label = localize('cannotRunGotoSymbolWithoutSymbolProvider', "The active text editor does not provide symbol information."); picker.items = [{ label, index: 0, kind: SymbolKind.String }]; picker.ariaLabel = label; diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 6ceb0eb162..c33cda843a 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -37,7 +37,6 @@ 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 { SuggestRangeHighlighter } from 'vs/editor/contrib/suggest/suggestRangeHighlighter'; import { MenuRegistry } from 'vs/platform/actions/common/actions'; // sticky suggest widget which doesn't disappear on focus out and such @@ -233,9 +232,6 @@ export class SuggestController implements IEditorContribution { }; this._toDispose.add(this.editor.onDidChangeConfiguration(() => updateFromConfig())); updateFromConfig(); - - // create range highlighter - this._toDispose.add(new SuggestRangeHighlighter(this)); } dispose(): void { diff --git a/src/vs/editor/contrib/suggest/suggestRangeHighlighter.ts b/src/vs/editor/contrib/suggest/suggestRangeHighlighter.ts deleted file mode 100644 index 5848384a5c..0000000000 --- a/src/vs/editor/contrib/suggest/suggestRangeHighlighter.ts +++ /dev/null @@ -1,128 +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 { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { Range } from 'vs/editor/common/core/range'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { CompletionItem } from 'vs/editor/contrib/suggest/suggest'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; -import { Emitter } from 'vs/base/common/event'; -import { domEvent } from 'vs/base/browser/event'; -import { domContentLoaded } from 'vs/base/browser/dom'; - -export class SuggestRangeHighlighter { - - private readonly _disposables = new DisposableStore(); - - private _decorations: string[] = []; - private _widgetListener?: IDisposable; - private _shiftKeyListener?: IDisposable; - private _currentItem?: CompletionItem; - - constructor(private readonly _controller: SuggestController) { - - this._disposables.add(_controller.model.onDidSuggest(e => { - if (!e.shy) { - const widget = this._controller.widget.getValue(); - const focused = widget.getFocusedItem(); - if (focused) { - this._highlight(focused.item); - } - if (!this._widgetListener) { - this._widgetListener = widget.onDidFocus(e => this._highlight(e.item)); - } - } - })); - - this._disposables.add(_controller.model.onDidCancel(() => { - this._reset(); - })); - } - - dispose(): void { - this._reset(); - this._disposables.dispose(); - dispose(this._widgetListener); - dispose(this._shiftKeyListener); - } - - private _reset(): void { - this._decorations = this._controller.editor.deltaDecorations(this._decorations, []); - if (this._shiftKeyListener) { - this._shiftKeyListener.dispose(); - this._shiftKeyListener = undefined; - } - } - - private _highlight(item: CompletionItem) { - - this._currentItem = item; - const opts = this._controller.editor.getOption(EditorOption.suggest); - let newDeco: IModelDeltaDecoration[] = []; - - if (opts.insertHighlight) { - if (!this._shiftKeyListener) { - this._shiftKeyListener = shiftKey.event(() => this._highlight(this._currentItem!)); - } - - const info = this._controller.getOverwriteInfo(item, shiftKey.isPressed); - const position = this._controller.editor.getPosition()!; - - if (opts.insertMode === 'insert' && info.overwriteAfter > 0) { - // wants inserts but got replace-mode -> highlight AFTER range - newDeco = [{ - range: new Range(position.lineNumber, position.column, position.lineNumber, position.column + info.overwriteAfter), - options: { inlineClassName: 'suggest-insert-unexpected' } - }]; - - } else if (opts.insertMode === 'replace' && info.overwriteAfter === 0) { - // want replace but likely got insert -> highlight AFTER range - const wordInfo = this._controller.editor.getModel()?.getWordAtPosition(position); - if (wordInfo && wordInfo.endColumn > position.column) { - newDeco = [{ - range: new Range(position.lineNumber, position.column, position.lineNumber, wordInfo.endColumn), - options: { inlineClassName: 'suggest-insert-unexpected' } - }]; - } - } - } - - // update editor decorations - this._decorations = this._controller.editor.deltaDecorations(this._decorations, newDeco); - } -} - -const shiftKey = new class ShiftKey extends Emitter { - - private readonly _subscriptions = new DisposableStore(); - private _isPressed: boolean = false; - - constructor() { - super(); - domContentLoaded().then(() => { - this._subscriptions.add(domEvent(document.body, 'keydown')(e => this.isPressed = e.shiftKey)); - this._subscriptions.add(domEvent(document.body, 'keyup')(() => this.isPressed = false)); - this._subscriptions.add(domEvent(document.body, 'mouseleave')(() => this.isPressed = false)); - this._subscriptions.add(domEvent(document.body, 'blur')(() => this.isPressed = false)); - }); - } - - get isPressed(): boolean { - return this._isPressed; - } - - set isPressed(value: boolean) { - if (this._isPressed !== value) { - this._isPressed = value; - this.fire(value); - } - } - - dispose() { - this._subscriptions.dispose(); - super.dispose(); - } -}; diff --git a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts new file mode 100644 index 0000000000..fb3a23f970 --- /dev/null +++ b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * 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 { 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 { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { DocumentRangeSemanticTokensProviderRegistry, DocumentRangeSemanticTokensProvider, SemanticTokens } from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { toMultilineTokens2, SemanticTokensProviderStyling } from 'vs/editor/common/services/semanticTokensProviderStyling'; + +class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.viewportSemanticTokens'; + + public static get(editor: ICodeEditor): ViewportSemanticTokensContribution { + return editor.getContribution(ViewportSemanticTokensContribution.ID); + } + + private readonly _editor: ICodeEditor; + private readonly _tokenizeViewport: RunOnceScheduler; + private _outstandingRequests: CancelablePromise[]; + + constructor( + editor: ICodeEditor, + @IModelService private readonly _modelService: IModelService + ) { + super(); + this._editor = editor; + this._tokenizeViewport = new RunOnceScheduler(() => this._tokenizeViewportNow(), 100); + this._outstandingRequests = []; + this._register(this._editor.onDidScrollChange(() => { + this._tokenizeViewport.schedule(); + })); + this._register(this._editor.onDidChangeModel(() => { + this._cancelAll(); + this._tokenizeViewport.schedule(); + })); + this._register(this._editor.onDidChangeModelContent((e) => { + this._cancelAll(); + this._tokenizeViewport.schedule(); + })); + this._register(DocumentRangeSemanticTokensProviderRegistry.onDidChange(() => { + this._cancelAll(); + this._tokenizeViewport.schedule(); + })); + } + + private static _getSemanticColoringProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null { + const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model); + return (result.length > 0 ? result[0] : null); + } + + private _cancelAll(): void { + for (const request of this._outstandingRequests) { + request.cancel(); + } + this._outstandingRequests = []; + } + + 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); + return; + } + } + } + + private _tokenizeViewportNow(): void { + if (!this._editor.hasModel()) { + return; + } + const model = this._editor.getModel(); + if (model.hasSemanticTokens()) { + return; + } + const provider = ViewportSemanticTokensContribution._getSemanticColoringProvider(model); + if (!provider) { + return; + } + const styling = this._modelService.getSemanticTokensProviderStyling(provider); + const visibleRanges = this._editor.getVisibleRanges(); + + this._outstandingRequests = this._outstandingRequests.concat(visibleRanges.map(range => this._requestRange(model, range, provider, styling))); + } + + private _requestRange(model: ITextModel, range: Range, provider: DocumentRangeSemanticTokensProvider, styling: SemanticTokensProviderStyling): CancelablePromise { + const requestVersionId = model.getVersionId(); + const request = createCancelablePromise(token => Promise.resolve(provider.provideDocumentRangeSemanticTokens(model, range, token))); + request.then((r) => { + if (!r || model.isDisposed() || model.getVersionId() !== requestVersionId) { + return; + } + model.setPartialSemanticTokens(range, toMultilineTokens2(r, styling, model.getLanguageIdentifier())); + }).then(() => this._removeOutstandingRequest(request), () => this._removeOutstandingRequest(request)); + return request; + } +} + +registerEditorContribution(ViewportSemanticTokensContribution.ID, ViewportSemanticTokensContribution); diff --git a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts index a820298c0c..afc378bc5c 100644 --- a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts +++ b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts @@ -37,7 +37,7 @@ suite('WordPartOperations', () => { test('cursorWordPartLeft - basic', () => { const EXPECTED = [ '|start| |line|', - '|this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use|', + '|this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use|', '|end| |line' ].join('\n'); const [text,] = deserializePipePositions(EXPECTED); @@ -67,7 +67,7 @@ suite('WordPartOperations', () => { }); test('cursorWordPartLeft - issue #53899: underscores', () => { - const EXPECTED = '|myvar| |=| |\'|demonstration|_____of| |selection| |with| |space|\''; + const EXPECTED = '|myvar| |=| |\'|demonstration_____|of| |selection| |with| |space|\''; const [text,] = deserializePipePositions(EXPECTED); const actualStops = testRepeatedActionAndExtractPositions( text, @@ -83,7 +83,7 @@ suite('WordPartOperations', () => { test('cursorWordPartRight - basic', () => { const EXPECTED = [ 'start| |line|', - '|this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use|', + '|this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use|', '|end| |line|' ].join('\n'); const [text,] = deserializePipePositions(EXPECTED); @@ -113,7 +113,7 @@ suite('WordPartOperations', () => { }); test('cursorWordPartRight - issue #53899: underscores', () => { - const EXPECTED = 'myvar| |=| |\'|demonstration_____|of| |selection| |with| |space|\'|'; + const EXPECTED = 'myvar| |=| |\'|demonstration|_____of| |selection| |with| |space|\'|'; const [text,] = deserializePipePositions(EXPECTED); const actualStops = testRepeatedActionAndExtractPositions( text, @@ -145,8 +145,40 @@ suite('WordPartOperations', () => { assert.deepEqual(actual, EXPECTED); }); + test('issue #93239 - cursorWordPartRight', () => { + const EXPECTED = [ + 'foo|_bar|', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 1), + ed => cursorWordPartRight(ed), + ed => ed.getPosition()!, + ed => ed.getPosition()!.equals(new Position(1, 8)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + + test('issue #93239 - cursorWordPartLeft', () => { + const EXPECTED = [ + '|foo_|bar', + ].join('\n'); + const [text,] = deserializePipePositions(EXPECTED); + const actualStops = testRepeatedActionAndExtractPositions( + text, + new Position(1, 8), + ed => cursorWordPartLeft(ed), + ed => ed.getPosition()!, + ed => ed.getPosition()!.equals(new Position(1, 1)) + ); + const actual = serializePipePositions(text, actualStops); + assert.deepEqual(actual, EXPECTED); + }); + test('deleteWordPartLeft - basic', () => { - const EXPECTED = '| |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use'; + const EXPECTED = '| |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use'; const [text,] = deserializePipePositions(EXPECTED); const actualStops = testRepeatedActionAndExtractPositions( text, @@ -160,7 +192,7 @@ suite('WordPartOperations', () => { }); test('deleteWordPartRight - basic', () => { - const EXPECTED = ' |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use|'; + const EXPECTED = ' |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use|'; const [text,] = deserializePipePositions(EXPECTED); const actualStops = testRepeatedActionAndExtractPositions( text, diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 98409a378e..83c678166b 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -22,6 +22,7 @@ import 'vs/editor/contrib/find/findController'; import 'vs/editor/contrib/folding/folding'; import 'vs/editor/contrib/fontZoom/fontZoom'; import 'vs/editor/contrib/format/formatActions'; +import 'vs/editor/contrib/gotoSymbol/documentSymbols'; import 'vs/editor/contrib/gotoSymbol/goToCommands'; import 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import 'vs/editor/contrib/gotoError/gotoError'; @@ -39,6 +40,7 @@ import 'vs/editor/contrib/snippet/snippetController2'; import 'vs/editor/contrib/suggest/suggestController'; import 'vs/editor/contrib/tokenization/tokenization'; import 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode'; +import 'vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens'; import 'vs/editor/contrib/wordHighlighter/wordHighlighter'; import 'vs/editor/contrib/wordOperations/wordOperations'; import 'vs/editor/contrib/wordPartOperations/wordPartOperations'; diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index 88ed900d0f..cd9fc2386a 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { MultilineTokens2, SparseEncodedTokens } from 'vs/editor/common/model/tokensStore'; +import { MultilineTokens2, SparseEncodedTokens, TokensStore2 } from 'vs/editor/common/model/tokensStore'; import { Range } from 'vs/editor/common/core/range'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { MetadataConsts, TokenMetadata } from 'vs/editor/common/modes'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { LineTokens } from 'vs/editor/common/core/lineTokens'; suite('TokensStore', () => { @@ -98,7 +99,7 @@ suite('TokensStore', () => { function testTokensAdjustment(rawInitialState: string[], edits: IIdentifiedSingleEditOperation[], rawFinalState: string[]) { const initialState = parseTokensState(rawInitialState); const model = createTextModel(initialState.text); - model.setSemanticTokens([initialState.tokens]); + model.setSemanticTokens([initialState.tokens], true); model.applyEdits(edits); @@ -183,7 +184,7 @@ suite('TokensStore', () => { 0, 38, 42, 245768, 0, 43, 47, 180232, ]))) - ]); + ], true); const lineTokens = model.getLineTokens(1); let decodedTokens: number[] = []; for (let i = 0, len = lineTokens.getCount(); i < len; i++) { @@ -212,4 +213,114 @@ suite('TokensStore', () => { model.dispose(); }); + test('partial tokens 1', () => { + const store = new TokensStore2(); + + // 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), [ + new MultilineTokens2(5, new SparseEncodedTokens(new Uint32Array([ + 0, 5, 10, 1, + 5, 5, 10, 2, + 10, 5, 10, 3, + 15, 5, 10, 4, + 20, 5, 10, 5, + 25, 5, 10, 6, + ]))) + ]); + + // setPartial: [18,1 -> 42,1], [(20,5-10),(25,5-10),(30,5-10),(35,5-10),(40,5-10)] + store.setPartial(new Range(18, 1, 42, 1), [ + new MultilineTokens2(20, new SparseEncodedTokens(new Uint32Array([ + 0, 5, 10, 4, + 5, 5, 10, 5, + 10, 5, 10, 6, + 15, 5, 10, 7, + 20, 5, 10, 8, + ]))) + ]); + + // 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), [ + new MultilineTokens2(5, new SparseEncodedTokens(new Uint32Array([ + 0, 5, 10, 1, + 5, 5, 10, 2, + 10, 5, 10, 3, + 15, 5, 10, 4, + 20, 5, 10, 5, + 25, 5, 10, 6, + ]))) + ]); + + const lineTokens = store.addSemanticTokens(10, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); + assert.equal(lineTokens.getCount(), 3); + }); + + test('partial tokens 2', () => { + const store = new TokensStore2(); + + // 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), [ + new MultilineTokens2(5, new SparseEncodedTokens(new Uint32Array([ + 0, 5, 10, 1, + 5, 5, 10, 2, + 10, 5, 10, 3, + 15, 5, 10, 4, + 20, 5, 10, 5, + 25, 5, 10, 6, + ]))) + ]); + + // setPartial: [6,1 -> 36,2], [(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10),(35,5-10)] + store.setPartial(new Range(6, 1, 36, 2), [ + new MultilineTokens2(10, new SparseEncodedTokens(new Uint32Array([ + 0, 5, 10, 2, + 5, 5, 10, 3, + 10, 5, 10, 4, + 15, 5, 10, 5, + 20, 5, 10, 6, + ]))) + ]); + + // setPartial: [17,1 -> 42,1], [(20,5-10),(25,5-10),(30,5-10),(35,5-10),(40,5-10)] + store.setPartial(new Range(17, 1, 42, 1), [ + new MultilineTokens2(20, new SparseEncodedTokens(new Uint32Array([ + 0, 5, 10, 4, + 5, 5, 10, 5, + 10, 5, 10, 6, + 15, 5, 10, 7, + 20, 5, 10, 8, + ]))) + ]); + + const lineTokens = store.addSemanticTokens(20, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); + assert.equal(lineTokens.getCount(), 3); + }); + + test('partial tokens 3', () => { + const store = new TokensStore2(); + + // 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), [ + new MultilineTokens2(5, new SparseEncodedTokens(new Uint32Array([ + 0, 5, 10, 1, + 5, 5, 10, 2, + 10, 5, 10, 3, + 15, 5, 10, 4, + 20, 5, 10, 5, + 25, 5, 10, 6, + ]))) + ]); + + // setPartial: [11,1 -> 16,2], [(15,5-10),(20,5-10)] + store.setPartial(new Range(11, 1, 16, 2), [ + new MultilineTokens2(10, new SparseEncodedTokens(new Uint32Array([ + 0, 5, 10, 3, + 5, 5, 10, 4, + ]))) + ]); + + const lineTokens = store.addSemanticTokens(5, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); + assert.equal(lineTokens.getCount(), 3); + }); + }); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 067c8d1a08..3ca10a0119 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -459,10 +459,10 @@ suite('viewLineRenderer.renderLine', () => { ]); let expectedOutput = [ - 'var', - '\u00a0קודמות\u00a0=\u00a0', - '"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"', - ';' + 'var', + '\u00a0קודמות\u00a0=\u00a0', + '"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"', + ';' ].join(''); let _actual = renderViewLine(new RenderLineInput( @@ -487,7 +487,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.equal(_actual.html, '' + expectedOutput + ''); assert.equal(_actual.containsRTL, true); }); @@ -676,7 +676,7 @@ suite('viewLineRenderer.renderLine', () => { let lineText = 'את גרמנית בהתייחסות שמו, שנתי המשפט אל חפש, אם כתב אחרים ולחבר. של התוכן אודות בויקיפדיה כלל, של עזרה כימיה היא. על עמוד יוצרים מיתולוגיה סדר, אם שכל שתפו לעברית שינויים, אם שאלות אנגלית עזה. שמות בקלות מה סדר.'; let lineParts = createViewLineTokens([createPart(lineText.length, 1)]); let expectedOutput = [ - 'את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.' + 'את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.' ]; let actual = renderViewLine(new RenderLineInput( false, @@ -699,7 +699,7 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + ''); + assert.equal(actual.html, '' + expectedOutput.join('') + ''); assert.equal(actual.containsRTL, true); }); diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts index 6f7fcafc13..3d140c275f 100644 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -107,7 +107,7 @@ function parseTest(fileName: string): ITest { return { content, assertions }; } -// @ts-ignore +// @ts-expect-error function executeTest(fileName: string, parseFunc: IParseFunc): void { const { content, assertions } = parseTest(fileName); const actual = parseFunc(content); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 743caf061e..3574a2d0b8 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3649,10 +3649,6 @@ declare namespace monaco.editor { * Overwrite word ends on accept. Default to false. */ insertMode?: 'insert' | 'replace'; - /** - * Show a highlight when suggestion replaces or keep text after the cursor. Defaults to false. - */ - insertHighlight?: boolean; /** * Enable graceful matching. Defaults to true. */ diff --git a/src/vs/nls.js b/src/vs/nls.js index ae8b2665d6..28242dabbc 100644 --- a/src/vs/nls.js +++ b/src/vs/nls.js @@ -14,6 +14,13 @@ *--------------------------------------------------------------------------------------------- *--------------------------------------------------------------------------------------------*/ 'use strict'; +var __spreadArrays = (this && this.__spreadArrays) || function () { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; +}; var NLSLoaderPlugin; (function (NLSLoaderPlugin) { var Environment = /** @class */ (function () { @@ -94,7 +101,7 @@ var NLSLoaderPlugin; for (var _i = 2; _i < arguments.length; _i++) { args[_i - 2] = arguments[_i]; } - return localize.apply(void 0, [_this._env, data, message].concat(args)); + return localize.apply(void 0, __spreadArrays([_this._env, data, message], args)); }; } NLSPlugin.prototype.setPseudoTranslation = function (value) { diff --git a/src/vs/platform/backup/electron-main/backup.ts b/src/vs/platform/backup/electron-main/backup.ts index 59effce987..90cdb6a7a9 100644 --- a/src/vs/platform/backup/electron-main/backup.ts +++ b/src/vs/platform/backup/electron-main/backup.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; @@ -15,6 +15,12 @@ export interface IWorkspaceBackupInfo { remoteAuthority?: string; } +export function isWorkspaceBackupInfo(obj: unknown): obj is IWorkspaceBackupInfo { + const candidate = obj as IWorkspaceBackupInfo; + + return candidate && isWorkspaceIdentifier(candidate.workspace); +} + export interface IBackupMainService { _serviceBrand: undefined; @@ -31,4 +37,12 @@ export interface IBackupMainService { unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void; unregisterFolderBackupSync(folderUri: URI): void; unregisterEmptyWindowBackupSync(backupFolder: string): void; + + /** + * All folders or workspaces that are known to have + * backups stored. This call is long running because + * it checks for each backup location if any backups + * are stored. + */ + getDirtyWorkspaces(): Promise>; } diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index fc3a03f350..5ec5b9e54a 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -8,8 +8,7 @@ import * as crypto from 'crypto'; import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs'; -import * as arrays from 'vs/base/common/arrays'; -import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; +import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -28,9 +27,9 @@ export class BackupMainService implements IBackupMainService { protected backupHome: string; protected workspacesJsonPath: string; - private rootWorkspaces: IWorkspaceBackupInfo[] = []; - private folderWorkspaces: URI[] = []; - private emptyWorkspaces: IEmptyWindowBackupInfo[] = []; + private workspaces: IWorkspaceBackupInfo[] = []; + private folders: URI[] = []; + private emptyWindows: IEmptyWindowBackupInfo[] = []; constructor( @IEnvironmentService environmentService: IEnvironmentService, @@ -51,31 +50,31 @@ export class BackupMainService implements IBackupMainService { // read empty workspaces backups first if (backups.emptyWorkspaceInfos) { - this.emptyWorkspaces = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos); + this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos); } else if (Array.isArray(backups.emptyWorkspaces)) { // read legacy entries - this.emptyWorkspaces = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(backupFolder => ({ backupFolder }))); + this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(emptyWindow => ({ backupFolder: emptyWindow }))); } // read workspace backups let rootWorkspaces: IWorkspaceBackupInfo[] = []; try { if (Array.isArray(backups.rootURIWorkspaces)) { - rootWorkspaces = backups.rootURIWorkspaces.map(f => ({ workspace: { id: f.id, configPath: URI.parse(f.configURIPath) }, remoteAuthority: f.remoteAuthority })); + rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) }, remoteAuthority: workspace.remoteAuthority })); } else if (Array.isArray(backups.rootWorkspaces)) { - rootWorkspaces = backups.rootWorkspaces.map(f => ({ workspace: { id: f.id, configPath: URI.file(f.configPath) } })); + rootWorkspaces = backups.rootWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } })); } } catch (e) { // ignore URI parsing exceptions } - this.rootWorkspaces = await this.validateWorkspaces(rootWorkspaces); + this.workspaces = await this.validateWorkspaces(rootWorkspaces); // read folder backups let workspaceFolders: URI[] = []; try { if (Array.isArray(backups.folderURIWorkspaces)) { - workspaceFolders = backups.folderURIWorkspaces.map(f => URI.parse(f)); + workspaceFolders = backups.folderURIWorkspaces.map(folder => URI.parse(folder)); } else if (Array.isArray(backups.folderWorkspaces)) { // migrate legacy folder paths workspaceFolders = []; @@ -93,7 +92,7 @@ export class BackupMainService implements IBackupMainService { // ignore URI parsing exceptions } - this.folderWorkspaces = await this.validateFolders(workspaceFolders); + this.folders = await this.validateFolders(workspaceFolders); // save again in case some workspaces or folders have been removed await this.save(); @@ -106,7 +105,7 @@ export class BackupMainService implements IBackupMainService { return []; } - return this.rootWorkspaces.slice(0); // return a copy + return this.workspaces.slice(0); // return a copy } getFolderBackupPaths(): URI[] { @@ -116,7 +115,7 @@ export class BackupMainService implements IBackupMainService { return []; } - return this.folderWorkspaces.slice(0); // return a copy + return this.folders.slice(0); // return a copy } isHotExitEnabled(): boolean { @@ -134,12 +133,12 @@ export class BackupMainService implements IBackupMainService { } getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] { - return this.emptyWorkspaces.slice(0); // return a copy + return this.emptyWindows.slice(0); // return a copy } registerWorkspaceBackupSync(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string { - if (!this.rootWorkspaces.some(window => workspaceInfo.workspace.id === window.workspace.id)) { - this.rootWorkspaces.push(workspaceInfo); + if (!this.workspaces.some(workspace => workspaceInfo.workspace.id === workspace.workspace.id)) { + this.workspaces.push(workspaceInfo); this.saveSync(); } @@ -188,16 +187,16 @@ export class BackupMainService implements IBackupMainService { unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void { const id = workspace.id; - let index = arrays.firstIndex(this.rootWorkspaces, w => w.workspace.id === id); + const index = this.workspaces.findIndex(workspace => workspace.workspace.id === id); if (index !== -1) { - this.rootWorkspaces.splice(index, 1); + this.workspaces.splice(index, 1); this.saveSync(); } } registerFolderBackupSync(folderUri: URI): string { - if (!this.folderWorkspaces.some(uri => areResourcesEquals(folderUri, uri))) { - this.folderWorkspaces.push(folderUri); + if (!this.folders.some(folder => areResourcesEquals(folderUri, folder))) { + this.folders.push(folderUri); this.saveSync(); } @@ -205,9 +204,9 @@ export class BackupMainService implements IBackupMainService { } unregisterFolderBackupSync(folderUri: URI): void { - let index = arrays.firstIndex(this.folderWorkspaces, uri => areResourcesEquals(folderUri, uri)); + const index = this.folders.findIndex(folder => areResourcesEquals(folderUri, folder)); if (index !== -1) { - this.folderWorkspaces.splice(index, 1); + this.folders.splice(index, 1); this.saveSync(); } } @@ -216,8 +215,8 @@ export class BackupMainService implements IBackupMainService { // Generate a new folder if this is a new empty workspace const backupFolder = backupFolderCandidate || this.getRandomEmptyWindowId(); - if (!this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, backupFolder, !platform.isLinux))) { - this.emptyWorkspaces.push({ backupFolder, remoteAuthority }); + if (!this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, backupFolder, !platform.isLinux))) { + this.emptyWindows.push({ backupFolder, remoteAuthority }); this.saveSync(); } @@ -225,9 +224,9 @@ export class BackupMainService implements IBackupMainService { } unregisterEmptyWindowBackupSync(backupFolder: string): void { - let index = arrays.firstIndex(this.emptyWorkspaces, w => !!w.backupFolder && isEqual(w.backupFolder, backupFolder, !platform.isLinux)); + const index = this.emptyWindows.findIndex(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, backupFolder, !platform.isLinux)); if (index !== -1) { - this.emptyWorkspaces.splice(index, 1); + this.emptyWindows.splice(index, 1); this.saveSync(); } } @@ -255,7 +254,7 @@ export class BackupMainService implements IBackupMainService { seenIds.add(workspace.id); const backupPath = this.getBackupPath(workspace.id); - const hasBackups = await this.hasBackups(backupPath); + const hasBackups = await this.doHasBackups(backupPath); // If the workspace has no backups, ignore it if (hasBackups) { @@ -287,7 +286,7 @@ export class BackupMainService implements IBackupMainService { seenIds.add(key); const backupPath = this.getBackupPath(this.getFolderHash(folderURI)); - const hasBackups = await this.hasBackups(backupPath); + const hasBackups = await this.doHasBackups(backupPath); // If the folder has no backups, ignore it if (hasBackups) { @@ -325,7 +324,7 @@ export class BackupMainService implements IBackupMainService { seenIds.add(backupFolder); const backupPath = this.getBackupPath(backupFolder); - if (await this.hasBackups(backupPath)) { + if (await this.doHasBackups(backupPath)) { result.push(backupInfo); } else { await this.deleteStaleBackup(backupPath); @@ -350,7 +349,7 @@ export class BackupMainService implements IBackupMainService { // New empty window backup let newBackupFolder = this.getRandomEmptyWindowId(); - while (this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, newBackupFolder, platform.isLinux))) { + while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, newBackupFolder, platform.isLinux))) { newBackupFolder = this.getRandomEmptyWindowId(); } @@ -362,7 +361,7 @@ export class BackupMainService implements IBackupMainService { this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); return false; } - this.emptyWorkspaces.push({ backupFolder: newBackupFolder }); + this.emptyWindows.push({ backupFolder: newBackupFolder }); return true; } @@ -371,7 +370,7 @@ export class BackupMainService implements IBackupMainService { // New empty window backup let newBackupFolder = this.getRandomEmptyWindowId(); - while (this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, newBackupFolder, platform.isLinux))) { + while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, newBackupFolder, platform.isLinux))) { newBackupFolder = this.getRandomEmptyWindowId(); } @@ -383,12 +382,53 @@ export class BackupMainService implements IBackupMainService { this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); return false; } - this.emptyWorkspaces.push({ backupFolder: newBackupFolder }); + this.emptyWindows.push({ backupFolder: newBackupFolder }); return true; } - private async hasBackups(backupPath: string): Promise { + async getDirtyWorkspaces(): Promise> { + const dirtyWorkspaces: Array = []; + + // Workspaces with backups + for (const workspace of this.workspaces) { + if ((await this.hasBackups(workspace))) { + dirtyWorkspaces.push(workspace.workspace); + } + } + + // Folders with backups + for (const folder of this.folders) { + if ((await this.hasBackups(folder))) { + dirtyWorkspaces.push(folder); + } + } + + return dirtyWorkspaces; + } + + private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | URI): Promise { + let backupPath: string; + + // Folder + if (URI.isUri(backupLocation)) { + backupPath = this.getBackupPath(this.getFolderHash(backupLocation)); + } + + // Workspace + else if (isWorkspaceBackupInfo(backupLocation)) { + backupPath = this.getBackupPath(backupLocation.workspace.id); + } + + // Empty + else { + backupPath = backupLocation.backupFolder; + } + + return this.doHasBackups(backupPath); + } + + private async doHasBackups(backupPath: string): Promise { try { const backupSchemas = await readdir(backupPath); @@ -427,10 +467,10 @@ export class BackupMainService implements IBackupMainService { private serializeBackups(): IBackupWorkspacesFormat { return { - rootURIWorkspaces: this.rootWorkspaces.map(f => ({ id: f.workspace.id, configURIPath: f.workspace.configPath.toString(), remoteAuthority: f.remoteAuthority })), - folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()), - emptyWorkspaceInfos: this.emptyWorkspaces, - emptyWorkspaces: this.emptyWorkspaces.map(info => info.backupFolder) + rootURIWorkspaces: this.workspaces.map(workspace => ({ id: workspace.workspace.id, configURIPath: workspace.workspace.configPath.toString(), remoteAuthority: workspace.remoteAuthority })), + folderURIWorkspaces: this.folders.map(folder => folder.toString()), + emptyWorkspaceInfos: this.emptyWindows, + emptyWorkspaces: this.emptyWindows.map(emptyWindow => emptyWindow.backupFolder) }; } diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 21bcc5f3cf..28d758f4a1 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -22,6 +22,7 @@ import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createHash } from 'crypto'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; suite('BackupMainService', () => { @@ -731,4 +732,45 @@ suite('BackupMainService', () => { } }); }); + + suite('getDirtyWorkspaces', () => { + test('should report if a workspace or folder has backups', async () => { + const folderBackupPath = service.registerFolderBackupSync(fooFile); + + const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath); + const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo); + + assert.equal(((await service.getDirtyWorkspaces()).length), 0); + + try { + await pfs.mkdirp(path.join(folderBackupPath, Schemas.file)); + await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled)); + } catch (error) { + // ignore - folder might exist already + } + + assert.equal(((await service.getDirtyWorkspaces()).length), 0); + + fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), ''); + fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), ''); + + const dirtyWorkspaces = await service.getDirtyWorkspaces(); + assert.equal(dirtyWorkspaces.length, 2); + + let found = 0; + for (const dirtyWorkpspace of dirtyWorkspaces) { + if (URI.isUri(dirtyWorkpspace)) { + if (isEqual(fooFile, dirtyWorkpspace)) { + found++; + } + } else { + if (isEqual(backupWorkspaceInfo.workspace.configPath, dirtyWorkpspace.configPath)) { + found++; + } + } + } + + assert.equal(found, 2); + }); + }); }); diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index b5febb04ec..3bafbe9766 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -54,8 +54,7 @@ export class BrowserClipboardService implements IClipboardService { } readFindText(): string { - // @ts-ignore - return undefined; + return undefined; // {{SQL CARBON EDIT}} strict-null-checks } writeFindText(text: string): void { } diff --git a/src/vs/platform/configuration/common/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts index 22195c31b5..8fb26a0b87 100644 --- a/src/vs/platform/configuration/common/configurationService.ts +++ b/src/vs/platform/configuration/common/configurationService.ts @@ -29,7 +29,6 @@ export class ConfigurationService extends Disposable implements IConfigurationSe fileService: IFileService ) { super(); - this._register(fileService.watch(settingsResource)); this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService)); this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel()); diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index 41cd03eb84..2a9da7611e 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -65,7 +65,8 @@ export class ElectronMainService implements IElectronMainService { workspace: window.openedWorkspace, folderUri: window.openedFolderUri, title: window.win.getTitle(), - filename: window.getRepresentedFilename() + filename: window.getRepresentedFilename(), + dirty: window.isDocumentEdited() })); } @@ -271,7 +272,7 @@ export class ElectronMainService implements IElectronMainService { async setDocumentEdited(windowId: number | undefined, edited: boolean): Promise { const window = this.windowById(windowId); if (window) { - window.win.setDocumentEdited(edited); + window.setDocumentEdited(edited); } } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 50032f3d67..9640e98158 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -95,7 +95,7 @@ export interface ParsedArgs { 'nolazy'?: boolean; 'force-device-scale-factor'?: string; 'force-renderer-accessibility'?: boolean; - 'ignore-certificate-error'?: boolean; + 'ignore-certificate-errors'?: boolean; 'allow-insecure-localhost'?: boolean; } @@ -154,6 +154,7 @@ export interface IEnvironmentService extends IUserHomeProvider { extensionsPath?: string; extensionDevelopmentLocationURI?: URI[]; extensionTestsLocationURI?: URI; + extensionEnabledProposedApi?: string[] | undefined; logExtensionHostCommunication?: boolean; debugExtensionHost: IExtensionHostDebugParams; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 476b4e31cf..8a7026b5a6 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as minimist from 'vscode-minimist'; +import * as minimist from 'minimist'; import * as os from 'os'; import { localize } from 'vs/nls'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; @@ -128,7 +128,7 @@ export const OPTIONS: OptionDescriptions> = { 'nolazy': { type: 'boolean' }, // node inspect 'force-device-scale-factor': { type: 'string' }, 'force-renderer-accessibility': { type: 'boolean' }, - 'ignore-certificate-error': { type: 'boolean' }, + 'ignore-certificate-errors': { type: 'boolean' }, 'allow-insecure-localhost': { type: 'boolean' }, '_urls': { type: 'string[]' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 1226d440ae..4ba758598b 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -233,6 +233,18 @@ export class EnvironmentService implements IEnvironmentService { return false; } + get extensionEnabledProposedApi(): string[] | undefined { + if (Array.isArray(this.args['enable-proposed-api'])) { + return this.args['enable-proposed-api']; + } + + if ('enable-proposed-api' in this.args) { + return []; + } + + return undefined; + } + @memoize get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this._args, this.isBuilt); } @memoize diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 3425e4f3e8..d5870f5508 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -26,6 +26,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ReadableStreamEvents, transform } from 'vs/base/common/stream'; import { createReadStream } from 'vs/platform/files/common/io'; +import { insert } from 'vs/base/common/arrays'; export interface IWatcherOptions { pollingInterval?: number; @@ -524,7 +525,7 @@ export class DiskFileSystemProvider extends Disposable implements // Add to list of folders to watch recursively const folderToWatch = { path: this.toFilePath(resource), excludes }; - this.recursiveFoldersToWatch.push(folderToWatch); + const remove = insert(this.recursiveFoldersToWatch, folderToWatch); // Trigger update this.refreshRecursiveWatchers(); @@ -532,7 +533,7 @@ export class DiskFileSystemProvider extends Disposable implements return toDisposable(() => { // Remove from list of folders to watch recursively - this.recursiveFoldersToWatch.splice(this.recursiveFoldersToWatch.indexOf(folderToWatch), 1); + remove(); // Trigger update this.refreshRecursiveWatchers(); @@ -543,10 +544,8 @@ export class DiskFileSystemProvider extends Disposable implements // Buffer requests for recursive watching to decide on right watcher // that supports potentially watching more than one folder at once - this.recursiveWatchRequestDelayer.trigger(() => { + this.recursiveWatchRequestDelayer.trigger(async () => { this.doRefreshRecursiveWatchers(); - - return Promise.resolve(); }); } diff --git a/src/vs/platform/files/node/watcher/win32/watcherService.ts b/src/vs/platform/files/node/watcher/win32/watcherService.ts index f5a5fb8b00..b22ac5b11d 100644 --- a/src/vs/platform/files/node/watcher/win32/watcherService.ts +++ b/src/vs/platform/files/node/watcher/win32/watcherService.ts @@ -6,7 +6,7 @@ import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { OutOfProcessWin32FolderWatcher } from 'vs/platform/files/node/watcher/win32/csharpWatcherService'; import { posix } from 'vs/base/common/path'; -import { rtrim, endsWith } from 'vs/base/common/strings'; +import { rtrim } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; export class FileWatcher implements IDisposable { @@ -22,7 +22,7 @@ export class FileWatcher implements IDisposable { ) { this.folder = folders[0]; - if (this.folder.path.indexOf('\\\\') === 0 && endsWith(this.folder.path, posix.sep)) { + 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 ("/"). diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index 61d301c111..c08d6a9d3a 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -11,7 +11,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import { localize } from 'vs/nls'; import { isEqualOrParent, basename } from 'vs/base/common/resources'; -import { endsWith } from 'vs/base/common/strings'; export interface ILabelService { _serviceBrand: undefined; @@ -61,7 +60,7 @@ export function getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, w } let filename = basename(workspace.configPath); - if (endsWith(filename, WORKSPACE_EXTENSION)) { + if (filename.endsWith(WORKSPACE_EXTENSION)) { filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } return localize('workspaceName', "{0} (Workspace)", filename); diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 8c8efb64c5..cc95e403d7 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -65,7 +65,7 @@ export interface INeverShowAgainOptions { /** * Whether to persist the choice in the current workspace or for all workspaces. By - * default it will be persisted for all workspaces. + * default it will be persisted for all workspaces (= `NeverShowAgainScope.GLOBAL`). */ readonly scope?: NeverShowAgainScope; } diff --git a/src/vs/platform/quickinput/browser/helpQuickAccess.ts b/src/vs/platform/quickinput/browser/helpQuickAccess.ts index 530c8da839..a317bf1eb9 100644 --- a/src/vs/platform/quickinput/browser/helpQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/helpQuickAccess.ts @@ -28,7 +28,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider { disposables.add(picker.onDidAccept(() => { const [item] = picker.selectedItems; if (item) { - this.quickInputService.quickAccess.show(item.prefix); + this.quickInputService.quickAccess.show(item.prefix, { preserveValue: true }); } })); @@ -37,7 +37,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider { disposables.add(picker.onDidChangeValue(value => { const providerDescriptor = this.registry.getQuickAccessProvider(value.substr(HelpQuickAccessProvider.PREFIX.length)); if (providerDescriptor && providerDescriptor.prefix && providerDescriptor.prefix !== HelpQuickAccessProvider.PREFIX) { - this.quickInputService.quickAccess.show(providerDescriptor.prefix); + this.quickInputService.quickAccess.show(providerDescriptor.prefix, { preserveValue: true }); } })); diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index bece73c1cc..ceac6566bf 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -11,14 +11,6 @@ import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cance import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { once } from 'vs/base/common/functional'; -interface IInternalQuickAccessOptions extends IQuickAccessOptions { - - /** - * Internal option to not rewrite the filter value at all but use it as is. - */ - preserveFilterValue?: boolean; -} - export class QuickAccessController extends Disposable implements IQuickAccessController { private readonly registry = Registry.as(Extensions.Quickaccess); @@ -39,7 +31,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon super(); } - show(value = '', options?: IInternalQuickAccessOptions): void { + show(value = '', options?: IQuickAccessOptions): void { // Find provider for the value to show const [provider, descriptor] = this.getOrInstantiateProvider(value); @@ -51,7 +43,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Apply value only if it is more specific than the prefix // from the provider and we are not instructed to preserve - if (value !== descriptor.prefix && !options?.preserveFilterValue) { + if (value !== descriptor.prefix && !options?.preserveValue) { visibleQuickAccess.picker.value = value; } @@ -62,7 +54,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } // Rewrite the filter value based on certain rules unless disabled - if (descriptor && !options?.preserveFilterValue) { + if (descriptor && !options?.preserveValue) { let newValue: string | undefined = undefined; // If we have a visible provider with a value, take it's filter value but @@ -116,11 +108,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon picker.show(); } - private adjustValueSelection(picker: IQuickPick, descriptor?: IQuickAccessProviderDescriptor, options?: IInternalQuickAccessOptions): void { + private adjustValueSelection(picker: IQuickPick, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void { let valueSelection: [number, number]; // Preserve: just always put the cursor at the end - if (options?.preserveFilterValue) { + if (options?.preserveValue) { valueSelection = [picker.value.length, picker.value.length]; } @@ -147,7 +139,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon disposables.add(picker.onDidChangeValue(value => { const [providerForValue] = this.getOrInstantiateProvider(value); if (providerForValue !== provider) { - this.show(value, { preserveFilterValue: true } /* do not rewrite value from user typing! */); + this.show(value, { preserveValue: true } /* do not rewrite value from user typing! */); } else { visibleQuickAccess.value = value; // remember the value in our visible one } diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index dd84531e06..088ea15901 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -6,8 +6,7 @@ import { IQuickPick, IQuickPickItem, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Registry } from 'vs/platform/registry/common/platform'; -import { first, coalesce } from 'vs/base/common/arrays'; -import { startsWith } from 'vs/base/common/strings'; +import { coalesce } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ItemActivation } from 'vs/base/parts/quickinput/common/quickInput'; @@ -22,7 +21,13 @@ export interface IQuickAccessOptions { * Allows to configure a different item activation strategy. * By default the first item in the list will get activated. */ - itemActivation?: ItemActivation + itemActivation?: ItemActivation; + + /** + * Wether to take the input value as is and not restore it + * from any existing value if quick access is visible. + */ + preserveValue?: boolean; } export interface IQuickAccessController { @@ -177,7 +182,7 @@ export class QuickAccessRegistry implements IQuickAccessRegistry { } getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined { - const result = prefix ? (first(this.providers, provider => startsWith(prefix, provider.prefix)) || undefined) : undefined; + const result = prefix ? (this.providers.find(provider => prefix.startsWith(provider.prefix)) || undefined) : undefined; return result || this.defaultProvider; } diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 36315f76b0..86c5b1f6d7 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -13,7 +13,6 @@ import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateR import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async'; -import { serializableToMap, mapToSerializable } from 'vs/base/common/map'; import { VSBuffer } from 'vs/base/common/buffer'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; @@ -291,7 +290,7 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase this.ensureWatching(); // now that the file must exist, ensure we watch it for changes - return serializableToMap(JSON.parse(itemsRaw.value.toString())); + return new Map(JSON.parse(itemsRaw.value.toString())); } async updateItems(request: IUpdateRequest): Promise { @@ -311,7 +310,7 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase try { this._hasPendingUpdate = true; - await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(mapToSerializable(items)))); + await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(Array.from(items.entries())))); this.ensureWatching(); // now that the file must exist, ensure we watch it for changes } finally { diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 473349f742..6defaff70e 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -7,7 +7,6 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event, Emitter } from 'vs/base/common/event'; import { IStorageChangeEvent, IStorageMainService } from 'vs/platform/storage/node/storageMainService'; import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage'; -import { mapToSerializable, serializableToMap, values } from 'vs/base/common/map'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { generateUuid } from 'vs/base/common/uuid'; @@ -117,7 +116,10 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC } }); - return { changed: mapToSerializable(changed), deleted: values(deleted) }; + return { + changed: Array.from(changed.entries()), + deleted: Array.from(deleted.values()) + }; } listen(_: unknown, event: string): Event { @@ -136,7 +138,7 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC // handle call switch (command) { case 'getItems': { - return mapToSerializable(this.storageMainService.items); + return Array.from(this.storageMainService.items.entries()); } case 'updateItems': { @@ -182,7 +184,7 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void { if (Array.isArray(e.changed) || Array.isArray(e.deleted)) { this._onDidChangeItemsExternal.fire({ - changed: e.changed ? serializableToMap(e.changed) : undefined, + changed: e.changed ? new Map(e.changed) : undefined, deleted: e.deleted ? new Set(e.deleted) : undefined }); } @@ -191,18 +193,18 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS async getItems(): Promise> { const items: Item[] = await this.channel.call('getItems'); - return serializableToMap(items); + return new Map(items); } updateItems(request: IUpdateRequest): Promise { const serializableRequest: ISerializableUpdateRequest = Object.create(null); if (request.insert) { - serializableRequest.insert = mapToSerializable(request.insert); + serializableRequest.insert = Array.from(request.insert.entries()); } if (request.delete) { - serializableRequest.delete = values(request.delete); + serializableRequest.delete = Array.from(request.delete.values()); } return this.channel.call('updateItems', serializableRequest); diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index 2685f642c4..f9add40e4d 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -28,7 +28,7 @@ export const fontStylePattern = '^(\\s*(-?italic|-?bold|-?underline))*\\s*$'; export interface TokenSelector { match(type: string, modifiers: string[], language: string): number; - readonly selectorString: string; + readonly id: string; } export interface TokenTypeOrModifierContribution { @@ -155,7 +155,7 @@ export namespace TokenStylingRule { } export function toJSONObject(rule: TokenStylingRule): any { return { - _selector: rule.selector.selectorString, + _selector: rule.selector.id, _style: TokenStyle.toJSONObject(rule.style) }; } @@ -164,7 +164,7 @@ export namespace TokenStylingRule { return true; } return r1 !== undefined && r2 !== undefined - && r1.selector && r2.selector && r1.selector.selectorString === r2.selector.selectorString + && r1.selector && r2.selector && r1.selector.id === r2.selector.id && TokenStyle.equals(r1.style, r2.style); } export function is(r: any): r is TokenStylingRule { @@ -203,10 +203,11 @@ export interface ITokenClassificationRegistry { /** * Parses a token selector from a selector string. * @param selectorString selector string in the form (*|type)(.modifier)* + * @param language language to which the selector applies or undefined if the selector is for all languafe * @returns the parsesd selector * @throws an error if the string is not a valid selector */ - parseTokenSelector(selectorString: string): TokenSelector; + parseTokenSelector(selectorString: string, language?: string): TokenSelector; /** * Register a TokenStyle default to the registry. @@ -335,13 +336,13 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { this.tokenStylingSchema.properties[`*.${id}`] = getStylingSchemeEntry(description, deprecationMessage); } - public parseTokenSelector(selectorString: string): TokenSelector { - const selector = parseClassifierString(selectorString); + public parseTokenSelector(selectorString: string, language?: string): TokenSelector { + const selector = parseClassifierString(selectorString, language); if (!selector.type) { return { match: () => -1, - selectorString + id: '$invalid' }; } @@ -352,7 +353,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { if (selector.language !== language) { return -1; } - score += 100; + score += 10; } if (selector.type !== TOKEN_TYPE_WILDCARD) { const hierarchy = this.getTypeHierarchy(type); @@ -370,7 +371,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { } return score + selector.modifiers.length * 100; }, - selectorString + id: `${[selector.type, ...selector.modifiers.sort()].join('.')}${selector.language !== undefined ? ':' + selector.language : ''}` }; } @@ -379,8 +380,8 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { } public deregisterTokenStyleDefault(selector: TokenSelector): void { - const selectorString = selector.selectorString; - this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.selectorString !== selectorString); + const selectorString = selector.id; + this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.id !== selectorString); } public deregisterTokenType(id: string): void { @@ -442,9 +443,11 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { const CHAR_LANGUAGE = TOKEN_CLASSIFIER_LANGUAGE_SEPARATOR.charCodeAt(0); const CHAR_MODIFIER = CLASSIFIER_MODIFIER_SEPARATOR.charCodeAt(0); -export function parseClassifierString(s: string): { type: string, modifiers: string[], language: string | undefined; } { +export function parseClassifierString(s: string, defaultLanguage: string): { type: string, modifiers: string[], language: string; }; +export function parseClassifierString(s: string, defaultLanguage?: string): { type: string, modifiers: string[], language: string | undefined; }; +export function parseClassifierString(s: string, defaultLanguage: string | undefined): { type: string, modifiers: string[], language: string | undefined; } { let k = s.length; - let language: string | undefined = undefined; + let language: string | undefined = defaultLanguage; const modifiers = []; for (let i = k - 1; i >= 0; i--) { diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 53ce3d9dee..db23a2d5fa 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; -import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -14,6 +13,10 @@ import Severity from 'vs/base/common/severity'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; +function uriGetComparisonKey(resource: URI): string { + return resource.toString(); +} + class ResourceStackElement { public readonly type = UndoRedoElementType.Resource; public readonly actual: IResourceUndoRedoElement; diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 1659494491..1bbd09340f 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources'; import { CancelablePromise } from 'vs/base/common/async'; @@ -144,6 +144,16 @@ export abstract class AbstractSynchroniser extends Disposable { } } + async getSyncPreview(): Promise { + if (!this.isEnabled()) { + return { hasLocalChanged: false, hasRemoteChanged: false }; + } + + const lastSyncUserData = await this.getLastSyncUserData(); + const remoteUserData = await this.getRemoteUserData(lastSyncUserData); + return this.generatePreview(remoteUserData, lastSyncUserData); + } + protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { // current version is not compatible with cloud version @@ -285,15 +295,14 @@ export abstract class AbstractSynchroniser extends Disposable { protected abstract readonly version: number; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; + protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; } -export interface IFileSyncPreviewResult { +export interface IFileSyncPreviewResult extends ISyncPreviewResult { readonly fileContent: IFileContent | null; readonly remoteUserData: IRemoteUserData; readonly lastSyncUserData: IRemoteUserData | null; readonly content: string | null; - readonly hasLocalChanged: boolean; - readonly hasRemoteChanged: boolean; readonly hasConflicts: boolean; } diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index f9526e27c9..bc95e82c16 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -20,7 +20,7 @@ import { joinPath, dirname, basename } from 'vs/base/common/resources'; import { format } from 'vs/base/common/jsonFormatter'; import { applyEdits } from 'vs/base/common/jsonEdit'; -interface ISyncPreviewResult { +interface IExtensionsSyncPreviewResult extends ISyncPreviewResult { readonly localExtensions: ISyncExtension[]; readonly remoteUserData: IRemoteUserData; readonly lastSyncUserData: ILastSyncUserData | null; @@ -82,7 +82,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const localExtensions = await this.getLocalExtensions(); const remoteExtensions = this.parseExtensions(remoteUserData.syncData); const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], this.getIgnoredExtensions()); - await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData }); + await this.apply({ + added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData, + hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0, + hasRemoteChanged: remote !== null + }); } // No remote exists to pull @@ -112,7 +116,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions()); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData }, true); + await this.apply({ + added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData, + hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0, + hasRemoteChanged: remote !== null + }, true); this.logService.info(`${this.syncResourceLogLabel}: Finished pushing extensions.`); } finally { @@ -163,12 +171,12 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const previewResult = await this.getPreview(remoteUserData, lastSyncUserData); + const previewResult = await this.generatePreview(remoteUserData, lastSyncUserData); await this.apply(previewResult); return SyncStatus.Idle; } - private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null; const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; @@ -183,22 +191,31 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions()); - return { added, removed, updated, remote, skippedExtensions, remoteUserData, localExtensions, lastSyncUserData }; + return { + added, + removed, + updated, + remote, + skippedExtensions, + remoteUserData, + localExtensions, + lastSyncUserData, + hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0, + hasRemoteChanged: remote !== null + }; } private getIgnoredExtensions() { return this.configurationService.getValue('sync.ignoredExtensions') || []; } - private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions }: ISyncPreviewResult, forcePush?: boolean): Promise { + private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreviewResult, forcePush?: boolean): Promise { - const hasChanges = added.length || removed.length || updated.length || remote; - - if (!hasChanges) { + if (!hasLocalChanged && !hasRemoteChanged) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`); } - if (added.length || removed.length || updated.length) { + if (hasLocalChanged) { // back up all disabled or market place extensions const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid); await this.backupLocal(JSON.stringify(backUpExtensions)); diff --git a/src/vs/platform/userDataSync/common/globalStateMerge.ts b/src/vs/platform/userDataSync/common/globalStateMerge.ts index d32ec46853..79c649f2cd 100644 --- a/src/vs/platform/userDataSync/common/globalStateMerge.ts +++ b/src/vs/platform/userDataSync/common/globalStateMerge.ts @@ -13,17 +13,18 @@ import { ILogService } from 'vs/platform/log/common/log'; export interface IMergeResult { local: { added: IStringDictionary, removed: string[], updated: IStringDictionary }; remote: IStringDictionary | null; + skipped: string[]; } -export function merge(localStorage: IStringDictionary, remoteStorage: IStringDictionary | null, baseStorage: IStringDictionary | null, storageKeys: ReadonlyArray, logService: ILogService): IMergeResult { +export function merge(localStorage: IStringDictionary, remoteStorage: IStringDictionary | null, baseStorage: IStringDictionary | null, storageKeys: ReadonlyArray, previouslySkipped: string[], logService: ILogService): IMergeResult { if (!remoteStorage) { - return { remote: localStorage, local: { added: {}, removed: [], updated: {} } }; + return { remote: localStorage, local: { added: {}, removed: [], updated: {} }, skipped: [] }; } const localToRemote = compare(localStorage, remoteStorage); if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { // No changes found between local and remote. - return { remote: null, local: { added: {}, removed: [], updated: {} } }; + return { remote: null, local: { added: {}, removed: [], updated: {} }, skipped: [] }; } const baseToRemote = baseStorage ? compare(baseStorage, remoteStorage) : { added: Object.keys(remoteStorage).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; @@ -31,17 +32,19 @@ export function merge(localStorage: IStringDictionary, remoteStor const local: { added: IStringDictionary, removed: string[], updated: IStringDictionary } = { added: {}, removed: [], updated: {} }; const remote: IStringDictionary = objects.deepClone(remoteStorage); + const skipped: string[] = []; // Added in remote for (const key of values(baseToRemote.added)) { const remoteValue = remoteStorage[key]; const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; if (!storageKey) { - logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`); + skipped.push(key); + logService.info(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`); continue; } if (storageKey.version !== remoteValue.version) { - logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`); + logService.info(`GlobalState: Skipped adding ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`); continue; } const localValue = localStorage[key]; @@ -60,11 +63,12 @@ export function merge(localStorage: IStringDictionary, remoteStor const remoteValue = remoteStorage[key]; const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; if (!storageKey) { - logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`); + skipped.push(key); + logService.info(`GlobalState: Skipped updating ${key} in local storage as is not registered.`); continue; } if (storageKey.version !== remoteValue.version) { - logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`); + logService.info(`GlobalState: Skipped updating ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`); continue; } const localValue = localStorage[key]; @@ -78,7 +82,7 @@ export function merge(localStorage: IStringDictionary, remoteStor for (const key of values(baseToRemote.removed)) { const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; if (!storageKey) { - logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`); + logService.info(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`); continue; } local.removed.push(key); @@ -99,6 +103,7 @@ export function merge(localStorage: IStringDictionary, remoteStor const remoteValue = remote[key]; const localValue = localStorage[key]; if (localValue.version < remoteValue.version) { + logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${localValue.version}' and remote version '${remoteValue.version} are not same.`); continue; } remote[key] = localValue; @@ -106,18 +111,36 @@ export function merge(localStorage: IStringDictionary, remoteStor // Removed in local for (const key of values(baseToLocal.removed)) { + // do not remove from remote if it is updated in remote if (baseToRemote.updated.has(key)) { continue; } - const remoteValue = remote[key]; + const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; - if (storageKey && storageKey.version < remoteValue.version) { + // do not remove from remote if storage key is not found + if (!storageKey) { + skipped.push(key); + logService.info(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`); continue; } + + const remoteValue = remote[key]; + // do not remove from remote if local data version is old + if (storageKey.version < remoteValue.version) { + logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`); + continue; + } + + // add to local if it was skipped before + if (previouslySkipped.indexOf(key) !== -1) { + local.added[key] = remote[key]; + continue; + } + delete remote[key]; } - return { local, remote: areSame(remote, remoteStorage) ? null : remote }; + return { local, remote: areSame(remote, remoteStorage) ? null : remote, skipped }; } function compare(from: IStringDictionary, to: IStringDictionary): { added: Set, removed: Set, updated: Set } { diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 2043bb3178..ba7cacd231 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -21,16 +21,22 @@ import { format } from 'vs/base/common/jsonFormatter'; import { applyEdits } from 'vs/base/common/jsonEdit'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys'; +import { equals } from 'vs/base/common/arrays'; const argvStoragePrefx = 'globalState.argv.'; const argvProperties: string[] = ['locale']; -interface ISyncPreviewResult { +interface IGlobalSyncPreviewResult extends ISyncPreviewResult { readonly local: { added: IStringDictionary, removed: string[], updated: IStringDictionary }; readonly remote: IStringDictionary | null; + readonly skippedStorageKeys: string[]; readonly localUserData: IGlobalState; readonly remoteUserData: IRemoteUserData; - readonly lastSyncUserData: IRemoteUserData | null; + readonly lastSyncUserData: ILastSyncUserData | null; +} + +interface ILastSyncUserData extends IRemoteUserData { + skippedStorageKeys: string[] | undefined; } export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { @@ -51,8 +57,16 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs ) { super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(this.environmentService.argvResource))); - this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire())); - this._register(Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key))(() => this._onDidChangeLocal.fire())); + this._register( + Event.any( + /* Locale change */ + Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)), + /* Storage change */ + Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)), + /* Storage key registered */ + this.storageKeysSyncRegistryService.onDidChangeStorageKeys + )((() => this._onDidChangeLocal.fire())) + ); } async pull(): Promise { @@ -67,14 +81,19 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.info(`${this.syncResourceLogLabel}: Started pulling ui state...`); this.setStatus(SyncStatus.Syncing); - const lastSyncUserData = await this.getLastSyncUserData(); + const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); if (remoteUserData.syncData !== null) { const localGlobalState = await this.getLocalGlobalState(); const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content); - const { local, remote } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), this.logService); - await this.apply({ local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData }); + const { local, remote, skipped } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); + await this.apply({ + local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData, + skippedStorageKeys: skipped, + hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0, + hasRemoteChanged: remote !== null + }); } // No remote exists to pull @@ -101,9 +120,14 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.setStatus(SyncStatus.Syncing); const localUserData = await this.getLocalGlobalState(); - const lastSyncUserData = await this.getLastSyncUserData(); + const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - await this.apply({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData }, true); + await this.apply({ + local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData, + skippedStorageKeys: [], + hasLocalChanged: false, + hasRemoteChanged: true + }, true); this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`); } finally { @@ -153,13 +177,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return false; } - protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { - const result = await this.getPreview(remoteUserData, lastSyncUserData); + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + const result = await this.generatePreview(remoteUserData, lastSyncUserData); await this.apply(result); return SyncStatus.Idle; } - private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; @@ -171,15 +195,17 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`); } - const { local, remote } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), this.logService); + const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); - return { local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData }; + return { + local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData, + skippedStorageKeys: skipped, + hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0, + hasRemoteChanged: remote !== null + }; } - private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData }: ISyncPreviewResult, forcePush?: boolean): Promise { - - const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0; - const hasRemoteChanged = remote !== null; + private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalSyncPreviewResult, forcePush?: boolean): Promise { if (!hasLocalChanged && !hasRemoteChanged) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`); @@ -201,10 +227,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`); } - if (lastSyncUserData?.ref !== remoteUserData.ref) { + if (lastSyncUserData?.ref !== remoteUserData.ref || !equals(lastSyncUserData.skippedStorageKeys, skippedStorageKeys)) { // update last sync this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`); - await this.updateLastSyncUserData(remoteUserData); + await this.updateLastSyncUserData(remoteUserData, { skippedStorageKeys }); this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`); } } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 4e4d2d9cad..fb20e6a9a3 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -265,7 +265,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return this.syncPreviewResultPromise; } - private async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise { const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; const lastSyncContent = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null; // Get file content last to get the latest diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 68cc805028..1a56d7cc81 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -309,14 +309,14 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { this.syncPreviewResultPromise = null; } - private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[]): Promise { + private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = []): Promise { if (!this.syncPreviewResultPromise) { this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, resolvedConflicts, token)); } return this.syncPreviewResultPromise; } - protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise { + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = [], token: CancellationToken = CancellationToken.None): Promise { const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index 2cbbf6e558..9cc5ab9f35 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -17,7 +17,7 @@ import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -interface ISyncPreviewResult { +interface ISinppetsSyncPreviewResult extends ISyncPreviewResult { readonly local: IStringDictionary; readonly remoteUserData: IRemoteUserData; readonly lastSyncUserData: IRemoteUserData | null; @@ -34,7 +34,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD protected readonly version: number = 1; private readonly snippetsFolder: URI; private readonly snippetsPreviewFolder: URI; - private syncPreviewResultPromise: CancelablePromise | null = null; + private syncPreviewResultPromise: CancelablePromise | null = null; constructor( @IEnvironmentService environmentService: IEnvironmentService, @@ -94,8 +94,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD const localSnippets = this.toSnippetsContents(local); const remoteSnippets = this.parseSnippets(remoteUserData.syncData); const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets); - this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ - added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {} + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, + hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, + hasRemoteChanged: remote !== null })); await this.apply(); } @@ -128,8 +130,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD const { added, removed, updated, remote } = merge(localSnippets, null, null); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ - added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {} + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, + hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, + hasRemoteChanged: remote !== null })); await this.apply(true); @@ -207,7 +211,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD let previewResult = await this.syncPreviewResultPromise!; this.cancel(); previewResult.resolvedConflicts[key] = content || null; - this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token)); + this.syncPreviewResultPromise = createCancelablePromise(token => this.doGeneratePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token)); previewResult = await this.syncPreviewResultPromise; this.setConflicts(previewResult.conflicts); if (!this.conflicts.length) { @@ -252,10 +256,9 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } } - private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { if (!this.syncPreviewResultPromise) { - this.syncPreviewResultPromise = createCancelablePromise(token => this.getSnippetsFileContents() - .then(local => this.generatePreview(local, remoteUserData, lastSyncUserData, {}, token))); + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token)); } return this.syncPreviewResultPromise; } @@ -274,7 +277,12 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } } - private async generatePreview(local: IStringDictionary, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary, token: CancellationToken): Promise { + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise { + return this.getSnippetsFileContents() + .then(local => this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token)); + } + + private async doGeneratePreview(local: IStringDictionary, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary = {}, token: CancellationToken = CancellationToken.None): Promise { const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; const lastSyncSnippets: IStringDictionary | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null; @@ -309,7 +317,18 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } } - return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote, resolvedConflicts }; + return { + remoteUserData, local, + lastSyncUserData, + added: mergeResult.added, + removed: mergeResult.removed, + updated: mergeResult.updated, + conflicts, + remote: mergeResult.remote, + resolvedConflicts, + hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0, + hasRemoteChanged: mergeResult.remote !== null + }; } private async apply(forcePush?: boolean): Promise { @@ -317,15 +336,13 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return; } - let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData } = await this.syncPreviewResultPromise; + let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise; - const hasChanges = Object.keys(added).length || removed.length || Object.keys(updated).length || remote; - - if (!hasChanges) { + if (!hasLocalChanged && !hasRemoteChanged) { this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`); } - if (Object.keys(added).length || removed.length || Object.keys(updated).length) { + if (hasLocalChanged) { // back up all snippets await this.backupLocal(JSON.stringify(this.toSnippetsContents(local))); await this.updateLocalSnippets(added, removed, updated, local); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index a6f43f528d..39fecbcfb0 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -254,6 +254,11 @@ export interface ISyncResourceHandle { export type Conflict = { remote: URI, local: URI }; +export interface ISyncPreviewResult { + readonly hasLocalChanged: boolean; + readonly hasRemoteChanged: boolean; +} + export interface IUserDataSynchroniser { readonly resource: SyncResource; @@ -268,6 +273,7 @@ export interface IUserDataSynchroniser { sync(ref?: string): Promise; stop(): Promise; + getSyncPreview(): Promise hasPreviouslySynced(): Promise hasLocalData(): Promise; resetLocal(): Promise; diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 881934db37..b4d3f8ef64 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -217,7 +217,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (await this.hasPreviouslySynced()) { return false; } - return await this.hasLocalData(); + if (!(await this.hasLocalData())) { + return false; + } + for (const synchroniser of [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser]) { + const preview = await synchroniser.getSyncPreview(); + if (preview.hasLocalChanged || preview.hasRemoteChanged) { + return true; + } + } + return false; } async reset(): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index c92473520e..4373f81ee1 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -12,7 +12,6 @@ import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/reques import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { IProductService } from 'vs/platform/product/common/productService'; -import { URI } from 'vs/base/common/uri'; import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; @@ -66,7 +65,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } const result = await asJson<{ url: string, created: number }[]>(context) || []; - return result.map(({ url, created }) => ({ ref: relativePath(uri, URI.parse(url).with({ scheme: uri.scheme, authority: uri.authority }))!, created: created * 1000 /* Server returns in seconds */ })); + return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ })); } async resolveContent(resource: SyncResource, ref: string): Promise { @@ -76,6 +75,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString(); const headers: IHeaders = {}; + headers['Cache-Control'] = 'no-cache'; const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None); diff --git a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts index 549f985830..0440726f6a 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateMerge.test.ts @@ -13,7 +13,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -25,7 +25,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -37,7 +37,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -50,7 +50,7 @@ suite('GlobalStateMerge', () => { const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; const base = { 'b': { version: 1, value: 'a' } }; - const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -62,7 +62,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } }); assert.deepEqual(actual.local.updated, {}); @@ -74,7 +74,7 @@ suite('GlobalStateMerge', () => { const local = {}; const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }); assert.deepEqual(actual.local.updated, {}); @@ -86,7 +86,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } }); assert.deepEqual(actual.local.updated, {}); @@ -98,7 +98,7 @@ suite('GlobalStateMerge', () => { const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; const remote = { 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -110,7 +110,7 @@ suite('GlobalStateMerge', () => { const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; const remote = {}; - const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -122,7 +122,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'a': { version: 1, value: 'b' } }; - const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } }); @@ -134,7 +134,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; const remote = { 'a': { version: 1, value: 'd' }, 'c': { version: 1, value: 'c' } }; - const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } }); assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'd' } }); @@ -146,7 +146,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; const remote = { 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -158,7 +158,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' }, 'c': { version: 1, value: 'c' } }; const remote = { 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -170,7 +170,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; - const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -182,7 +182,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'b' } }; const remote = { 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -194,7 +194,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'd' }, 'b': { version: 1, value: 'b' } }; const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } }; - const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -206,7 +206,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'a': { version: 1, value: 'b' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } }); @@ -219,7 +219,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'd' } }; const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } }; - const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } }); assert.deepEqual(actual.local.updated, {}); @@ -232,7 +232,7 @@ suite('GlobalStateMerge', () => { const local = {}; const remote = { 'a': { version: 1, value: 'b' } }; - const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } }); @@ -245,7 +245,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'd' } }; const remote = { 'a': { version: 1, value: 'b' } }; - const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService()); + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } }); @@ -257,7 +257,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -269,7 +269,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -281,7 +281,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'a': { version: 1, value: 'b' } }; - const actual = merge(local, remote, local, [], new NullLogService()); + const actual = merge(local, remote, local, [], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -293,7 +293,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -305,7 +305,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'c' } }; const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -317,7 +317,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 2, value: 'c' } }; const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService()); + const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -330,7 +330,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], new NullLogService()); + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -343,7 +343,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService()); + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -356,7 +356,7 @@ suite('GlobalStateMerge', () => { const local = { 'a': { version: 1, value: 'a' } }; const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; - const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService()); + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], [], new NullLogService()); assert.deepEqual(actual.local.added, {}); assert.deepEqual(actual.local.updated, {}); @@ -364,4 +364,17 @@ suite('GlobalStateMerge', () => { assert.deepEqual(actual.remote, local); }); + test('merge when a local value is not yet registered', async () => { + const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } }; + const local = { 'a': { version: 1, value: 'a' } }; + const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } }; + + const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], [], new NullLogService()); + + assert.deepEqual(actual.local.added, {}); + assert.deepEqual(actual.local.updated, {}); + assert.deepEqual(actual.local.removed, []); + assert.deepEqual(actual.remote, null); + }); + }); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 60cdc364e2..38ff99b72d 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; @@ -49,6 +49,10 @@ class TestSynchroniser extends AbstractSynchroniser { this.syncBarrier.open(); } + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + return { hasLocalChanged: false, hasRemoteChanged: false }; + } + } suite('TestSynchronizer', () => { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 0230612342..e63afc6fdb 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -103,17 +103,17 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te await testObject.pull(); assert.deepEqual(target.requests, [ - // Manifest + /* first time sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - // Settings { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, - // Snippets { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - // Global state + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + /* pull */ + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, ]); @@ -143,17 +143,14 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te await testObject.pull(); assert.deepEqual(target.requests, [ - // Manifest + /* first time sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - // Settings { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - // Keybindings + /* pull */ + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, - // Snippets { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, ]); @@ -178,18 +175,18 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te await testObject.sync(); assert.deepEqual(target.requests, [ - // Manifest + /* first time sync */ + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + /* sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - // Settings { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, - // Snippets { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, ]); @@ -220,21 +217,19 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te await testObject.sync(); assert.deepEqual(target.requests, [ - // Manifest + /* first time sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + + /* first time sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - // Settings { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, - // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, - // Snippets { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, - // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, ]); diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index a436ea3866..df2aa2b44a 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -15,6 +15,7 @@ export interface IOpenedWindow { folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; + dirty: boolean; } export interface IBaseOpenWindowsOptions { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 7659022753..656ae1e1e3 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -80,6 +80,9 @@ export interface ICodeWindow extends IDisposable { setRepresentedFilename(name: string): void; getRepresentedFilename(): string | undefined; + setDocumentEdited(edited: boolean): void; + isDocumentEdited(): boolean; + handleTitleDoubleClick(): void; updateTouchBar(items: ISerializableCommandAction[][]): void; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 7857e69918..0bb50862f8 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -454,16 +454,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) // - let foldersToRestore: URI[] = []; let workspacesToRestore: IWorkspacePathToOpen[] = []; if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) { - let foldersToRestore = this.backupMainService.getFolderBackupPaths(); - foldersToOpen.push(...foldersToRestore.map(f => ({ folderUri: f, remoteAuhority: getRemoteAuthority(f) }))); - // collect from workspaces with hot-exit backups and from previous window session - workspacesToRestore = [...this.backupMainService.getWorkspaceBackups(), ...this.workspacesMainService.getUntitledWorkspacesSync()]; + // Untitled workspaces are always restored + workspacesToRestore = this.workspacesMainService.getUntitledWorkspacesSync(); workspacesToOpen.push(...workspacesToRestore); + // Empty windows with backups are always restored emptyToRestore.push(...this.backupMainService.getEmptyWindowBackupPaths()); } else { emptyToRestore.length = 0; @@ -495,7 +493,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const usedWindow = usedWindows[i]; if ( (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace - (usedWindow.openedFolderUri && foldersToRestore.some(uri => isEqual(uri, usedWindow.openedFolderUri))) || // skip over restored folder (usedWindow.backupPath && emptyToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath))) // skip over restored empty window ) { continue; diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index fc6134c36e..c88bf25fb0 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; -import { extname } from 'vs/base/common/path'; +import { extname, isAbsolute } from 'vs/base/common/path'; import { dirname, resolvePath, isEqualAuthority, isEqualOrParent, relativePath, extname as resourceExtname } from 'vs/base/common/resources'; import * as jsonEdit from 'vs/base/common/jsonEdit'; import * as json from 'vs/base/common/json'; @@ -18,7 +18,7 @@ import { toSlashes } from 'vs/base/common/extpath'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { ILogService } from 'vs/platform/log/common/log'; -import { Event as CommonEvent } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const WORKSPACE_EXTENSION = 'code-workspace'; @@ -31,18 +31,21 @@ export interface IWorkspacesService { _serviceBrand: undefined; - // Management + // Workspaces Management enterWorkspace(path: URI): Promise; createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; getWorkspaceIdentifier(workspacePath: URI): Promise; - // History - readonly onRecentlyOpenedChange: CommonEvent; + // Workspaces History + readonly onRecentlyOpenedChange: Event; addRecentlyOpened(recents: IRecent[]): Promise; removeRecentlyOpened(workspaces: URI[]): Promise; clearRecentlyOpened(): Promise; getRecentlyOpened(): Promise; + + // Dirty Workspaces + getDirtyWorkspaces(): Promise>; } export interface IRecentlyOpened { @@ -203,21 +206,22 @@ const SLASH = '/'; * Undefined is returned if the folderURI and the targetConfigFolderURI don't have the same schema or authority * * @param folderURI a workspace folder + * @param forceAbsolute if set, keep the path absolute * @param folderName a workspace name * @param targetConfigFolderURI the folder where the workspace is living in * @param useSlashForPath if set, use forward slashes for file paths on windows */ -export function getStoredWorkspaceFolder(folderURI: URI, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder { +export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder { if (folderURI.scheme !== targetConfigFolderURI.scheme) { return { name: folderName, uri: folderURI.toString(true) }; } - let folderPath: string | undefined; - if (isEqualOrParent(folderURI, targetConfigFolderURI)) { - // use relative path - folderPath = relativePath(targetConfigFolderURI, folderURI) || '.'; // always uses forward slashes - if (isWindows && folderURI.scheme === Schemas.file && !useSlashForPath) { + let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined; + if (folderPath !== undefined) { + if (folderPath.length === 0) { + folderPath = '.'; + } else if (isWindows && folderURI.scheme === Schemas.file && !useSlashForPath) { // Windows gets special treatment: // - use backslahes unless slash is used by other existing folders folderPath = folderPath.replace(/\//g, '\\'); @@ -249,7 +253,7 @@ export function getStoredWorkspaceFolder(folderURI: URI, folderName: string | un * Rewrites the content of a workspace file to be saved at a new location. * Throws an exception if file is not a valid workspace file */ -export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, targetConfigPathURI: URI) { +export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) { let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents); const sourceConfigFolder = dirname(configPathURI); @@ -258,12 +262,17 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, const rewrittenFolders: IStoredWorkspaceFolder[] = []; const slashForPath = useSlashForPath(storedWorkspace.folders); - // Rewrite absolute paths to relative paths if the target workspace folder - // is a parent of the location of the workspace file itself. Otherwise keep - // using absolute paths. for (const folder of storedWorkspace.folders) { - let folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); - rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, folder.name, targetConfigFolder, slashForPath)); + const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); + let absolute; + if (isFromUntitledWorkspace) { + // if it was an untitled workspace, try to make paths relative + absolute = false; + } else { + // for existing workspaces, preserve whether a path was absolute or relative + absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); + } + rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath)); } // Preserve as much of the existing workspace as possible by using jsonEdit diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 02cd191cc8..b1bc83909b 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -9,7 +9,6 @@ import { IStateService } from 'vs/platform/state/node/state'; import { app, JumpListCategory } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { getBaseLabel, getPathLabel, splitName } from 'vs/base/common/labels'; -import { IPath } from 'vs/platform/windows/common/windows'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/workspaces/common/workspaces'; @@ -24,6 +23,7 @@ import { exists } from 'vs/base/node/pfs'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; export const IWorkspacesHistoryMainService = createDecorator('workspacesHistoryMainService'); @@ -34,7 +34,7 @@ export interface IWorkspacesHistoryMainService { readonly onRecentlyOpenedChange: CommonEvent; addRecentlyOpened(recents: IRecent[]): void; - getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened; + getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened; removeRecentlyOpened(paths: URI[]): void; clearRecentlyOpened(): void; @@ -241,20 +241,23 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa this._onRecentlyOpenedChange.fire(); } - getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened { + getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened { const workspaces: Array = []; const files: IRecentFile[] = []; // Add current workspace to beginning if set + const currentWorkspace = include?.config?.workspace; if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) { workspaces.push({ workspace: currentWorkspace }); } + const currentFolder = include?.config?.folderUri; if (currentFolder) { workspaces.push({ folderUri: currentFolder }); } // Add currently files to open to the beginning if any + const currentFiles = include?.config?.filesToOpenOrCreate; if (currentFiles) { for (let currentFile of currentFiles) { const fileUri = currentFile.fileUri; @@ -402,14 +405,14 @@ function location(recent: IRecent): URI { return recent.workspace.configPath; } -function indexOfWorkspace(arr: IRecent[], workspace: IWorkspaceIdentifier): number { - return arrays.firstIndex(arr, w => isRecentWorkspace(w) && w.workspace.id === workspace.id); +function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): number { + return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id); } -function indexOfFolder(arr: IRecent[], folderURI: ISingleFolderWorkspaceIdentifier): number { - return arrays.firstIndex(arr, f => isRecentFolder(f) && areResourcesEqual(f.folderUri, folderURI)); +function indexOfFolder(arr: IRecent[], candidate: ISingleFolderWorkspaceIdentifier): number { + return arr.findIndex(folder => isRecentFolder(folder) && areResourcesEqual(folder.folderUri, candidate)); } -function indexOfFile(arr: IRecentFile[], fileURI: URI): number { - return arrays.firstIndex(arr, f => areResourcesEqual(f.fileUri, fileURI)); +function indexOfFile(arr: IRecentFile[], candidate: URI): number { + return arr.findIndex(file => areResourcesEqual(file.fileUri, candidate)); } diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index b9f2840c7b..2565f4195e 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -175,7 +175,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; for (const folder of folders) { - storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, folder.name, untitledWorkspaceConfigFolder)); + storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder)); } return { diff --git a/src/vs/platform/workspaces/electron-main/workspacesService.ts b/src/vs/platform/workspaces/electron-main/workspacesService.ts index 5936b99651..f2c7e570dd 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesService.ts @@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; +import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; export class WorkspacesService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { @@ -17,7 +18,8 @@ export class WorkspacesService implements AddFirstParameterToFunctions { - const window = this.windowsMainService.getWindowById(windowId); - if (window?.config) { - return this.workspacesHistoryMainService.getRecentlyOpened(window.config.workspace, window.config.folderUri, window.config.filesToOpenOrCreate); - } - - return this.workspacesHistoryMainService.getRecentlyOpened(); + return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId)); } async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise { @@ -72,4 +69,13 @@ export class WorkspacesService implements AddFirstParameterToFunctions> { + return this.backupMainService.getDirtyWorkspaces(); + } + + //#endregion } diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index da450e880e..481febcdac 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -11,7 +11,7 @@ import * as pfs from 'vs/base/node/pfs'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService'; -import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { NullLogService } from 'vs/platform/log/common/log'; import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; @@ -56,6 +56,7 @@ export class TestDialogMainService implements IDialogMainService { } export class TestBackupMainService implements IBackupMainService { + _serviceBrand: undefined; isHotExitEnabled(): boolean { @@ -97,6 +98,10 @@ export class TestBackupMainService implements IBackupMainService { unregisterEmptyWindowBackupSync(backupFolder: string): void { throw new Error('Method not implemented.'); } + + async getDirtyWorkspaces(): Promise<(IWorkspaceIdentifier | URI)[]> { + return []; + } } suite('WorkspacesMainService', () => { @@ -109,11 +114,27 @@ suite('WorkspacesMainService', () => { } } - function createWorkspace(folders: string[], names?: string[]) { + function createUntitledWorkspace(folders: string[], names?: string[]) { return service.createUntitledWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); } - function createWorkspaceSync(folders: string[], names?: string[]) { + function createWorkspace(workspaceConfigPath: string, folders: (string | URI)[], names?: string[]): void { + + const ws: IStoredWorkspace = { + folders: [] + }; + for (let i = 0; i < folders.length; i++) { + const f = folders[i]; + const s: IStoredWorkspaceFolder = f instanceof URI ? { uri: f.toString() } : { path: f }; + if (names) { + s.name = names[i]; + } + ws.folders.push(s); + } + fs.writeFileSync(workspaceConfigPath, JSON.stringify(ws)); + } + + function createUntitledWorkspaceSync(folders: string[], names?: string[]) { return service.createUntitledWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); } @@ -149,7 +170,7 @@ suite('WorkspacesMainService', () => { } test('createWorkspace (folders)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); assert.ok(workspace); assert.ok(fs.existsSync(workspace.configPath.fsPath)); assert.ok(service.isUntitledWorkspace(workspace)); @@ -163,7 +184,7 @@ suite('WorkspacesMainService', () => { }); test('createWorkspace (folders with name)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']); assert.ok(workspace); assert.ok(fs.existsSync(workspace.configPath.fsPath)); assert.ok(service.isUntitledWorkspace(workspace)); @@ -195,7 +216,7 @@ suite('WorkspacesMainService', () => { }); test('createWorkspaceSync (folders)', () => { - const workspace = createWorkspaceSync([process.cwd(), os.tmpdir()]); + const workspace = createUntitledWorkspaceSync([process.cwd(), os.tmpdir()]); assert.ok(workspace); assert.ok(fs.existsSync(workspace.configPath.fsPath)); assert.ok(service.isUntitledWorkspace(workspace)); @@ -210,7 +231,7 @@ suite('WorkspacesMainService', () => { }); test('createWorkspaceSync (folders with names)', () => { - const workspace = createWorkspaceSync([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']); + const workspace = createUntitledWorkspaceSync([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']); assert.ok(workspace); assert.ok(fs.existsSync(workspace.configPath.fsPath)); assert.ok(service.isUntitledWorkspace(workspace)); @@ -243,7 +264,7 @@ suite('WorkspacesMainService', () => { }); test('resolveWorkspaceSync', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); assert.ok(service.resolveLocalWorkspaceSync(workspace.configPath)); // make it a valid workspace path @@ -262,7 +283,7 @@ suite('WorkspacesMainService', () => { }); test('resolveWorkspaceSync (support relative paths)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] })); const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); @@ -270,7 +291,7 @@ suite('WorkspacesMainService', () => { }); test('resolveWorkspaceSync (support relative paths #2)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] })); const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); @@ -278,7 +299,7 @@ suite('WorkspacesMainService', () => { }); test('resolveWorkspaceSync (support relative paths #3)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] })); const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); @@ -286,7 +307,7 @@ suite('WorkspacesMainService', () => { }); test('resolveWorkspaceSync (support invalid JSON via fault tolerant parsing)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); fs.writeFileSync(workspace.configPath.fsPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); @@ -296,14 +317,15 @@ suite('WorkspacesMainService', () => { test('rewriteWorkspaceFileForNewLocation', async () => { const folder1 = process.cwd(); // absolute path because outside of tmpDir const tmpDir = os.tmpdir(); - const tmpInsideDir = path.join(os.tmpdir(), 'inside'); + const tmpInsideDir = path.join(tmpDir, 'inside'); - const workspace = await createWorkspace([folder1, tmpInsideDir, path.join(tmpInsideDir, 'somefolder')]); - const origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); + const firstConfigPath = path.join(tmpDir, 'myworkspace0.code-workspace'); + createWorkspace(firstConfigPath, [folder1, 'inside', path.join('inside', 'somefolder')]); + const origContent = fs.readFileSync(firstConfigPath).toString(); - let origConfigPath = workspace.configPath; + let origConfigPath = URI.file(firstConfigPath); let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace')); - let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, workspaceConfigPath); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath); let ws = (JSON.parse(newContent) as IStoredWorkspace); assert.equal(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); // absolute path because outside of tmpdir @@ -312,7 +334,7 @@ suite('WorkspacesMainService', () => { origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); ws = (JSON.parse(newContent) as IStoredWorkspace); assert.equal(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); @@ -321,51 +343,51 @@ suite('WorkspacesMainService', () => { origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); ws = (JSON.parse(newContent) as IStoredWorkspace); assert.equal(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); - assertPathEquals((ws.folders[1]).path, tmpInsideDir); - assertPathEquals((ws.folders[2]).path, path.join(tmpInsideDir, 'somefolder')); + assertPathEquals((ws.folders[1]).path, isWindows ? '..\\inside' : '../inside'); + assertPathEquals((ws.folders[2]).path, isWindows ? '..\\inside\\somefolder' : '../inside/somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace'); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); ws = (JSON.parse(newContent) as IStoredWorkspace); assert.equal(ws.folders.length, 3); assert.equal((ws.folders[0]).uri, URI.file(folder1).toString(true)); assert.equal((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); assert.equal((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); - service.deleteUntitledWorkspaceSync(workspace); + fs.unlinkSync(firstConfigPath); }); test('rewriteWorkspaceFileForNewLocation (preserves comments)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]); const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = `// this is a comment\n${origContent}`; - let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); assert.equal(0, newContent.indexOf('// this is a comment')); service.deleteUntitledWorkspaceSync(workspace); }); test('rewriteWorkspaceFileForNewLocation (preserves forward slashes)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]); const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); const ws = (JSON.parse(newContent) as IStoredWorkspace); assert.ok(ws.folders.every(f => (f).path.indexOf('\\') < 0)); service.deleteUntitledWorkspaceSync(workspace); }); - test('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { + test.skip('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { if (!isWindows) { return Promise.resolve(); } @@ -375,10 +397,10 @@ suite('WorkspacesMainService', () => { const folder2Location = '\\\\server\\share2\\some\\path'; const folder3Location = path.join(os.tmpdir(), 'wsloc', 'inner', 'more'); - const workspace = await createWorkspace([folder1Location, folder2Location, folder3Location]); + const workspace = await createUntitledWorkspace([folder1Location, folder2Location, folder3Location]); const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); const ws = (JSON.parse(newContent) as IStoredWorkspace); assertPathEquals((ws.folders[0]).path, folder1Location); assertPathEquals((ws.folders[1]).path, folder2Location); @@ -388,14 +410,14 @@ suite('WorkspacesMainService', () => { }); test('deleteUntitledWorkspaceSync (untitled)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); assert.ok(fs.existsSync(workspace.configPath.fsPath)); service.deleteUntitledWorkspaceSync(workspace); assert.ok(!fs.existsSync(workspace.configPath.fsPath)); }); test('deleteUntitledWorkspaceSync (saved)', async () => { - const workspace = await createWorkspace([process.cwd(), os.tmpdir()]); + const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); service.deleteUntitledWorkspaceSync(workspace); }); @@ -405,14 +427,14 @@ suite('WorkspacesMainService', () => { let untitled = service.getUntitledWorkspacesSync(); assert.equal(untitled.length, 0); - const untitledOne = await createWorkspace([process.cwd(), os.tmpdir()]); + const untitledOne = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); assert.ok(fs.existsSync(untitledOne.configPath.fsPath)); untitled = service.getUntitledWorkspacesSync(); assert.equal(1, untitled.length); assert.equal(untitledOne.id, untitled[0].workspace.id); - const untitledTwo = await createWorkspace([os.tmpdir(), process.cwd()]); + const untitledTwo = await createUntitledWorkspace([os.tmpdir(), process.cwd()]); assert.ok(fs.existsSync(untitledTwo.configPath.fsPath)); assert.ok(fs.existsSync(untitledOne.configPath.fsPath), `Unexpected workspaces count of 1 (expected 2): ${untitledOne.configPath.fsPath} does not exist anymore?`); const untitledHome = dirname(dirname(untitledTwo.configPath)); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 2f0e0155a8..0120b66a0e 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3150,6 +3150,258 @@ declare module 'vscode' { prepareRename?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } + /** + * A semantic tokens legend contains the needed information to decipher + * the integer encoded representation of semantic tokens. + */ + export class SemanticTokensLegend { + /** + * The possible token types. + */ + public readonly tokenTypes: string[]; + /** + * The possible token modifiers. + */ + public readonly tokenModifiers: string[]; + + constructor(tokenTypes: string[], tokenModifiers: string[]); + } + + /** + * A semantic tokens builder can help with creating a `SemanticTokens` instance + * which contains delta encoded semantic tokens. + */ + export class SemanticTokensBuilder { + + constructor(legend?: SemanticTokensLegend); + + /** + * Add another token. + * + * @param line The token start line number (absolute value). + * @param char The token start character (absolute value). + * @param length The token length in characters. + * @param tokenType The encoded token type. + * @param tokenModifiers The encoded token modifiers. + */ + push(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void; + + /** + * Add another token. Use only when providing a legend. + * + * @param range The range of the token. Must be single-line. + * @param tokenType The token type. + * @param tokenModifiers The token modifiers. + */ + push(range: Range, tokenType: string, tokenModifiers?: string[]): void; + + /** + * Finish and create a `SemanticTokens` instance. + */ + build(resultId?: string): SemanticTokens; + } + + /** + * Represents semantic tokens, either in a range or in an entire document. + * See [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens) for an explanation of the format. + * See `SemanticTokensBuilder` for a helper to create an instance. + */ + export class SemanticTokens { + /** + * The result id of the tokens. + * + * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). + */ + readonly resultId?: string; + /** + * The actual tokens data. + * See [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens) for an explanation of the format. + */ + readonly data: Uint32Array; + + constructor(data: Uint32Array, resultId?: string); + } + + /** + * Represents edits to semantic tokens. + * See [provideDocumentSemanticTokensEdits](#DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits) for an explanation of the format. + */ + export class SemanticTokensEdits { + /** + * The result id of the tokens. + * + * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). + */ + readonly resultId?: string; + /** + * The edits to the tokens data. + * All edits refer to the initial data state. + */ + readonly edits: SemanticTokensEdit[]; + + constructor(edits: SemanticTokensEdit[], resultId?: string); + } + + /** + * Represents an edit to semantic tokens. + * See [provideDocumentSemanticTokensEdits](#DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits) for an explanation of the format. + */ + export class SemanticTokensEdit { + /** + * The start offset of the edit. + */ + readonly start: number; + /** + * The count of elements to remove. + */ + readonly deleteCount: number; + /** + * The elements to insert. + */ + readonly data?: Uint32Array; + + constructor(start: number, deleteCount: number, data?: Uint32Array); + } + + /** + * The document semantic tokens provider interface defines the contract between extensions and + * semantic tokens. + */ + export interface DocumentSemanticTokensProvider { + /** + * An optional event to signal that the semantic tokens from this provider have changed. + */ + onDidChangeSemanticTokens?: Event; + + /** + * A file can contain many tokens, perhaps even hundreds of thousands of tokens. Therefore, to improve + * the memory consumption around describing semantic tokens, we have decided to avoid allocating an object + * for each token and we represent tokens from a file as an array of integers. Furthermore, the position + * of each token is expressed relative to the token before it because most tokens remain stable relative to + * each other when edits are made in a file. + * + * --- + * In short, each token takes 5 integers to represent, so a specific token `i` in the file consists of the following array indices: + * - at index `5*i` - `deltaLine`: token line number, relative to the previous token + * - at index `5*i+1` - `deltaStart`: token start character, relative to the previous token (relative to 0 or the previous token's start if they are on the same line) + * - at index `5*i+2` - `length`: the length of the token. A token cannot be multiline. + * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticTokensLegend.tokenTypes`. We currently ask that `tokenType` < 65536. + * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticTokensLegend.tokenModifiers` + * + * --- + * ### How to encode tokens + * + * Here is an example for encoding a file with 3 tokens in a uint32 array: + * ``` + * { line: 2, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, + * { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, + * { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } + * ``` + * + * 1. First of all, a legend must be devised. This legend must be provided up-front and capture all possible token types. + * For this example, we will choose the following legend which must be passed in when registering the provider: + * ``` + * tokenTypes: ['property', 'type', 'class'], + * tokenModifiers: ['private', 'static'] + * ``` + * + * 2. The first transformation step is to encode `tokenType` and `tokenModifiers` as integers using the legend. Token types are looked + * up by index, so a `tokenType` value of `1` means `tokenTypes[1]`. Multiple token modifiers can be set by using bit flags, + * so a `tokenModifier` value of `3` is first viewed as binary `0b00000011`, which means `[tokenModifiers[0], tokenModifiers[1]]` because + * bits 0 and 1 are set. Using this legend, the tokens now are: + * ``` + * { line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, + * { line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 }, + * { line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } + * ``` + * + * 3. The next step is to represent each token relative to the previous token in the file. In this case, the second token + * is on the same line as the first token, so the `startChar` of the second token is made relative to the `startChar` + * of the first token, so it will be `10 - 5`. The third token is on a different line than the second token, so the + * `startChar` of the third token will not be altered: + * ``` + * { deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, + * { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 }, + * { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } + * ``` + * + * 4. Finally, the last step is to inline each of the 5 fields for a token in a single array, which is a memory friendly representation: + * ``` + * // 1st token, 2nd token, 3rd token + * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] + * ``` + * + * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. + * *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'. + */ + provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): ProviderResult; + + /** + * Instead of always returning all the tokens in a file, it is possible for a `DocumentSemanticTokensProvider` to implement + * this method (`updateSemanticTokens`) and then return incremental updates to the previously provided semantic tokens. + * + * --- + * ### How tokens change when the document changes + * + * Let's look at how tokens might change. + * + * Continuing with the above example, suppose a new line was inserted at the top of the file. + * That would make all the tokens move down by one line (notice how the line has changed for each one): + * ``` + * { line: 3, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, + * { line: 3, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, + * { line: 6, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } + * ``` + * The integer encoding of the tokens does not change substantially because of the delta-encoding of positions: + * ``` + * // 1st token, 2nd token, 3rd token + * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] + * ``` + * It is possible to express these new tokens in terms of an edit applied to the previous tokens: + * ``` + * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens + * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // new tokens + * + * edit: { start: 0, deleteCount: 1, data: [3] } // replace integer at offset 0 with 3 + * ``` + * + * Furthermore, let's assume that a new token has appeared on line 4: + * ``` + * { line: 3, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, + * { line: 3, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, + * { line: 4, startChar: 3, length: 5, tokenType: "property", tokenModifiers: ["static"] }, + * { line: 6, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } + * ``` + * The integer encoding of the tokens is: + * ``` + * // 1st token, 2nd token, 3rd token, 4th token + * [ 3,5,3,0,3, 0,5,4,1,0, 1,3,5,0,2, 2,2,7,2,0, ] + * ``` + * Again, it is possible to express these new tokens in terms of an edit applied to the previous tokens: + * ``` + * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens + * [ 3,5,3,0,3, 0,5,4,1,0, 1,3,5,0,2, 2,2,7,2,0, ] // new tokens + * + * edit: { start: 10, deleteCount: 1, data: [1,3,5,0,2,2] } // replace integer at offset 10 with [1,3,5,0,2,2] + * ``` + * + * *NOTE*: If the provider cannot compute `SemanticTokensEdits`, it can "give up" and return all the tokens in the document again. + * *NOTE*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state. + */ + provideDocumentSemanticTokensEdits?(document: TextDocument, previousResultId: string, token: CancellationToken): ProviderResult; + } + + /** + * The document range semantic tokens provider interface defines the contract between extensions and + * semantic tokens. + */ + export interface DocumentRangeSemanticTokensProvider { + /** + * See [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens). + */ + provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; + } + /** * Value-object describing what options formatting should use. */ @@ -6582,6 +6834,290 @@ declare module 'vscode' { deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable; } + /** + * Provider for text based custom editors. + * + * Text based custom editors use a [`TextDocument`](#TextDocument) as their data model. This considerably simplifies + * implementing a custom editor as it allows VS Code to handle many common operations such as + * undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`. + * + * You should use text based custom editors when dealing with text based file formats, such as `xml` or `json`. + * For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider). + */ + export interface CustomTextEditorProvider { + + /** + * Resolve a custom editor for a given text resource. + * + * This is called when a user first opens a resource for a `CustomTextEditorProvider`, or if they reopen an + * existing editor using this `CustomTextEditorProvider`. + * + * To resolve a custom editor, the provider must fill in its initial html content and hook up all + * the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later, + * for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. + * + * @param document Document for the resource to resolve. + * @param webviewPanel Webview to resolve. + * @param token A cancellation token that indicates the result is no longer needed. + * + * @return Thenable indicating that the custom editor has been resolved. + */ + resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable | void; + } + + /** + * Defines the editing capability of a custom editor. This allows the custom editor to hook into standard + * editor events such as `undo` or `save`. + * + * @param EditType Type of edits used for the documents this delegate handles. + */ + interface CustomEditorEditingDelegate { + /** + * Save the resource. + * + * @param document Document to save. + * @param cancellation Token that signals the save is no longer required (for example, if another save was triggered). + * + * @return Thenable signaling that the save has completed. + */ + save(document: CustomDocument, cancellation: CancellationToken): Thenable; + + /** + * Save the existing resource at a new path. + * + * @param document Document to save. + * @param targetResource Location to save to. + * + * @return Thenable signaling that the save has completed. + */ + saveAs(document: CustomDocument, targetResource: Uri): Thenable; + + /** + * Event triggered by extensions to signal to VS Code that an edit has occurred. + */ + readonly onDidEdit: Event>; + + /** + * Apply a set of edits. + * + * Note that is not invoked when `onDidEdit` is called because `onDidEdit` implies also updating the view to reflect the edit. + * + * @param document Document to apply edits to. + * @param edit Array of edits. Sorted from oldest to most recent. + * + * @return Thenable signaling that the change has completed. + */ + applyEdits(document: CustomDocument, edits: ReadonlyArray): Thenable; + + /** + * Undo a set of edits. + * + * This is triggered when a user undoes an edit. + * + * @param document Document to undo edits from. + * @param edit Array of edits. Sorted from most recent to oldest. + * + * @return Thenable signaling that the change has completed. + */ + undoEdits(document: CustomDocument, edits: ReadonlyArray): Thenable; + + /** + * Revert the file to its last saved state. + * + * @param document Document to revert. + * @param edits Added or applied edits. + * + * @return Thenable signaling that the change has completed. + */ + revert(document: CustomDocument, edits: CustomDocumentRevert): Thenable; + + /** + * Back up the resource in its current state. + * + * Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in + * its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in + * the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource, + * your extension should first check to see if any backups exist for the resource. If there is a backup, your + * extension should load the file contents from there instead of from the resource in the workspace. + * + * `backup` is triggered whenever an edit it made. Calls to `backup` are debounced so that if multiple edits are + * made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when + * `auto save` is enabled (since auto save already persists resource ). + * + * @param document Document to backup. + * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your + * extension to decided how to respond to cancellation. If for example your extension is backing up a large file + * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather + * than cancelling it to ensure that VS Code has some valid backup. + */ + backup(document: CustomDocument, cancellation: CancellationToken): Thenable; + } + + /** + * Event triggered by extensions to signal to VS Code that an edit has occurred on a `CustomDocument`. + * + * @param EditType Type of edits used for the document. + */ + interface CustomDocumentEditEvent { + /** + * Document the edit is for. + */ + readonly document: CustomDocument; + + /** + * Object that describes the edit. + * + * Edit objects are passed back to your extension in `CustomEditorEditingDelegate.undoEdits`, + * `CustomEditorEditingDelegate.applyEdits`, and `CustomEditorEditingDelegate.revert`. + */ + readonly edit: EditType; + + /** + * Display name describing the edit. + */ + readonly label?: string; + } + + /** + * Delta for edits undone/redone while reverting for a `CustomDocument`. + * + * @param EditType Type of edits used for the document being reverted. + */ + interface CustomDocumentRevert { + /** + * List of edits that were undone to get the document back to its on disk state. + */ + readonly undoneEdits: ReadonlyArray; + + /** + * List of edits that were reapplied to get the document back to its on disk state. + */ + readonly appliedEdits: ReadonlyArray; + } + + /** + * Represents a custom document used by a [`CustomEditorProvider`](#CustomEditorProvider). + * + * All custom documents must subclass `CustomDocument`. Custom documents are only used within a given + * `CustomEditorProvider`. The lifecycle of a `CustomDocument` is managed by VS Code. When no more references + * remain to a `CustomDocument`, it is disposed of. + * + * @param EditType Type of edits used in this document. + */ + class CustomDocument { + /** + * @param uri The associated resource for this document. + */ + constructor(uri: Uri); + + /** + * The associated uri for this document. + */ + readonly uri: Uri; + + /** + * Is this document representing an untitled file which has never been saved yet. + */ + readonly isUntitled: boolean; + + /** + * The version number of this document (it will strictly increase after each + * change, including undo/redo). + */ + readonly version: number; + + /** + * `true` if there are unpersisted changes. + */ + readonly isDirty: boolean; + + /** + * List of edits from document open to the document's current state. + * + * `appliedEdits` returns a copy of the edit stack at the current point in time. Your extension should always + * use `CustomDocument.appliedEdits` to check the edit stack instead of holding onto a reference to `appliedEdits`. + */ + readonly appliedEdits: ReadonlyArray; + + /** + * List of edits from document open to the document's last saved point. + * + * The save point will be behind `appliedEdits` if the user saves and then continues editing, + * or in front of the last entry in `appliedEdits` if the user saves and then hits undo. + * + * `savedEdits` returns a copy of the edit stack at the current point in time. Your extension should always + * use `CustomDocument.savedEdits` to check the edit stack instead of holding onto a reference to `savedEdits`. + */ + readonly savedEdits: ReadonlyArray; + + /** + * `true` if the document has been closed. A closed document isn't synchronized anymore + * and won't be reused when the same resource is opened again. + */ + readonly isClosed: boolean; + + /** + * Event fired when there are no more references to the `CustomDocument`. + * + * This happens when all custom editors for the document have been closed. Once a `CustomDocument` is disposed, + * it will not be reused when the same resource is opened again. + */ + readonly onDidDispose: Event; + } + + /** + * Provider for custom editors that use a custom document model. + * + * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). + * This gives extensions full control over actions such as edit, save, and backup. + * + * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple + * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. + * + * @param EditType Type of edits used by the editors of this provider. + */ + export interface CustomEditorProvider { + + /** + * Create a new document for a given resource. + * + * `openCustomDocument` is called when the first editor for a given resource is opened, and the resolve document + * is passed to `resolveCustomEditor`. The resolved `CustomDocument` is re-used for subsequent editor opens. + * If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at + * this point will trigger another call to `openCustomDocument`. + * + * @param uri Uri of the document to open. + * @param token A cancellation token that indicates the result is no longer needed. + * + * @return The custom document. + */ + openCustomDocument(uri: Uri, token: CancellationToken): Thenable> | CustomDocument; + + /** + * Resolve a custom editor for a given resource. + * + * This is called whenever the user opens a new editor for this `CustomEditorProvider`. + * + * To resolve a custom editor, the provider must fill in its initial html content and hook up all + * the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later, + * for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. + * + * @param document Document for the resource being resolved. + * @param webviewPanel Webview to resolve. + * @param token A cancellation token that indicates the result is no longer needed. + * + * @return Optional thenable indicating that the custom editor has been resolved. + */ + resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable | void; + + /** + * Defines the editing capability of the provider. + * + * When not provided, editors for this provider are considered readonly. + */ + readonly editingDelegate?: CustomEditorEditingDelegate; + } + /** * The clipboard provides read and write access to the system's clipboard. */ @@ -7418,6 +7954,24 @@ declare module 'vscode' { * @param serializer Webview serializer. */ export function registerWebviewPanelSerializer(viewType: string, serializer: WebviewPanelSerializer): Disposable; + + /** + * Register a new provider for a custom editor. + * + * @param viewType Type of the custom editor provider. This should match the `viewType` from the + * `package.json` contributions. + * @param provider Provider that resolves custom editors. + * @param options Options for the provider. + * + * @return Disposable that unregisters the provider. + */ + export function registerCustomEditorProvider( + viewType: string, + provider: CustomEditorProvider | CustomTextEditorProvider, + options?: { + readonly webviewOptions?: WebviewPanelOptions; + } + ): Disposable; } /** @@ -9291,6 +9845,32 @@ declare module 'vscode' { */ export function registerRenameProvider(selector: DocumentSelector, provider: RenameProvider): Disposable; + /** + * Register a semantic tokens provider for a whole document. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A document semantic tokens provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerDocumentSemanticTokensProvider(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; + + /** + * Register a semantic tokens provider for a document range. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A document range semantic tokens provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerDocumentRangeSemanticTokensProvider(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; + /** * Register a formatting provider for a document. * diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 70b95168bc..5152315479 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -247,222 +247,6 @@ declare module 'vscode' { //#endregion - //#region Semantic tokens: https://github.com/microsoft/vscode/issues/86415 - - export class SemanticTokensLegend { - public readonly tokenTypes: string[]; - public readonly tokenModifiers: string[]; - - constructor(tokenTypes: string[], tokenModifiers: string[]); - } - - export class SemanticTokensBuilder { - constructor(); - push(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void; - build(): Uint32Array; - } - - export class SemanticTokens { - /** - * The result id of the tokens. - * - * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). - */ - readonly resultId?: string; - readonly data: Uint32Array; - - constructor(data: Uint32Array, resultId?: string); - } - - export class SemanticTokensEdits { - /** - * The result id of the tokens. - * - * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). - */ - readonly resultId?: string; - readonly edits: SemanticTokensEdit[]; - - constructor(edits: SemanticTokensEdit[], resultId?: string); - } - - export class SemanticTokensEdit { - readonly start: number; - readonly deleteCount: number; - readonly data?: Uint32Array; - - constructor(start: number, deleteCount: number, data?: Uint32Array); - } - - /** - * The document semantic tokens provider interface defines the contract between extensions and - * semantic tokens. - */ - export interface DocumentSemanticTokensProvider { - /** - * An optional event to signal that the semantic tokens from this provider have changed. - */ - onDidChangeSemanticTokens?: Event; - - /** - * A file can contain many tokens, perhaps even hundreds of thousands of tokens. Therefore, to improve - * the memory consumption around describing semantic tokens, we have decided to avoid allocating an object - * for each token and we represent tokens from a file as an array of integers. Furthermore, the position - * of each token is expressed relative to the token before it because most tokens remain stable relative to - * each other when edits are made in a file. - * - * --- - * In short, each token takes 5 integers to represent, so a specific token `i` in the file consists of the following array indices: - * - at index `5*i` - `deltaLine`: token line number, relative to the previous token - * - at index `5*i+1` - `deltaStart`: token start character, relative to the previous token (relative to 0 or the previous token's start if they are on the same line) - * - at index `5*i+2` - `length`: the length of the token. A token cannot be multiline. - * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticTokensLegend.tokenTypes`. We currently ask that `tokenType` < 65536. - * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticTokensLegend.tokenModifiers` - * - * --- - * ### How to encode tokens - * - * Here is an example for encoding a file with 3 tokens in a uint32 array: - * ``` - * { line: 2, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, - * { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, - * { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } - * ``` - * - * 1. First of all, a legend must be devised. This legend must be provided up-front and capture all possible token types. - * For this example, we will choose the following legend which must be passed in when registering the provider: - * ``` - * tokenTypes: ['property', 'type', 'class'], - * tokenModifiers: ['private', 'static'] - * ``` - * - * 2. The first transformation step is to encode `tokenType` and `tokenModifiers` as integers using the legend. Token types are looked - * up by index, so a `tokenType` value of `1` means `tokenTypes[1]`. Multiple token modifiers can be set by using bit flags, - * so a `tokenModifier` value of `3` is first viewed as binary `0b00000011`, which means `[tokenModifiers[0], tokenModifiers[1]]` because - * bits 0 and 1 are set. Using this legend, the tokens now are: - * ``` - * { line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, - * { line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 }, - * { line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } - * ``` - * - * 3. The next step is to represent each token relative to the previous token in the file. In this case, the second token - * is on the same line as the first token, so the `startChar` of the second token is made relative to the `startChar` - * of the first token, so it will be `10 - 5`. The third token is on a different line than the second token, so the - * `startChar` of the third token will not be altered: - * ``` - * { deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, - * { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 }, - * { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } - * ``` - * - * 4. Finally, the last step is to inline each of the 5 fields for a token in a single array, which is a memory friendly representation: - * ``` - * // 1st token, 2nd token, 3rd token - * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] - * ``` - * - * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. - * *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'. - */ - provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): ProviderResult; - - /** - * Instead of always returning all the tokens in a file, it is possible for a `DocumentSemanticTokensProvider` to implement - * this method (`updateSemanticTokens`) and then return incremental updates to the previously provided semantic tokens. - * - * --- - * ### How tokens change when the document changes - * - * Let's look at how tokens might change. - * - * Continuing with the above example, suppose a new line was inserted at the top of the file. - * That would make all the tokens move down by one line (notice how the line has changed for each one): - * ``` - * { line: 3, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, - * { line: 3, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, - * { line: 6, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } - * ``` - * The integer encoding of the tokens does not change substantially because of the delta-encoding of positions: - * ``` - * // 1st token, 2nd token, 3rd token - * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] - * ``` - * It is possible to express these new tokens in terms of an edit applied to the previous tokens: - * ``` - * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens - * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // new tokens - * - * edit: { start: 0, deleteCount: 1, data: [3] } // replace integer at offset 0 with 3 - * ``` - * - * Furthermore, let's assume that a new token has appeared on line 4: - * ``` - * { line: 3, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, - * { line: 3, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, - * { line: 4, startChar: 3, length: 5, tokenType: "property", tokenModifiers: ["static"] }, - * { line: 6, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } - * ``` - * The integer encoding of the tokens is: - * ``` - * // 1st token, 2nd token, 3rd token, 4th token - * [ 3,5,3,0,3, 0,5,4,1,0, 1,3,5,0,2, 2,2,7,2,0, ] - * ``` - * Again, it is possible to express these new tokens in terms of an edit applied to the previous tokens: - * ``` - * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens - * [ 3,5,3,0,3, 0,5,4,1,0, 1,3,5,0,2, 2,2,7,2,0, ] // new tokens - * - * edit: { start: 10, deleteCount: 1, data: [1,3,5,0,2,2] } // replace integer at offset 10 with [1,3,5,0,2,2] - * ``` - * - * *NOTE*: If the provider cannot compute `SemanticTokensEdits`, it can "give up" and return all the tokens in the document again. - * *NOTE*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state. - */ - provideDocumentSemanticTokensEdits?(document: TextDocument, previousResultId: string, token: CancellationToken): ProviderResult; - } - - /** - * The document range semantic tokens provider interface defines the contract between extensions and - * semantic tokens. - */ - export interface DocumentRangeSemanticTokensProvider { - /** - * See [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens). - */ - provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; - } - - export namespace languages { - /** - * Register a semantic tokens provider for a whole document. - * - * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure - * of the selected provider will cause a failure of the whole operation. - * - * @param selector A selector that defines the documents this provider is applicable to. - * @param provider A document semantic tokens provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - */ - export function registerDocumentSemanticTokensProvider(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; - - /** - * Register a semantic tokens provider for a document range. - * - * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure - * of the selected provider will cause a failure of the whole operation. - * - * @param selector A selector that defines the documents this provider is applicable to. - * @param provider A document range semantic tokens provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - */ - export function registerDocumentRangeSemanticTokensProvider(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; - } - - //#endregion - //#region editor insets: https://github.com/microsoft/vscode/issues/85682 export interface WebviewEditorInset { @@ -945,7 +729,7 @@ declare module 'vscode' { //#region LogLevel: https://github.com/microsoft/vscode/issues/85992 /** - * The severity level of a log message + * @deprecated DO NOT USE, will be removed */ export enum LogLevel { Trace = 1, @@ -959,12 +743,12 @@ declare module 'vscode' { export namespace env { /** - * Current logging level. + * @deprecated DO NOT USE, will be removed */ export const logLevel: LogLevel; /** - * An [event](#Event) that fires when the log level has changed. + * @deprecated DO NOT USE, will be removed */ export const onDidChangeLogLevel: Event; } @@ -1125,7 +909,7 @@ declare module 'vscode' { /** * Handles a link that is activated within the terminal. * - * @return Whether the link was handled, the link was handled this link will not be + * @return Whether the link was handled, if the link was handled this link will not be * considered by any other extension or by the default built-in link handler. */ handleLink(terminal: Terminal, link: string): ProviderResult; @@ -1398,288 +1182,14 @@ declare module 'vscode' { //#endregion - //#region Custom editors: https://github.com/microsoft/vscode/issues/77131 + //#region Custom editor move https://github.com/microsoft/vscode/issues/86146 - /** - * Defines the editing capability of a custom webview editor. This allows the webview editor to hook into standard - * editor events such as `undo` or `save`. - * - * @param EditType Type of edits used for the documents this delegate handles. - */ - interface CustomEditorEditingDelegate { - /** - * Save the resource. - * - * @param document Document to save. - * @param cancellation Token that signals the save is no longer required (for example, if another save was triggered). - * - * @return Thenable signaling that the save has completed. - */ - save(document: CustomDocument, cancellation: CancellationToken): Thenable; + // TODO: Also for custom editor - /** - * Save the existing resource at a new path. - * - * @param document Document to save. - * @param targetResource Location to save to. - * - * @return Thenable signaling that the save has completed. - */ - saveAs(document: CustomDocument, targetResource: Uri): Thenable; - - /** - * Event triggered by extensions to signal to VS Code that an edit has occurred. - */ - readonly onDidEdit: Event>; - - /** - * Apply a set of edits. - * - * Note that is not invoked when `onDidEdit` is called because `onDidEdit` implies also updating the view to reflect the edit. - * - * @param document Document to apply edits to. - * @param edit Array of edits. Sorted from oldest to most recent. - * - * @return Thenable signaling that the change has completed. - */ - applyEdits(document: CustomDocument, edits: readonly EditType[]): Thenable; - - /** - * Undo a set of edits. - * - * This is triggered when a user undoes an edit. - * - * @param document Document to undo edits from. - * @param edit Array of edits. Sorted from most recent to oldest. - * - * @return Thenable signaling that the change has completed. - */ - undoEdits(document: CustomDocument, edits: readonly EditType[]): Thenable; - - /** - * Revert the file to its last saved state. - * - * @param document Document to revert. - * @param edits Added or applied edits. - * - * @return Thenable signaling that the change has completed. - */ - revert(document: CustomDocument, edits: CustomDocumentRevert): Thenable; - - /** - * Back up the resource in its current state. - * - * Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in - * its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in - * the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource, - * your extension should first check to see if any backups exist for the resource. If there is a backup, your - * extension should load the file contents from there instead of from the resource in the workspace. - * - * `backup` is triggered whenever an edit it made. Calls to `backup` are debounced so that if multiple edits are - * made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when - * `auto save` is enabled (since auto save already persists resource ). - * - * @param document Document to revert. - * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your - * extension to decided how to respond to cancellation. If for example your extension is backing up a large file - * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather - * than cancelling it to ensure that VS Code has some valid backup. - */ - backup(document: CustomDocument, cancellation: CancellationToken): Thenable; - } - - /** - * Event triggered by extensions to signal to VS Code that an edit has occurred on a `CustomDocument`. - * - * @param EditType Type of edits used for the document. - */ - interface CustomDocumentEditEvent { - /** - * Document the edit is for. - */ - readonly document: CustomDocument; - - /** - * Object that describes the edit. - * - * Edit objects are passed back to your extension in `CustomEditorEditingDelegate.undoEdits`, - * `CustomEditorEditingDelegate.applyEdits`, and `CustomEditorEditingDelegate.revert`. - */ - readonly edit: EditType; - - /** - * Display name describing the edit. - */ - readonly label?: string; - } - - /** - * Data about a revert for a `CustomDocument`. - */ - interface CustomDocumentRevert { - /** - * List of edits that were undone to get the document back to its on disk state. - */ - readonly undoneEdits: readonly EditType[]; - - /** - * List of edits that were reapplied to get the document back to its on disk state. - */ - readonly appliedEdits: readonly EditType[]; - } - - /** - * Represents a custom document used by a `CustomEditorProvider`. - * - * All custom documents must subclass `CustomDocument`. Custom documents are only used within a given - * `CustomEditorProvider`. The lifecycle of a `CustomDocument` is managed by VS Code. When no more references - * remain to a `CustomDocument`, it is disposed of. - * - * @param EditType Type of edits used in this document. - */ - class CustomDocument { - /** - * @param viewType The associated uri for this document. - * @param uri The associated viewType for this document. - */ - constructor(viewType: string, uri: Uri); - - /** - * The associated viewType for this document. - */ - readonly viewType: string; - - /** - * The associated uri for this document. - */ - readonly uri: Uri; - - /** - * Is this document representing an untitled file which has never been saved yet. - */ - readonly isUntitled: boolean; - - /** - * The version number of this document (it will strictly increase after each - * change, including undo/redo). - */ - readonly version: number; - - /** - * `true` if there are unpersisted changes. - */ - readonly isDirty: boolean; - - /** - * List of edits from document open to the document's current state. - */ - readonly appliedEdits: ReadonlyArray; - - /** - * List of edits from document open to the document's last saved point. - * - * The save point will be behind `appliedEdits` if the user saves and then continues editing, - * or in front of the last entry in `appliedEdits` if the user saves and then hits undo. - */ - readonly savedEdits: ReadonlyArray; - - /** - * `true` if the document has been closed. A closed document isn't synchronized anymore - * and won't be re-used when the same resource is opened again. - */ - readonly isClosed: boolean; - - /** - * Event fired when there are no more references to the `CustomDocument`. - */ - readonly onDidDispose: Event; - } - - /** - * Provider for webview editors that use a custom data model. - * - * Custom webview editors use [`CustomDocument`](#CustomDocument) as their data model. - * This gives extensions full control over actions such as edit, save, and backup. - * - * You should use custom text based editors when dealing with binary files or more complex scenarios. For simple text - * based documents, use [`WebviewTextEditorProvider`](#WebviewTextEditorProvider) instead. - */ - export interface CustomEditorProvider { - - /** - * Resolve the model for a given resource. - * - * `resolveCustomDocument` is called when the first editor for a given resource is opened, and the resolve document - * is passed to `resolveCustomEditor`. The resolved `CustomDocument` is re-used for subsequent editor opens. - * If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at - * this point will trigger another call to `resolveCustomDocument`. - * - * @param uri Uri of the document to open. - * @param token A cancellation token that indicates the result is no longer needed. - * - * @return The custom document. - */ - openCustomDocument(uri: Uri, token: CancellationToken): Thenable>; - - /** - * Resolve a webview editor for a given resource. - * - * This is called when a user first opens a resource for a `CustomEditorProvider`, or if they reopen an - * existing editor using this `CustomEditorProvider`. - * - * To resolve a webview editor, the provider must fill in its initial html content and hook up all - * the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later, - * for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details - * - * @param document Document for the resource being resolved. - * @param webviewPanel Webview to resolve. - * @param token A cancellation token that indicates the result is no longer needed. - * - * @return Thenable indicating that the webview editor has been resolved. - */ - resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable; - - /** - * Defines the editing capability of a custom webview document. - * - * When not provided, the document is considered readonly. - */ - readonly editingDelegate?: CustomEditorEditingDelegate; - } - - /** - * Provider for text based webview editors. - * - * Text based webview editors use a [`TextDocument`](#TextDocument) as their data model. This considerably simplifies - * implementing a webview editor as it allows VS Code to handle many common operations such as - * undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`. - * - * You should use text based webview editors when dealing with text based file formats, such as `xml` or `json`. - * For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider). - */ export interface CustomTextEditorProvider { - /** - * Resolve a webview editor for a given text resource. - * - * This is called when a user first opens a resource for a `CustomTextEditorProvider`, or if they reopen an - * existing editor using this `CustomTextEditorProvider`. - * - * To resolve a webview editor, the provider must fill in its initial html content and hook up all - * the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later, - * for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. - * - * @param document Document for the resource to resolve. - * @param webviewPanel Webview to resolve. - * @param token A cancellation token that indicates the result is no longer needed. - * - * @return Thenable indicating that the webview editor has been resolved. - */ - resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable; /** - * TODO: discuss this at api sync. - * * Handle when the underlying resource for a custom editor is renamed. * * This allows the webview for the editor be preserved throughout the rename. If this method is not implemented, @@ -1694,26 +1204,6 @@ declare module 'vscode' { moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel, token: CancellationToken): Thenable; } - namespace window { - /** - * Register a new provider for a custom editor. - * - * @param viewType Type of the webview editor provider. This should match the `viewType` from the - * `package.json` contributions. - * @param provider Provider that resolves editors. - * @param options Options for the provider - * - * @return Disposable that unregisters the provider. - */ - export function registerCustomEditorProvider( - viewType: string, - provider: CustomEditorProvider | CustomTextEditorProvider, - options?: { - readonly webviewOptions?: WebviewPanelOptions; - } - ): Disposable; - } - //#endregion @@ -1811,7 +1301,7 @@ declare module 'vscode' { runnable?: boolean; /** - * Execution order information of the cell + * The order in which this cell was executed. */ executionOrder?: number; } @@ -1828,22 +1318,27 @@ declare module 'vscode' { export interface NotebookDocumentMetadata { /** * Controls if users can add or delete cells - * Default to true + * Defaults to true */ - editable: boolean; + editable?: boolean; /** * Default value for [cell editable metadata](#NotebookCellMetadata.editable). - * Default to true. + * Defaults to true. */ - cellEditable: boolean; + cellEditable?: boolean; /** * Default value for [cell runnable metadata](#NotebookCellMetadata.runnable). - * Default to true. + * Defaults to true. */ - cellRunnable: boolean; + cellRunnable?: boolean; + /** + * Whether the [execution order](#NotebookCellMetadata.executionOrder) indicator will be displayed. + * Defaults to true. + */ + hasExecutionOrder?: boolean; } export interface NotebookDocument { @@ -1853,7 +1348,7 @@ declare module 'vscode' { readonly cells: NotebookCell[]; languages: string[]; displayOrder?: GlobPattern[]; - metadata?: NotebookDocumentMetadata; + metadata: NotebookDocumentMetadata; } export interface NotebookEditorCellEdit { @@ -1882,7 +1377,7 @@ declare module 'vscode' { export interface NotebookProvider { resolveNotebook(editor: NotebookEditor): Promise; - executeCell(document: NotebookDocument, cell: NotebookCell | undefined): Promise; + executeCell(document: NotebookDocument, cell: NotebookCell | undefined, token: CancellationToken): Promise; save(document: NotebookDocument): Promise; } @@ -2155,41 +1650,6 @@ declare module 'vscode' { //#endregion - - //#region https://github.com/microsoft/vscode/issues/90208 - - export interface ExtensionContext { - /** - * Get the uri of a resource contained in the extension. - * - * @param relativePath A relative path to a resource contained in the extension. - * @return The uri of the resource. - */ - asExtensionUri(relativePath: string): Uri; - - /** - * - */ - readonly extensionUri: Uri; - } - - export interface Extension { - /** - * Get the uri of a resource contained in the extension. - * - * @param relativePath A relative path to a resource contained in the extension. - * @return The uri of the resource. - */ - asExtensionUri(relativePath: string): Uri; - - /** - * - */ - readonly extensionUri: Uri; - } - - //#endregion - //#region https://github.com/microsoft/vscode/issues/86788 export interface CodeActionProviderMetadata { @@ -2247,13 +1707,44 @@ declare module 'vscode' { //#region https://github.com/microsoft/vscode/issues/90208 + export interface ExtensionContext { + /** + * @deprecated THIS API PROPOSAL WILL BE DROPPED + */ + asExtensionUri(relativePath: string): Uri; + + /** + * The uri of the directory containing the extension. + */ + readonly extensionUri: Uri; + } + + export interface Extension { + /** + * @deprecated THIS API PROPOSAL WILL BE DROPPED + */ + asExtensionUri(relativePath: string): Uri; + + /** + * The uri of the directory containing the extension. + */ + readonly extensionUri: Uri; + } + export namespace Uri { /** + * Create a new uri which path is the result of joining + * the path of the base uri with the provided path fragments. * - * @param base - * @param pathFragments - * @returns A new uri + * * Note that `joinPath` only affects the path component + * and all other components (scheme, authority, query, and fragment) are + * left as they are. + * * Note that the base uri must have a path; an error is thrown otherwise. + * + * @param base An uri. Must have a path. + * @param pathFragments One more more path fragments + * @returns A new uri which path is joined with the given fragments */ export function joinPath(base: Uri, ...pathFragments: string[]): Uri; } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index f97730d468..f1864ca179 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -12,7 +12,7 @@ import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContex import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import Severity from 'vs/base/common/severity'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -53,6 +53,7 @@ export class MainThreadAuthenticationProvider extends Disposable { private _sessionMenuItems = new Map(); private _accounts = new Map(); // Map account name to session ids private _sessions = new Map(); // Map account id to name + private _signInMenuItem: IMenuItem | undefined; constructor( private readonly _proxy: ExtHostAuthenticationShape, @@ -94,7 +95,9 @@ export class MainThreadAuthenticationProvider extends Disposable { quickPick.show(); } - private registerCommandsAndContextMenuItems(): void { + private async registerCommandsAndContextMenuItems(): Promise { + const sessions = await this._proxy.$getSessions(this.id); + if (this.dependents.length) { this._register(CommandsRegistry.registerCommand({ id: `signIn${this.id}`, @@ -103,19 +106,21 @@ export class MainThreadAuthenticationProvider extends Disposable { }, })); - this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + this._signInMenuItem = { group: '2_providers', command: { id: `signIn${this.id}`, - title: nls.localize('addAccount', "Sign in to {0}", this.displayName) + title: sessions.length + ? nls.localize('addAnotherAccount', "Sign in to another {0} account", this.displayName) + : nls.localize('addAccount', "Sign in to {0}", this.displayName) }, order: 3 - })); + }; + + this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, this._signInMenuItem)); } - this._proxy.$getSessions(this.id).then(sessions => { - sessions.forEach(session => this.registerSession(session)); - }); + sessions.forEach(session => this.registerSession(session)); } private registerSession(session: modes.AuthenticationSession) { @@ -205,11 +210,19 @@ export class MainThreadAuthenticationProvider extends Disposable { this._sessionMenuItems.delete(accountName); } this._accounts.delete(accountName); + + if (this._signInMenuItem) { + this._signInMenuItem.command.title = nls.localize('addAccount', "Sign in to {0}", this.displayName); + } } } }); addedSessions.forEach(session => this.registerSession(session)); + + if (addedSessions.length && this._signInMenuItem) { + this._signInMenuItem.command.title = nls.localize('addAnotherAccount', "Sign in to another {0} account", this.displayName); + } } login(scopes: string[]): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 353eba3556..3789061cdf 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -21,7 +21,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { mixin } from 'vs/base/common/objects'; -import { decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; +import { decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 640b44354b..6a5283e5a6 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -13,6 +13,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class MainThreadNotebookDocument extends Disposable { private _textModel: NotebookTextModel; @@ -163,8 +164,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); } - async executeNotebook(viewType: string, uri: URI): Promise { - return this._proxy.$executeNotebook(viewType, uri, undefined); + async executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise { + return this._proxy.$executeNotebook(viewType, uri, undefined, token); } async $postMessage(handle: number, value: any): Promise { @@ -232,8 +233,8 @@ export class MainThreadNotebookController implements IMainNotebookController { mainthreadNotebook?.textModel.$spliceNotebookCellOutputs(cellHandle, splices); } - async executeNotebook(viewType: string, uri: URI): Promise { - this._mainThreadNotebook.executeNotebook(viewType, uri); + async executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise { + this._mainThreadNotebook.executeNotebook(viewType, uri, token); } onDidReceiveMessage(uri: UriComponents, message: any): void { @@ -266,8 +267,8 @@ export class MainThreadNotebookController implements IMainNotebookController { document?.textModel.updateRenderers(renderers); } - async executeNotebookCell(uri: URI, handle: number): Promise { - return this._proxy.$executeNotebook(this._viewType, uri, handle); + async executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise { + return this._proxy.$executeNotebook(this._viewType, uri, handle, token); } async destoryNotebookDocument(notebook: INotebookTextModel): Promise { diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 4f0554a33a..ef5c9e1067 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -43,7 +43,7 @@ const configurationEntrySchema: IJSONSchema = { default: 'window', enumDescriptions: [ nls.localize('scope.application.description', "Configuration that can be configured only in the user settings."), - nls.localize('scope.machine.description', "Configuration that can be configured only in the user settings when the extension is running locally, or only in the remote settings when the extension is running remotely."), + nls.localize('scope.machine.description', "Configuration that can be configured only in the user settings or only in the remote settings."), nls.localize('scope.window.description', "Configuration that can be configured in the user, remote or workspace settings."), nls.localize('scope.resource.description', "Configuration that can be configured in the user, remote, workspace or folder settings."), nls.localize('scope.language-overridable.description', "Resource configuration that can be configured in language specific settings."), diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a919d127e5..9af9e0b638 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -391,11 +391,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguageFeatures.registerOnTypeFormattingEditProvider(extension, checkSelector(selector), provider, [firstTriggerCharacter].concat(moreTriggerCharacters)); }, registerDocumentSemanticTokensProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentSemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { - checkProposedApiEnabled(extension); return extHostLanguageFeatures.registerDocumentSemanticTokensProvider(extension, checkSelector(selector), provider, legend); }, registerDocumentRangeSemanticTokensProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentRangeSemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { - checkProposedApiEnabled(extension); return extHostLanguageFeatures.registerDocumentRangeSemanticTokensProvider(extension, checkSelector(selector), provider, legend); }, registerSignatureHelpProvider(selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, firstItem?: string | vscode.SignatureHelpProviderMetadata, ...remaining: string[]): vscode.Disposable { @@ -588,7 +586,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer); }, registerCustomEditorProvider: (viewType: string, provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider, options?: { webviewOptions?: vscode.WebviewPanelOptions }) => { - checkProposedApiEnabled(extension); return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options?.webviewOptions); }, registerDecorationProvider(provider: vscode.DecorationProvider) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0c08f3c876..dd5d4c5d55 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1538,7 +1538,7 @@ export interface ExtHostCommentsShape { export interface ExtHostNotebookShape { $resolveNotebook(viewType: string, uri: UriComponents): Promise; - $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise; + $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise; $saveNotebook(viewType: string, uri: UriComponents): Promise; $updateActiveEditor(viewType: string, uri: UriComponents): Promise; $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index fded0797e3..9e967b6c48 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -27,7 +27,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { IURITransformer } from 'vs/base/common/uriIpc'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; +import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { Cache } from './cache'; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 8cfabdd9d2..c28497751b 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -13,8 +13,9 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Disposable as VSCodeDisposable } from './extHostTypes'; +import { CancellationToken } from 'vs/base/common/cancellation'; interface IObservable { proxy: T; @@ -37,12 +38,6 @@ function getObservable(obj: T): IObservable { }; } -const notebookDocumentMetadataDefaults: vscode.NotebookDocumentMetadata = { - editable: true, - cellEditable: true, - cellRunnable: true -}; - export class ExtHostCell extends Disposable implements vscode.NotebookCell { private originalSource: string[]; @@ -80,7 +75,7 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { this.originalSource = this._content.split(/\r|\n|\r\n/g); this._outputs = outputs; - const observableMetadata = getObservable(_metadata || {} as any); + const observableMetadata = getObservable(_metadata || {}); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { this.updateMetadata(); @@ -115,8 +110,9 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { } set metadata(newMetadata: vscode.NotebookCellMetadata) { + // Don't apply metadata defaults here, 'undefined' means 'inherit from document metadata' this._metadataChangeListener.dispose(); - const observableMetadata = getObservable(newMetadata || {} as any); // TODO defaults + const observableMetadata = getObservable(newMetadata); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { this.updateMetadata(); @@ -175,15 +171,24 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); } - private _metadata: vscode.NotebookDocumentMetadata | undefined = notebookDocumentMetadataDefaults; + private _metadata: Required = notebookDocumentMetadataDefaults; + private _metadataChangeListener: IDisposable; get metadata() { return this._metadata; } - set metadata(newMetadata: vscode.NotebookDocumentMetadata | undefined) { - this._metadata = newMetadata || notebookDocumentMetadataDefaults; - this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); + set metadata(newMetadata: Required) { + this._metadataChangeListener.dispose(); + newMetadata = { + ...notebookDocumentMetadataDefaults, + ...newMetadata + }; + const observableMetadata = getObservable(newMetadata); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this.updateMetadata(); + })); } private _displayOrder: string[] = []; @@ -210,6 +215,16 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo public renderingHandler: ExtHostNotebookOutputRenderingHandler ) { super(); + + const observableMetadata = getObservable(notebookDocumentMetadataDefaults); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this.updateMetadata(); + })); + } + + private updateMetadata() { + this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); } dispose() { @@ -686,7 +701,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return Promise.resolve(undefined); } - async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { + async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise { let provider = this._notebookProviders.get(viewType); if (!provider) { @@ -700,7 +715,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - return provider.provider.executeCell(document!, cell); + return provider.provider.executeCell(document!, cell, token); } async $saveNotebook(viewType: string, uri: UriComponents): Promise { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 5d2bc9360d..871977039b 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -20,12 +20,10 @@ import { assertIsDefined } from 'vs/base/common/types'; import { Schemas } from 'vs/base/common/network'; function es5ClassCompat(target: Function): any { - ///@ts-ignore + ///@ts-expect-error function _() { return Reflect.construct(target, arguments, this.constructor); } Object.defineProperty(_, 'name', Object.getOwnPropertyDescriptor(target, 'name')!); - ///@ts-ignore Object.setPrototypeOf(_, target); - ///@ts-ignore Object.setPrototypeOf(_.prototype, target.prototype); return _; } @@ -2428,24 +2426,126 @@ export class SemanticTokensLegend { } } +function isStrArrayOrUndefined(arg: any): arg is string[] | undefined { + if (typeof arg === 'undefined') { + return true; + } + if (Array.isArray(arg)) { + for (const element of arg) { + if (typeof element !== 'string') { + return false; + } + } + return true; + } + return false; +} + export class SemanticTokensBuilder { private _prevLine: number; private _prevChar: number; + private _dataIsSortedAndDeltaEncoded: boolean; private _data: number[]; private _dataLen: number; + private _tokenTypeStrToInt: Map; + private _tokenModifierStrToInt: Map; + private _hasLegend: boolean; - constructor() { + constructor(legend?: vscode.SemanticTokensLegend) { this._prevLine = 0; this._prevChar = 0; + this._dataIsSortedAndDeltaEncoded = true; this._data = []; this._dataLen = 0; + this._tokenTypeStrToInt = new Map(); + this._tokenModifierStrToInt = new Map(); + this._hasLegend = false; + if (legend) { + this._hasLegend = true; + for (let i = 0, len = legend.tokenTypes.length; i < len; i++) { + this._tokenTypeStrToInt.set(legend.tokenTypes[i], i); + } + for (let i = 0, len = legend.tokenModifiers.length; i < len; i++) { + this._tokenModifierStrToInt.set(legend.tokenModifiers[i], i); + } + } } - public push(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void { + public push(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void; + public push(range: Range, tokenType: string, tokenModifiers?: string[]): void; + public push(arg0: any, arg1: any, arg2: any, arg3?: any, arg4?: any): void { + if (typeof arg0 === 'number' && typeof arg1 === 'number' && typeof arg2 === 'number' && typeof arg3 === 'number' && typeof arg4 === 'number') { + // 1st overload + return this._pushEncoded(arg0, arg1, arg2, arg3, arg4); + } + if (Range.isRange(arg0) && typeof arg1 === 'string' && isStrArrayOrUndefined(arg2)) { + // 2nd overload + return this._push(arg0, arg1, arg2); + } + throw illegalArgument(); + } + + private _push(range: vscode.Range, tokenType: string, tokenModifiers?: string[]): void { + if (!this._hasLegend) { + throw new Error('Legend must be provided in constructor'); + } + if (range.start.line !== range.end.line) { + throw new Error('`range` cannot span multiple lines'); + } + if (!this._tokenTypeStrToInt.has(tokenType)) { + throw new Error('`tokenType` is not in the provided legend'); + } + const line = range.start.line; + const char = range.start.character; + const length = range.end.character - range.start.character; + const nTokenType = this._tokenTypeStrToInt.get(tokenType)!; + let nTokenModifiers = 0; + if (tokenModifiers) { + for (const tokenModifier of tokenModifiers) { + if (!this._tokenModifierStrToInt.has(tokenModifier)) { + throw new Error('`tokenModifier` is not in the provided legend'); + } + const nTokenModifier = this._tokenModifierStrToInt.get(tokenModifier)!; + nTokenModifiers |= (1 << nTokenModifier) >>> 0; + } + } + this._pushEncoded(line, char, length, nTokenType, nTokenModifiers); + } + + private _pushEncoded(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void { + if (this._dataIsSortedAndDeltaEncoded && (line < this._prevLine || (line === this._prevLine && char < this._prevChar))) { + // push calls were ordered and are no longer ordered + this._dataIsSortedAndDeltaEncoded = false; + + // Remove delta encoding from data + const tokenCount = (this._data.length / 5) | 0; + let prevLine = 0; + let prevChar = 0; + for (let i = 0; i < tokenCount; i++) { + let line = this._data[5 * i]; + let char = this._data[5 * i + 1]; + + if (line === 0) { + // on the same line as previous token + line = prevLine; + char += prevChar; + } else { + // on a different line than previous token + line += prevLine; + } + + this._data[5 * i] = line; + this._data[5 * i + 1] = char; + + prevLine = line; + prevChar = char; + } + } + let pushLine = line; let pushChar = char; - if (this._dataLen > 0) { + if (this._dataIsSortedAndDeltaEncoded && this._dataLen > 0) { pushLine -= this._prevLine; if (pushLine === 0) { pushChar -= this._prevChar; @@ -2462,8 +2562,55 @@ export class SemanticTokensBuilder { this._prevChar = char; } - public build(): Uint32Array { - return new Uint32Array(this._data); + private static _sortAndDeltaEncode(data: number[]): Uint32Array { + let pos: number[] = []; + const tokenCount = (data.length / 5) | 0; + for (let i = 0; i < tokenCount; i++) { + pos[i] = i; + } + pos.sort((a, b) => { + const aLine = data[5 * a]; + const bLine = data[5 * b]; + if (aLine === bLine) { + const aChar = data[5 * a + 1]; + const bChar = data[5 * b + 1]; + return aChar - bChar; + } + return aLine - bLine; + }); + const result = new Uint32Array(data.length); + let prevLine = 0; + let prevChar = 0; + for (let i = 0; i < tokenCount; i++) { + const srcOffset = 5 * pos[i]; + const line = data[srcOffset + 0]; + const char = data[srcOffset + 1]; + const length = data[srcOffset + 2]; + const tokenType = data[srcOffset + 3]; + const tokenModifiers = data[srcOffset + 4]; + + const pushLine = line - prevLine; + const pushChar = (pushLine === 0 ? char - prevChar : char); + + const dstOffset = 5 * i; + result[dstOffset + 0] = pushLine; + result[dstOffset + 1] = pushChar; + result[dstOffset + 2] = length; + result[dstOffset + 3] = tokenType; + result[dstOffset + 4] = tokenModifiers; + + prevLine = line; + prevChar = char; + } + + return result; + } + + public build(resultId?: string): SemanticTokens { + if (!this._dataIsSortedAndDeltaEncoded) { + return new SemanticTokens(SemanticTokensBuilder._sortAndDeltaEncode(this._data), resultId); + } + return new SemanticTokens(new Uint32Array(this._data), resultId); } } @@ -2600,7 +2747,6 @@ export class CustomDocument implements vscode.CustomDocument readonly #edits = new Cache('edits'); - readonly #viewType: string; readonly #uri: vscode.Uri; #editState: EditState = { @@ -2611,15 +2757,12 @@ export class CustomDocument implements vscode.CustomDocument #isDisposed = false; #version = 1; - constructor(viewType: string, uri: vscode.Uri) { - this.#viewType = viewType; + constructor(uri: vscode.Uri) { this.#uri = uri; } //#region Public API - public get viewType(): string { return this.#viewType; } - public get uri(): vscode.Uri { return this.#uri; } public get fileName(): string { return this.uri.fsPath; } @@ -2672,11 +2815,11 @@ export class CustomDocument implements vscode.CustomDocument /** @internal*/ _addEdit(edit: EditType): number { const id = this.#edits.add([edit]); - this.#editState = { - allEdits: [...this.#editState.allEdits.slice(0, this.#editState.currentIndex), id], + this._updateEditState({ + allEdits: [...this.#editState.allEdits.slice(0, this.#editState.currentIndex + 1), id], currentIndex: this.#editState.currentIndex + 1, saveIndex: this.#editState.saveIndex, - }; + }); return id; } } diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index b1802f7177..c73c721ce6 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -269,16 +269,16 @@ class WebviewDocumentStore { return this._documents.get(this.key(viewType, resource)); } - public add(document: extHostTypes.CustomDocument) { - const key = this.key(document.viewType, document.uri); + public add(viewType: string, document: extHostTypes.CustomDocument) { + const key = this.key(viewType, document.uri); if (this._documents.has(key)) { - throw new Error(`Document already exists for viewType:${document.viewType} resource:${document.uri}`); + throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`); } this._documents.set(key, document); } - public delete(document: extHostTypes.CustomDocument) { - const key = this.key(document.viewType, document.uri); + public delete(viewType: string, document: extHostTypes.CustomDocument) { + const key = this.key(viewType, document.uri); this._documents.delete(key); } @@ -414,7 +414,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { disposables.add(provider.editingDelegate.onDidEdit(e => { const document = e.document; const editId = (document as extHostTypes.CustomDocument)._addEdit(e.edit); - this._proxy.$onDidEdit(document.uri, document.viewType, editId, e.label); + this._proxy.$onDidEdit(document.uri, viewType, editId, e.label); })); } } @@ -516,7 +516,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { const revivedResource = URI.revive(resource); const document = await entry.provider.openCustomDocument(revivedResource, cancellation); - this._documents.add(document as extHostTypes.CustomDocument); + this._documents.add(viewType, document as extHostTypes.CustomDocument); return { editable: !!entry.provider.editingDelegate, }; @@ -534,7 +534,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { const revivedResource = URI.revive(resource); const document = this.getCustomDocument(viewType, revivedResource); - this._documents.delete(document); + this._documents.delete(viewType, document); document._dispose(); } diff --git a/src/vs/workbench/api/common/shared/semanticTokens.ts b/src/vs/workbench/api/common/shared/semanticTokens.ts deleted file mode 100644 index cecc816f78..0000000000 --- a/src/vs/workbench/api/common/shared/semanticTokens.ts +++ /dev/null @@ -1,113 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { VSBuffer } from 'vs/base/common/buffer'; - -export interface IFullSemanticTokensDto { - id: number; - type: 'full'; - data: Uint32Array; -} - -export interface IDeltaSemanticTokensDto { - id: number; - type: 'delta'; - deltas: { start: number; deleteCount: number; data?: Uint32Array; }[]; -} - -export type ISemanticTokensDto = IFullSemanticTokensDto | IDeltaSemanticTokensDto; - -const enum EncodedSemanticTokensType { - Full = 1, - Delta = 2 -} - -export function encodeSemanticTokensDto(semanticTokens: ISemanticTokensDto): VSBuffer { - const buff = VSBuffer.alloc(encodedSize2(semanticTokens)); - let offset = 0; - buff.writeUInt32BE(semanticTokens.id, offset); offset += 4; - if (semanticTokens.type === 'full') { - buff.writeUInt8(EncodedSemanticTokensType.Full, offset); offset += 1; - buff.writeUInt32BE(semanticTokens.data.length, offset); offset += 4; - for (const uint of semanticTokens.data) { - buff.writeUInt32BE(uint, offset); offset += 4; - } - } else { - buff.writeUInt8(EncodedSemanticTokensType.Delta, offset); offset += 1; - buff.writeUInt32BE(semanticTokens.deltas.length, offset); offset += 4; - for (const delta of semanticTokens.deltas) { - buff.writeUInt32BE(delta.start, offset); offset += 4; - buff.writeUInt32BE(delta.deleteCount, offset); offset += 4; - if (delta.data) { - buff.writeUInt32BE(delta.data.length, offset); offset += 4; - for (const uint of delta.data) { - buff.writeUInt32BE(uint, offset); offset += 4; - } - } else { - buff.writeUInt32BE(0, offset); offset += 4; - } - } - } - return buff; -} - -function encodedSize2(semanticTokens: ISemanticTokensDto): number { - let result = 0; - result += 4; // id - result += 1; // type - if (semanticTokens.type === 'full') { - result += 4; // data length - result += semanticTokens.data.byteLength; - } else { - result += 4; // delta count - for (const delta of semanticTokens.deltas) { - result += 4; // start - result += 4; // deleteCount - result += 4; // data length - if (delta.data) { - result += delta.data.byteLength; - } - } - } - return result; -} - -export function decodeSemanticTokensDto(buff: VSBuffer): ISemanticTokensDto { - let offset = 0; - const id = buff.readUInt32BE(offset); offset += 4; - const type: EncodedSemanticTokensType = buff.readUInt8(offset); offset += 1; - if (type === EncodedSemanticTokensType.Full) { - const length = buff.readUInt32BE(offset); offset += 4; - const data = new Uint32Array(length); - for (let j = 0; j < length; j++) { - data[j] = buff.readUInt32BE(offset); offset += 4; - } - return { - id: id, - type: 'full', - data: data - }; - } - const deltaCount = buff.readUInt32BE(offset); offset += 4; - let deltas: { start: number; deleteCount: number; data?: Uint32Array; }[] = []; - for (let i = 0; i < deltaCount; i++) { - const start = buff.readUInt32BE(offset); offset += 4; - const deleteCount = buff.readUInt32BE(offset); offset += 4; - const length = buff.readUInt32BE(offset); offset += 4; - let data: Uint32Array | undefined; - if (length > 0) { - data = new Uint32Array(length); - for (let j = 0; j < length; j++) { - data[j] = buff.readUInt32BE(offset); offset += 4; - } - } - deltas[i] = { start, deleteCount, data }; - } - return { - id: id, - type: 'delta', - deltas: deltas - }; -} diff --git a/src/vs/workbench/api/common/shared/semanticTokensDto.ts b/src/vs/workbench/api/common/shared/semanticTokensDto.ts new file mode 100644 index 0000000000..1f83f183e6 --- /dev/null +++ b/src/vs/workbench/api/common/shared/semanticTokensDto.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import * as platform from 'vs/base/common/platform'; + +export interface IFullSemanticTokensDto { + id: number; + type: 'full'; + data: Uint32Array; +} + +export interface IDeltaSemanticTokensDto { + id: number; + type: 'delta'; + deltas: { start: number; deleteCount: number; data?: Uint32Array; }[]; +} + +export type ISemanticTokensDto = IFullSemanticTokensDto | IDeltaSemanticTokensDto; + +const enum EncodedSemanticTokensType { + Full = 1, + Delta = 2 +} + +function reverseEndianness(arr: Uint8Array): void { + for (let i = 0, len = arr.length; i < len; i += 4) { + // flip bytes 0<->3 and 1<->2 + const b0 = arr[i + 0]; + const b1 = arr[i + 1]; + const b2 = arr[i + 2]; + const b3 = arr[i + 3]; + arr[i + 0] = b3; + arr[i + 1] = b2; + arr[i + 2] = b1; + arr[i + 3] = b0; + } +} + +function toLittleEndianBuffer(arr: Uint32Array): VSBuffer { + const uint8Arr = new Uint8Array(arr.buffer, arr.byteOffset, arr.length * 4); + if (!platform.isLittleEndian()) { + // the byte order must be changed + reverseEndianness(uint8Arr); + } + return VSBuffer.wrap(uint8Arr); +} + +function fromLittleEndianBuffer(buff: VSBuffer): Uint32Array { + const uint8Arr = buff.buffer; + if (!platform.isLittleEndian()) { + // the byte order must be changed + reverseEndianness(uint8Arr); + } + if (uint8Arr.byteOffset % 4 === 0) { + return new Uint32Array(uint8Arr.buffer, uint8Arr.byteOffset); + } else { + // unaligned memory access doesn't work on all platforms + const data = new Uint8Array(uint8Arr.byteLength); + data.set(uint8Arr); + return new Uint32Array(data.buffer, data.byteOffset); + } +} + +export function encodeSemanticTokensDto(semanticTokens: ISemanticTokensDto): VSBuffer { + const dest = new Uint32Array(encodeSemanticTokensDtoSize(semanticTokens)); + let offset = 0; + dest[offset++] = semanticTokens.id; + if (semanticTokens.type === 'full') { + dest[offset++] = EncodedSemanticTokensType.Full; + dest[offset++] = semanticTokens.data.length; + dest.set(semanticTokens.data, offset); offset += semanticTokens.data.length; + } else { + dest[offset++] = EncodedSemanticTokensType.Delta; + dest[offset++] = semanticTokens.deltas.length; + for (const delta of semanticTokens.deltas) { + dest[offset++] = delta.start; + dest[offset++] = delta.deleteCount; + if (delta.data) { + dest[offset++] = delta.data.length; + dest.set(delta.data, offset); offset += delta.data.length; + } else { + dest[offset++] = 0; + } + } + } + return toLittleEndianBuffer(dest); +} + +function encodeSemanticTokensDtoSize(semanticTokens: ISemanticTokensDto): number { + let result = 0; + result += ( + + 1 // id + + 1 // type + ); + if (semanticTokens.type === 'full') { + result += ( + + 1 // data length + + semanticTokens.data.length + ); + } else { + result += ( + + 1 // delta count + ); + result += ( + + 1 // start + + 1 // deleteCount + + 1 // data length + ) * semanticTokens.deltas.length; + for (const delta of semanticTokens.deltas) { + if (delta.data) { + result += delta.data.length; + } + } + } + return result; +} + +export function decodeSemanticTokensDto(_buff: VSBuffer): ISemanticTokensDto { + const src = fromLittleEndianBuffer(_buff); + let offset = 0; + const id = src[offset++]; + const type: EncodedSemanticTokensType = src[offset++]; + if (type === EncodedSemanticTokensType.Full) { + const length = src[offset++]; + const data = src.subarray(offset, offset + length); offset += length; + return { + id: id, + type: 'full', + data: data + }; + } + const deltaCount = src[offset++]; + let deltas: { start: number; deleteCount: number; data?: Uint32Array; }[] = []; + for (let i = 0; i < deltaCount; i++) { + const start = src[offset++]; + const deleteCount = src[offset++]; + const length = src[offset++]; + let data: Uint32Array | undefined; + if (length > 0) { + data = src.subarray(offset, offset + length); offset += length; + } + deltas[i] = { start, deleteCount, data }; + } + return { + id: id, + type: 'delta', + deltas: deltas + }; +} diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 553b5a7fae..2e69dcce0f 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/screencast'; +import 'vs/css!./media/actions'; import { Action } from 'vs/base/common/actions'; import * as nls from 'vs/nls'; diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 64e368670b..dba680c227 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/actions'; - import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; @@ -27,8 +25,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor } from 'vs/workbench/common/views'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); const viewCategory = nls.localize('view', "View"); @@ -520,7 +518,7 @@ export class MoveFocusedViewAction extends Action { @IContextKeyService private contextKeyService: IContextKeyService, @INotificationService private notificationService: INotificationService, @IActivityBarService private activityBarService: IActivityBarService, - @IViewletService private viewletService: IViewletService + @IPanelService private panelService: IPanelService ) { super(id, label); } @@ -544,36 +542,58 @@ export class MoveFocusedViewAction extends Action { const quickPick = this.quickInputService.createQuickPick(); quickPick.placeholder = nls.localize('moveFocusedView.selectDestination', "Select a Destination for the View"); + const items: Array = []; + + items.push({ + type: 'separator', + label: nls.localize('sidebar', "Side Bar") + }); + + items.push({ + id: '_.sidebar.newcontainer', + label: nls.localize('moveFocusedView.newContainerInSidebar', "New Container in Side Bar") + }); + const pinnedViewlets = this.activityBarService.getPinnedViewletIds(); - const items: Array = this.viewletService.getViewlets() - .filter(viewlet => { - if (viewlet.id === this.viewDescriptorService.getViewContainer(focusedViewId)!.id) { + items.push(...pinnedViewlets + .filter(viewletId => { + if (viewletId === this.viewDescriptorService.getViewContainer(focusedViewId)!.id) { return false; } - return !viewContainerRegistry.get(viewlet.id)!.rejectAddedViews && pinnedViewlets.indexOf(viewlet.id) !== -1; + return !viewContainerRegistry.get(viewletId)!.rejectAddedViews; }) - .map(viewlet => { + .map(viewletId => { return { - id: viewlet.id, - label: viewlet.name, + id: viewletId, + label: viewContainerRegistry.get(viewletId)!.name }; - }); + })); - if (this.viewDescriptorService.getViewLocation(focusedViewId) !== ViewContainerLocation.Panel) { - items.unshift({ - type: 'separator', - label: nls.localize('sidebar', "Side Bar") - }); - items.push({ - type: 'separator', - label: nls.localize('panel', "Panel") - }); - items.push({ - id: '_.panel.newcontainer', - label: nls.localize('moveFocusedView.newContainerInPanel', "New Container in Panel"), - }); - } + items.push({ + type: 'separator', + label: nls.localize('panel', "Panel") + }); + items.push({ + id: '_.panel.newcontainer', + label: nls.localize('moveFocusedView.newContainerInPanel', "New Container in Panel"), + }); + + const pinnedPanels = this.panelService.getPinnedPanels(); + items.push(...pinnedPanels + .filter(panel => { + if (panel.id === this.viewDescriptorService.getViewContainer(focusedViewId)!.id) { + return false; + } + + return !viewContainerRegistry.get(panel.id)!.rejectAddedViews; + }) + .map(panel => { + return { + id: panel.id, + label: viewContainerRegistry.get(panel.id)!.name + }; + })); quickPick.items = items; @@ -583,6 +603,9 @@ export class MoveFocusedViewAction extends Action { if (destination.id === '_.panel.newcontainer') { this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Panel); this.viewsService.openView(focusedViewId, true); + } else if (destination.id === '_.sidebar.newcontainer') { + this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Sidebar); + this.viewsService.openView(focusedViewId, true); } else if (destination.id) { this.viewDescriptorService.moveViewsToContainer([viewDescriptor], viewContainerRegistry.get(destination.id)!); this.viewsService.openView(focusedViewId, true); diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 0ecf173b4b..2d43d9cb37 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -901,7 +901,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: '84256', + id: 'list.scrollLeft', weight: KeybindingWeight.WorkbenchContrib, when: WorkbenchListFocusContextKey, handler: accessor => { diff --git a/src/vs/workbench/browser/actions/media/actions.css b/src/vs/workbench/browser/actions/media/actions.css index 9f532bf471..58ff36df63 100644 --- a/src/vs/workbench/browser/actions/media/actions.css +++ b/src/vs/workbench/browser/actions/media/actions.css @@ -2,3 +2,53 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-workspace::before { + content: "\ea76"; /* Close icon flips between black dot and "X" for dirty workspaces */ +} + +.monaco-workbench .screencast-mouse { + position: absolute; + border: 2px solid red; + border-radius: 20px; + width: 20px; + height: 20px; + top: 0; + left: 0; + z-index: 100000; + content: ' '; + pointer-events: none; + display: none; +} + +.monaco-workbench .screencast-keyboard { + position: absolute; + background-color: rgba(0, 0, 0 ,0.5); + width: 100%; + height: 100px; + bottom: 20%; + left: 0; + z-index: 100000; + pointer-events: none; + color: #eee; + line-height: 100px; + text-align: center; + font-size: 56px; + transition: opacity 0.3s ease-out; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.monaco-workbench .screencast-keyboard:empty { + opacity: 0; +} + +.monaco-workbench .screencast-keyboard > .key { + padding: 0 8px; + box-shadow: inset 0 -3px 0 hsla(0,0%,73%,.4); + margin-right: 6px; + border: 1px solid hsla(0,0%,80%,.4); + border-radius: 5px; + background-color: rgba(255, 255, 255, 0.05); +} diff --git a/src/vs/workbench/browser/actions/media/screencast.css b/src/vs/workbench/browser/actions/media/screencast.css deleted file mode 100644 index 0391ee490e..0000000000 --- a/src/vs/workbench/browser/actions/media/screencast.css +++ /dev/null @@ -1,50 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .screencast-mouse { - position: absolute; - border: 2px solid red; - border-radius: 20px; - width: 20px; - height: 20px; - top: 0; - left: 0; - z-index: 100000; - content: ' '; - pointer-events: none; - display: none; -} - -.monaco-workbench .screencast-keyboard { - position: absolute; - background-color: rgba(0, 0, 0 ,0.5); - width: 100%; - height: 100px; - bottom: 20%; - left: 0; - z-index: 100000; - pointer-events: none; - color: #eee; - line-height: 100px; - text-align: center; - font-size: 56px; - transition: opacity 0.3s ease-out; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.monaco-workbench .screencast-keyboard:empty { - opacity: 0; -} - -.monaco-workbench .screencast-keyboard > .key { - padding: 0 8px; - box-shadow: inset 0 -3px 0 hsla(0,0%,73%,.4); - margin-right: 6px; - border: 1px solid hsla(0,0%,80%,.4); - border-radius: 5px; - background-color: rgba(255, 255, 255, 0.05); -} diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 68ddcdc590..b71a2c9b42 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/actions'; - import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; @@ -16,13 +14,13 @@ import { IsFullscreenContext } from 'vs/workbench/browser/contextkeys'; import { IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { IRecentWorkspace, IRecentFolder, IRecentFile, IRecent, isRecentFolder, isRecentWorkspace, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IRecent, isRecentFolder, isRecentWorkspace, IWorkspacesService, IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { FileKind } from 'vs/platform/files/common/files'; @@ -31,9 +29,15 @@ import { isMacintosh } from 'vs/base/common/platform'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { inQuickPickContext, getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ResourceMap } from 'vs/base/common/map'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; +interface IRecentlyOpenedPick extends IQuickPickItem { + resource: URI, + openable: IWindowOpenable; +} + abstract class BaseOpenRecentAction extends Action { private readonly removeFromRecentlyOpened: IQuickInputButton = { @@ -41,6 +45,12 @@ abstract class BaseOpenRecentAction extends Action { tooltip: nls.localize('remove', "Remove from Recently Opened") }; + private readonly dirtyRecentlyOpened: IQuickInputButton = { + iconClass: 'dirty-workspace codicon-circle-filled', + tooltip: nls.localize('dirtyRecentlyOpened', "Workspace With Dirty Files"), + alwaysVisible: true + }; + constructor( id: string, label: string, @@ -51,7 +61,8 @@ abstract class BaseOpenRecentAction extends Action { private keybindingService: IKeybindingService, private modelService: IModelService, private modeService: IModeService, - private hostService: IHostService + private hostService: IHostService, + private dialogService: IDialogService ) { super(id, label); } @@ -59,61 +70,53 @@ abstract class BaseOpenRecentAction extends Action { protected abstract isQuickNavigate(): boolean; async run(): Promise { - const { workspaces, files } = await this.workspacesService.getRecentlyOpened(); + const recentlyOpened = await this.workspacesService.getRecentlyOpened(); + const dirtyWorkspacesAndFolders = await this.workspacesService.getDirtyWorkspaces(); - this.openRecent(workspaces, files); - } + // Identify all folders and workspaces with dirty files + const dirtyFolders = new ResourceMap(); + const dirtyWorkspaces = new ResourceMap(); + for (const dirtyWorkspace of dirtyWorkspacesAndFolders) { + if (URI.isUri(dirtyWorkspace)) { + dirtyFolders.set(dirtyWorkspace, true); + } else { + dirtyWorkspaces.set(dirtyWorkspace.configPath, dirtyWorkspace); + } + } - private async openRecent(recentWorkspaces: Array, recentFiles: IRecentFile[]): Promise { - - const toPick = (recent: IRecent, labelService: ILabelService, buttons: IQuickInputButton[] | undefined) => { - let openable: IWindowOpenable | undefined; - let iconClasses: string[]; - let fullLabel: string | undefined; - let resource: URI | undefined; - - // Folder + // Identify all recently opened folders and workspaces + const recentFolders = new ResourceMap(); + const recentWorkspaces = new ResourceMap(); + for (const recent of recentlyOpened.workspaces) { if (isRecentFolder(recent)) { - resource = recent.folderUri; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FOLDER); - openable = { folderUri: resource }; - fullLabel = recent.label || labelService.getWorkspaceLabel(resource, { verbose: true }); + recentFolders.set(recent.folderUri, true); + } else { + recentWorkspaces.set(recent.workspace.configPath, recent.workspace); } + } - // Workspace - else if (isRecentWorkspace(recent)) { - resource = recent.workspace.configPath; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER); - openable = { workspaceUri: resource }; - fullLabel = recent.label || labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); + // Fill in all known recently opened workspaces + const workspacePicks: IRecentlyOpenedPick[] = []; + for (const recent of recentlyOpened.workspaces) { + const isDirty = isRecentFolder(recent) ? dirtyFolders.has(recent.folderUri) : dirtyWorkspaces.has(recent.workspace.configPath); + + workspacePicks.push(this.toQuickPick(recent, isDirty)); + } + + // Fill any backup workspace that is not yet shown at the end + for (const dirtyWorkspaceOrFolder of dirtyWorkspacesAndFolders) { + if (URI.isUri(dirtyWorkspaceOrFolder) && !recentFolders.has(dirtyWorkspaceOrFolder)) { + workspacePicks.push(this.toQuickPick({ folderUri: dirtyWorkspaceOrFolder }, true)); + } else if (isWorkspaceIdentifier(dirtyWorkspaceOrFolder) && !recentWorkspaces.has(dirtyWorkspaceOrFolder.configPath)) { + workspacePicks.push(this.toQuickPick({ workspace: dirtyWorkspaceOrFolder }, true)); } + } - // File - else { - resource = recent.fileUri; - iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FILE); - openable = { fileUri: resource }; - fullLabel = recent.label || labelService.getUriLabel(resource); - } - - const { name, parentPath } = splitName(fullLabel); - - return { - iconClasses, - label: name, - description: parentPath, - buttons, - openable, - resource - }; - }; - - const workspacePicks = recentWorkspaces.map(workspace => toPick(workspace, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined)); - const filePicks = recentFiles.map(p => toPick(p, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined)); + const filePicks = recentlyOpened.files.map(p => this.toQuickPick(p, false)); // focus second entry if the first recent workspace is the current workspace - const firstEntry = recentWorkspaces[0]; - let autoFocusSecondEntry: boolean = firstEntry && this.contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri); + const firstEntry = recentlyOpened.workspaces[0]; + const autoFocusSecondEntry: boolean = firstEntry && this.contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri); let keyMods: IKeyMods | undefined; @@ -129,8 +132,27 @@ abstract class BaseOpenRecentAction extends Action { onKeyMods: mods => keyMods = mods, quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, onDidTriggerItemButton: async context => { - await this.workspacesService.removeRecentlyOpened([context.item.resource]); - context.removeItem(); + + // Remove + if (context.button === this.removeFromRecentlyOpened) { + await this.workspacesService.removeRecentlyOpened([context.item.resource]); + context.removeItem(); + } + + // Dirty Workspace + else if (context.button === this.dirtyRecentlyOpened) { + const result = await this.dialogService.confirm({ + type: 'question', + title: nls.localize('dirtyWorkspace', "Workspace with Dirty Files"), + message: nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the dirty files?"), + detail: nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with dirty files cannot be removed until all dirty files have been saved or reverted.") + }); + + if (result.confirmed) { + this.hostService.openWindow([context.item.openable]); + this.quickInputService.cancel(); + } + } } }); @@ -138,6 +160,48 @@ abstract class BaseOpenRecentAction extends Action { return this.hostService.openWindow([pick.openable], { forceNewWindow: keyMods?.ctrlCmd, forceReuseWindow: keyMods?.alt }); } } + + private toQuickPick(recent: IRecent, isDirty: boolean): IRecentlyOpenedPick { + let openable: IWindowOpenable | undefined; + let iconClasses: string[]; + let fullLabel: string | undefined; + let resource: URI | undefined; + + // Folder + if (isRecentFolder(recent)) { + resource = recent.folderUri; + iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FOLDER); + openable = { folderUri: resource }; + fullLabel = recent.label || this.labelService.getWorkspaceLabel(resource, { verbose: true }); + } + + // Workspace + else if (isRecentWorkspace(recent)) { + resource = recent.workspace.configPath; + iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER); + openable = { workspaceUri: resource }; + fullLabel = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); + } + + // File + else { + resource = recent.fileUri; + iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FILE); + openable = { fileUri: resource }; + fullLabel = recent.label || this.labelService.getUriLabel(resource); + } + + const { name, parentPath } = splitName(fullLabel); + + return { + iconClasses, + label: name, + description: parentPath, + buttons: isDirty ? [this.dirtyRecentlyOpened] : [this.removeFromRecentlyOpened], + openable, + resource + }; + } } export class OpenRecentAction extends BaseOpenRecentAction { @@ -155,9 +219,10 @@ export class OpenRecentAction extends BaseOpenRecentAction { @IModelService modelService: IModelService, @IModeService modeService: IModeService, @ILabelService labelService: ILabelService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IDialogService dialogService: IDialogService ) { - super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService); + super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService, dialogService); } protected isQuickNavigate(): boolean { @@ -180,9 +245,10 @@ class QuickPickRecentAction extends BaseOpenRecentAction { @IModelService modelService: IModelService, @IModeService modeService: IModeService, @ILabelService labelService: ILabelService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IDialogService dialogService: IDialogService ) { - super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService); + super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService, dialogService); } protected isQuickNavigate(): boolean { diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 2a4315ab60..bcd7f2a582 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -8,7 +8,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; -import { find } from 'vs/base/common/arrays'; +import { find, insert } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IEditorDescriptor { @@ -94,15 +94,11 @@ class EditorRegistry implements IEditorRegistry { registerEditor(descriptor: EditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): IDisposable { this.mapEditorToInputs.set(descriptor, inputDescriptors); - this.editors.push(descriptor); + const remove = insert(this.editors, descriptor); return toDisposable(() => { this.mapEditorToInputs.delete(descriptor); - - const index = this.editors.indexOf(descriptor); - if (index !== -1) { - this.editors.splice(index, 1); - } + remove(); }); } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 69ddf92389..52113c69cb 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -115,8 +115,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { @IProductService private readonly productService: IProductService ) { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); - this.migrateFromOldCachedViewletsValue(); storageKeysSyncRegistryService.registerStorageKey({ key: ActivitybarPart.PINNED_VIEWLETS, version: 1 }); + this.migrateFromOldCachedViewletsValue(); this.cachedViewlets = this.getCachedViewlets(); for (const cachedViewlet of this.cachedViewlets) { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index fb1e248487..e8c0d82f2e 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -275,7 +275,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX, contextKey: editorPickerContextKey, placeholder: nls.localize('editorQuickAccessPlaceholder', "Type the name of an editor to open it."), - helpEntries: [{ description: nls.localize('activeGroupEditorsByMostRecentlyUsedQuickAccess', "Show Editors in Active Group by Most Recently Used."), needsEditor: false }] + helpEntries: [{ description: nls.localize('activeGroupEditorsByMostRecentlyUsedQuickAccess', "Show Editors in Active Group by Most Recently Used"), needsEditor: false }] }); quickAccessRegistry.registerQuickAccessProvider({ diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index e4f8b28344..c4298e737d 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -18,7 +18,6 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { values } from 'vs/base/common/map'; import { ItemActivation, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AllEditorsByMostRecentlyUsedQuickAccess, ActiveGroupEditorsByMostRecentlyUsedQuickAccess, AllEditorsByAppearanceQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; @@ -578,7 +577,7 @@ export abstract class BaseCloseAllAction extends Action { dirtyEditorsToConfirm.add(name); } - const confirm = await this.fileDialogService.showSaveConfirm(values(dirtyEditorsToConfirm)); + const confirm = await this.fileDialogService.showSaveConfirm(Array.from(dirtyEditorsToConfirm.values())); if (confirm === ConfirmResult.CANCEL) { return; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 33894861a9..315d083c61 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -894,7 +894,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private async doShowEditor(editor: EditorInput, active: boolean, options?: EditorOptions): Promise { // Show in editor control if the active editor changed - let openEditorPromise: Promise; + let openEditorPromise: Promise | undefined; if (active) { openEditorPromise = (async () => { try { @@ -915,7 +915,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } })(); } else { - openEditorPromise = Promise.resolve(undefined); // inactive: return undefined as result to signal this + openEditorPromise = undefined; // inactive: return undefined as result to signal this } // Show in title control after editor control because some actions depend on it diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 2fc17f4354..c74222f535 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -13,7 +13,6 @@ import { GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid'; import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorPartOptions, IEditorPartOptionsChangeEvent } from 'vs/workbench/common/editor'; -import { values } from 'vs/base/common/map'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct, coalesce } from 'vs/base/common/arrays'; import { IEditorGroupsAccessor, IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions } from 'vs/workbench/browser/parts/editor/editor'; @@ -211,7 +210,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro } get groups(): IEditorGroupView[] { - return values(this.groupViews); + return Array.from(this.groupViews.values()); } get count(): number { diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index 617555bf13..087bc38d31 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -62,7 +62,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro // Filtering const filteredEditorEntries = this.doGetEditorPickItems().filter(entry => { - if (!query.value) { + if (!query.normalized) { return true; } @@ -79,7 +79,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro }); // Sorting - if (query.value) { + if (query.normalized) { const groups = this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).map(group => group.id); filteredEditorEntries.sort((entryA, entryB) => { if (entryA.groupId !== entryB.groupId) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index d66ff3f627..fb8282f5a8 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -23,7 +23,6 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Severity } from 'vs/platform/notification/common/notification'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { startsWith } from 'vs/base/common/strings'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -142,7 +141,7 @@ class NotificationMessageRenderer { } else { let title = node.title; - if (!title && startsWith(node.href, 'command:')) { + if (!title && node.href.startsWith('command:')) { title = localize('executeCommand', "Click to execute command '{0}'", node.href.substr('command:'.length)); } else if (!title) { title = node.href; diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 650f482421..d847c8337b 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -46,13 +46,13 @@ border-right-width: 0; /* no border when editor area is hiden */ } -.monaco-workbench .part.panel > .composite.tit > .title-actions .monaco-action-bar .action-item .action-label { +.monaco-workbench .part.panel > .composite.title > .title-actions .monaco-action-bar .action-item .action-label { outline-offset: -2px; } /** Panel Switcher */ -.monaco-workbench .part.panel > .composite.tit > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.codicon-more { +.monaco-workbench .part.panel > .composite.title > .panel-switcher-container.composite-bar > .monaco-action-bar .action-label.codicon-more { display: flex; align-items: center; justify-content: center; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index e15a06d0ab..66b18c977a 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -31,7 +31,6 @@ import { coalesce, find } from 'vs/base/common/arrays'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ToggleStatusbarVisibilityAction } from 'vs/workbench/browser/actions/layoutActions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { values } from 'vs/base/common/map'; import { assertIsDefined } from 'vs/base/common/types'; import { Emitter, Event } from 'vs/base/common/event'; import { Command } from 'vs/editor/common/modes'; @@ -227,7 +226,7 @@ class StatusbarViewModel extends Disposable { private saveState(): void { if (this.hidden.size > 0) { - this.storageService.store(StatusbarViewModel.HIDDEN_ENTRIES_KEY, JSON.stringify(values(this.hidden)), StorageScope.GLOBAL); + this.storageService.store(StatusbarViewModel.HIDDEN_ENTRIES_KEY, JSON.stringify(Array.from(this.hidden.values())), StorageScope.GLOBAL); } else { this.storageService.remove(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.GLOBAL); } @@ -360,9 +359,9 @@ export class StatusbarPart extends Part implements IStatusbarService { ) { super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); + storageKeysSyncRegistryService.registerStorageKey({ key: StatusbarViewModel.HIDDEN_ENTRIES_KEY, version: 1 }); this.viewModel = this._register(new StatusbarViewModel(storageService)); this.onDidChangeEntryVisibility = this.viewModel.onDidChangeEntryVisibility; - storageKeysSyncRegistryService.registerStorageKey({ key: StatusbarViewModel.HIDDEN_ENTRIES_KEY, version: 1 }); this.registerListeners(); } diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 3478a67369..eff8b49d03 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -109,8 +109,7 @@ export class ContributableViewsModel extends Disposable { const added: IAddedViewDescriptorRef[] = []; const removed: IViewDescriptorRef[] = []; - for (const { id, visible, size } of viewDescriptors) { - const { visibleIndex, viewDescriptor, state } = this.find(id); + for (const { visibleIndex, viewDescriptor, state, visible, size } of viewDescriptors.map(({ id, visible, size }) => ({ ...this.find(id), visible, size }))) { if (!viewDescriptor.canToggleVisibility) { throw new Error(`Can't toggle this view's visibility`); @@ -338,13 +337,14 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { const globalViewsStateStorageId = `${viewletStateStorageId}.hidden`; + storageKeysSyncRegistryService.registerStorageKey({ key: globalViewsStateStorageId, version: 1 }); const viewStates = PersistentContributableViewsModel.loadViewsStates(viewletStateStorageId, globalViewsStateStorageId, storageService); super(container, viewDescriptorService, viewStates); + this.storageService = storageService; this.workspaceViewsStateStorageId = viewletStateStorageId; this.globalViewsStateStorageId = globalViewsStateStorageId; - this.storageService = storageService; this._register(Event.any( this.onDidAdd, @@ -353,7 +353,6 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { Event.map(this.onDidChangeViewState, viewDescriptorRef => [viewDescriptorRef])) (viewDescriptorRefs => this.saveViewsStates())); - storageKeysSyncRegistryService.registerStorageKey({ key: this.globalViewsStateStorageId, version: 1 }); this._globalViewsStatesValue = this.getStoredGlobalViewsStatesValue(); this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); } diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index b403d80aac..9e81f5253a 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -32,8 +32,8 @@ export class DiffEditorModel extends EditorModel { async load(): Promise { await Promise.all([ - this._originalModel ? this._originalModel.load() : Promise.resolve(undefined), - this._modifiedModel ? this._modifiedModel.load() : Promise.resolve(undefined), + this._originalModel?.load(), + this._modifiedModel?.load(), ]); return this; diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts index ce5aa3ace1..4c441f24a1 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts @@ -228,7 +228,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // If we still have dirty working copies, save those directly // unless the save was not successful (e.g. cancelled) if (result !== false) { - await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : Promise.resolve(true))); + await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : true)); } } @@ -244,7 +244,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // If we still have dirty working copies, revert those directly // unless the revert operation was not successful (e.g. cancelled) - await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.revert(revertOptions) : Promise.resolve())); + await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.revert(revertOptions) : undefined)); } private noVeto(backupsToDiscard: IWorkingCopy[]): boolean | Promise { diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 13537cbe45..a67c4cf558 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -70,12 +70,6 @@ class InspectEditorTokensController extends Disposable implements IEditorContrib this._register(this._editor.onDidChangeModel((e) => this.stop())); this._register(this._editor.onDidChangeModelLanguage((e) => this.stop())); this._register(this._editor.onKeyUp((e) => e.keyCode === KeyCode.Escape && this.stop())); - this._register(this._themeService.onDidColorThemeChange(_ => { - if (this._widget) { - this.stop(); - this.launch(); - } - })); } public dispose(): void { @@ -199,12 +193,11 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { private readonly _editor: IActiveCodeEditor; private readonly _modeService: IModeService; private readonly _themeService: IWorkbenchThemeService; + private readonly _textMateService: ITextMateService; private readonly _notificationService: INotificationService; private readonly _configurationService: IConfigurationService; private readonly _model: ITextModel; private readonly _domNode: HTMLElement; - private readonly _grammar: Promise; - private readonly _semanticTokens: Promise; private readonly _currentRequestCancellationTokenSource: CancellationTokenSource; constructor( @@ -220,16 +213,17 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { this._editor = editor; this._modeService = modeService; this._themeService = themeService; + this._textMateService = textMateService; this._notificationService = notificationService; this._configurationService = configurationService; this._model = this._editor.getModel(); this._domNode = document.createElement('div'); this._domNode.className = 'token-inspect-widget'; this._currentRequestCancellationTokenSource = new CancellationTokenSource(); - this._grammar = textMateService.createGrammar(this._model.getLanguageIdentifier().language); - this._semanticTokens = this._computeSemanticTokens(); this._beginCompute(this._editor.getPosition()); this._register(this._editor.onDidChangeCursorPosition((e) => this._beginCompute(this._editor.getPosition()))); + this._register(themeService.onDidColorThemeChange(_ => this._beginCompute(this._editor.getPosition()))); + this._register(configurationService.onDidChangeConfiguration(e => e.affectsConfiguration('editor.semanticHighlighting.enabled') && this._beginCompute(this._editor.getPosition()))); this._editor.addContentWidget(this); } @@ -245,10 +239,13 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { } private _beginCompute(position: Position): void { + const grammar = this._textMateService.createGrammar(this._model.getLanguageIdentifier().language); + const semanticTokens = this._computeSemanticTokens(); + dom.clearNode(this._domNode); this._domNode.appendChild(document.createTextNode(nls.localize('inspectTMScopesWidget.loading', "Loading..."))); - Promise.all([this._grammar, this._semanticTokens]).then(([grammar, semanticTokens]) => { + Promise.all([grammar, semanticTokens]).then(([grammar, semanticTokens]) => { if (this._isDisposed) { return; } @@ -551,9 +548,9 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { } else if (TokenStylingRule.is(definition)) { const scope = theme.getTokenStylingRuleScope(definition); if (scope === 'setting') { - return `User settings: ${definition.selector.selectorString} - ${this._renderStyleProperty(definition.style, property)}`; + return `User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`; } else if (scope === 'theme') { - return `Color theme: ${definition.selector.selectorString} - ${this._renderStyleProperty(definition.style, property)}`; + return `Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`; } return ''; } else { diff --git a/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts b/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts index 14258d8eef..a56d70e427 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts @@ -11,6 +11,7 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; /** * Shows a message when opening a large file which has been memory optimized (and features disabled). @@ -23,9 +24,13 @@ export class LargeFileOptimizationsWarner extends Disposable implements IEditorC private readonly _editor: ICodeEditor, @INotificationService private readonly _notificationService: INotificationService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super(); + // opt-in to syncing + const neverShowAgainId = 'editor.contrib.largeFileOptimizationsWarner'; + storageKeysSyncRegistryService.registerStorageKey({ key: neverShowAgainId, version: 1 }); this._register(this._editor.onDidChangeModel((e) => { const model = this._editor.getModel(); @@ -56,7 +61,7 @@ export class LargeFileOptimizationsWarner extends Disposable implements IEditorC }); } } - ], { neverShowAgain: { id: 'editor.contrib.largeFileOptimizationsWarner' } }); + ], { neverShowAgain: { id: neverShowAgainId } }); } })); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/semanticTokensHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/semanticTokensHelp.ts index f7ac453d2f..2eef853e00 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/semanticTokensHelp.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/semanticTokensHelp.ts @@ -15,6 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; /** * Shows a message when semantic tokens are shown the first time. @@ -30,10 +31,15 @@ export class SemanticTokensHelp extends Disposable implements IEditorContributio @INotificationService _notificationService: INotificationService, @IOpenerService _openerService: IOpenerService, @IWorkbenchThemeService _themeService: IWorkbenchThemeService, - @IEditorService _editorService: IEditorService + @IEditorService _editorService: IEditorService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super(); + // opt-in to syncing + const neverShowAgainId = 'editor.contrib.semanticTokensHelp'; + storageKeysSyncRegistryService.registerStorageKey({ key: neverShowAgainId, version: 1 }); + const toDispose = this._register(new DisposableStore()); const localToDispose = toDispose.add(new DisposableStore()); const installChangeTokenListener = (model: ITextModel) => { @@ -75,7 +81,7 @@ export class SemanticTokensHelp extends Disposable implements IEditorContributio _openerService.open(URI.parse(url)); } } - ], { neverShowAgain: { id: 'editor.contrib.semanticTokensHelp' } }); + ], { neverShowAgain: { id: neverShowAgainId } }); })); }; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index c5c8f02aab..b7cfcb4ff7 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -164,6 +164,10 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { public async resolve(): Promise { await super.resolve(); + if (this.isDisposed()) { + return null; + } + if (!this._modelRef) { this._modelRef = this._register(assertIsDefined(await this.customEditorService.models.tryRetain(this.resource, this.viewType))); this._register(this._modelRef.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); @@ -177,31 +181,30 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + // See if we can keep using the same custom editor provider const editorInfo = this.customEditorService.getCustomEditor(this.viewType); if (editorInfo?.matches(newResource)) { - // We can keep using the same custom editor provider - - if (!this._moveHandler) { - return { - editor: this.customEditorService.createInput(newResource, this.viewType, group), - }; - } - - this._moveHandler(newResource); - const newEditor = this.instantiationService.createInstance(CustomEditorInput, - newResource, - this.viewType, - this.id, - new Lazy(() => undefined!), // this webview is replaced in the transfer call - this._fromBackup, - ); - this.transfer(newEditor); - newEditor.updateGroup(group); - return { editor: newEditor }; - } else { - // const possible = this.customEditorService.getContributedCustomEditors(newResource); - return { editor: this.editorService.createEditorInput({ resource: newResource, forceFile: true }) }; + return { editor: this.doMove(group, newResource) }; } + + return { editor: this.editorService.createEditorInput({ resource: newResource, forceFile: true }) }; + } + + private doMove(group: GroupIdentifier, newResource: URI): IEditorInput { + if (!this._moveHandler) { + return this.customEditorService.createInput(newResource, this.viewType, group); + } + + this._moveHandler(newResource); + const newEditor = this.instantiationService.createInstance(CustomEditorInput, + newResource, + this.viewType, + this.id, + new Lazy(() => undefined!), + this._fromBackup); // this webview is replaced in the transfer call + this.transfer(newEditor); + newEditor.updateGroup(group); + return newEditor; } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 0646c5dfc0..cc6431956b 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -330,10 +330,10 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return; } - // See if the new resource can be opened in a custom editor - if (!this.getAllCustomEditors(newResource).allEditors - .some(editor => editor.priority !== CustomEditorPriority.option) - ) { + const possibleEditors = this.getAllCustomEditors(newResource); + + // See if we have any non-optional custom editor for this resource + if (!possibleEditors.allEditors.some(editor => editor.priority !== CustomEditorPriority.option)) { return; } @@ -359,19 +359,25 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return; } - // If there is, show a single prompt for all editors to see if the user wants to re-open them - // - // TODO: instead of prompting eagerly, it'd likly be better to replace all the editors with - // ones that would prompt when they first become visible - await new Promise(resolve => setTimeout(resolve, 50)); - const pickedViewType = await this.showOpenWithPrompt(newResource); - if (!pickedViewType) { + let viewType: string | undefined; + if (possibleEditors.defaultEditor) { + viewType = possibleEditors.defaultEditor.id; + } else { + // If there is, show a single prompt for all editors to see if the user wants to re-open them + // + // TODO: instead of prompting eagerly, it'd likly be better to replace all the editors with + // ones that would prompt when they first become visible + await new Promise(resolve => setTimeout(resolve, 50)); + viewType = await this.showOpenWithPrompt(newResource); + } + + if (!viewType) { return; } for (const [group, entries] of editorsToReplace) { this.editorService.replaceEditors(entries.map(editor => { - const replacement = this.createInput(newResource, pickedViewType, group); + const replacement = this.createInput(newResource, viewType!, group); return { editor, replacement, @@ -384,7 +390,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ } } -export const customEditorsAssociationsKey = 'workbench.experimental.editorAssociations'; +export const customEditorsAssociationsKey = 'workbench.editorAssociations'; export type CustomEditorAssociation = CustomEditorSelector & { readonly viewType: string; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index f8d03c793f..7b51ecb24a 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -36,6 +36,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; const $ = dom.$; @@ -76,7 +77,7 @@ export class BreakpointsView extends ViewPane { ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.minimumBodySize = this.maximumBodySize = getExpandedBodySize(this.debugService.getModel()); + this.updateSize(); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); } @@ -227,12 +228,15 @@ export class BreakpointsView extends ViewPane { ]; } + private updateSize(): void { + // Adjust expanded body size + this.minimumBodySize = this.orientation === Orientation.VERTICAL ? getExpandedBodySize(this.debugService.getModel()) : 170; + this.maximumBodySize = this.orientation === Orientation.VERTICAL ? this.minimumBodySize : Number.POSITIVE_INFINITY; + } + private onBreakpointsChange(): void { if (this.isBodyVisible()) { - this.minimumBodySize = getExpandedBodySize(this.debugService.getModel()); - if (this.maximumBodySize < Number.POSITIVE_INFINITY) { - this.maximumBodySize = this.minimumBodySize; - } + this.updateSize(); if (this.list) { this.list.splice(0, this.list.length, this.elements); this.needsRefresh = false; diff --git a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts index 21e1bb98f7..aa80160a47 100644 --- a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts +++ b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts @@ -13,7 +13,6 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService' import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { mapToSerializable } from 'vs/base/common/map'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceProvider, IWorkspace } from 'vs/workbench/services/host/browser/browserHostService'; import { IProcessEnvironment } from 'vs/base/common/platform'; @@ -107,8 +106,8 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i // Open debug window as new window. Pass ParsedArgs over. return this.workspaceProvider.open(debugWorkspace, { - reuse: false, // debugging always requires a new window - payload: mapToSerializable(environment) // mandatory properties to enable debugging + reuse: false, // debugging always requires a new window + payload: Array.from(environment.entries()) // mandatory properties to enable debugging }); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index c5e36e4e50..cbb910b758 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -27,7 +27,7 @@ import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, import { /*RatingsWidget, InstallCountWidget, */RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; @@ -40,7 +40,7 @@ import { Color } from 'vs/base/common/color'; import { assign } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtensionsTree, ExtensionData } from 'vs/workbench/contrib/extensions/browser/extensionsViewer'; +import { ExtensionsTree, ExtensionData, ExtensionsGridView, getExtensions } from 'vs/workbench/contrib/extensions/browser/extensionsViewer'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -132,7 +132,6 @@ const NavbarSection = { Contributions: 'contributions', Changelog: 'changelog', Dependencies: 'dependencies', - ExtensionPack: 'extensionPack' }; interface ILayoutParticipant { @@ -320,8 +319,6 @@ export class ExtensionEditor extends BaseEditor { private async updateTemplate(input: ExtensionsInput, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise { const runningExtensions = await this.extensionService.getExtensions(); - const colorThemes = await this.workbenchThemeService.getColorThemes(); - const fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); this.activeElement = null; this.editorLoadComplete = false; @@ -422,10 +419,13 @@ export class ExtensionEditor extends BaseEditor { const systemDisabledWarningAction = this.instantiationService.createInstance(SystemDisabledWarningAction); const actions = [ reloadAction, + this.instantiationService.createInstance(SyncIgnoredIconAction), this.instantiationService.createInstance(StatusLabelAction), this.instantiationService.createInstance(UpdateAction), - this.instantiationService.createInstance(SetColorThemeAction, colorThemes), - this.instantiationService.createInstance(SetFileIconThemeAction, fileIconThemes), + this.instantiationService.createInstance(SetColorThemeAction, await this.workbenchThemeService.getColorThemes()), + this.instantiationService.createInstance(SetFileIconThemeAction, await this.workbenchThemeService.getFileIconThemes()), + this.instantiationService.createInstance(SetProductIconThemeAction, await this.workbenchThemeService.getProductIconThemes()), + this.instantiationService.createInstance(EnableDropDownAction), this.instantiationService.createInstance(DisableDropDownAction, runningExtensions), this.instantiationService.createInstance(RemoteInstallAction), @@ -457,9 +457,6 @@ export class ExtensionEditor extends BaseEditor { if (manifest) { combinedInstallAction.manifest = manifest; } - if (extension.extensionPack.length) { - template.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); - } if (manifest && manifest.contributes) { template.navbar.push(NavbarSection.Contributions, localize('contributions', "Feature Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); } @@ -597,7 +594,6 @@ export class ExtensionEditor extends BaseEditor { case NavbarSection.Contributions: return this.openContributions(template); case NavbarSection.Changelog: return this.openChangelog(template); case NavbarSection.Dependencies: return this.openDependencies(extension, template); - case NavbarSection.ExtensionPack: return this.openExtensionPack(extension, template); } return Promise.resolve(null); } @@ -852,10 +848,37 @@ export class ExtensionEditor extends BaseEditor { `; } - private openReadme(template: IExtensionEditorTemplate): Promise { + private async openReadme(template: IExtensionEditorTemplate): Promise { + const manifest = await this.extensionManifest!.get().promise; + if (manifest && manifest.extensionPack && manifest.extensionPack.length) { + return this.openExtensionPackReadme(manifest, template); + } return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template); } + private async openExtensionPackReadme(manifest: IExtensionManifest, template: IExtensionEditorTemplate): Promise { + const extensionPackReadme = append(template.content, $('div', { class: 'extension-pack-readme' })); + extensionPackReadme.style.margin = '0 auto'; + extensionPackReadme.style.maxWidth = '882px'; + + const extensionPack = append(extensionPackReadme, $('div', { class: 'extension-pack' })); + toggleClass(extensionPackReadme, 'narrow', manifest.extensionPack!.length <= 2); + + const extensionPackHeader = append(extensionPack, $('div.header')); + extensionPackHeader.textContent = localize('extension pack', "Extension Pack ({0})", manifest.extensionPack!.length); + const extensionPackContent = append(extensionPack, $('div', { class: 'extension-pack-content' })); + extensionPackContent.setAttribute('tabindex', '0'); + append(extensionPack, $('div.footer')); + const readmeContent = append(extensionPackReadme, $('div.readme-content')); + + await Promise.all([ + this.renderExtensionPack(manifest, extensionPackContent), + this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }), + ]); + + return { focus: () => extensionPackContent.focus() }; + } + private openChangelog(template: IExtensionEditorTemplate): Promise { return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template); } @@ -938,28 +961,19 @@ export class ExtensionEditor extends BaseEditor { return Promise.resolve({ focus() { dependenciesTree.domFocus(); } }); } - private openExtensionPack(extension: IExtension, template: IExtensionEditorTemplate): Promise { + private async renderExtensionPack(manifest: IExtensionManifest, parent: HTMLElement): Promise { const content = $('div', { class: 'subcontent' }); - const scrollableContent = new DomScrollableElement(content, {}); - append(template.content, scrollableContent.getDomNode()); - this.contentDisposables.add(scrollableContent); + const scrollableContent = new DomScrollableElement(content, { useShadows: false }); + append(parent, scrollableContent.getDomNode()); - const extensionsPackTree = this.instantiationService.createInstance(ExtensionsTree, - new ExtensionData(extension, null, extension => extension.extensionPack || [], this.extensionsWorkbenchService), content, - { - listBackground: editorBackground - }); - const layout = () => { - scrollableContent.scanDomNode(); - const scrollDimensions = scrollableContent.getScrollDimensions(); - extensionsPackTree.layout(scrollDimensions.height); - }; - const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); - this.contentDisposables.add(toDisposable(removeLayoutParticipant)); - - this.contentDisposables.add(extensionsPackTree); + const extensionsGridView = this.instantiationService.createInstance(ExtensionsGridView, content); + const extensions: IExtension[] = await getExtensions(manifest.extensionPack!, this.extensionsWorkbenchService); + extensionsGridView.setExtensions(extensions); scrollableContent.scanDomNode(); - return Promise.resolve({ focus() { extensionsPackTree.domFocus(); } }); + + this.contentDisposables.add(scrollableContent); + this.contentDisposables.add(extensionsGridView); + this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout: () => scrollableContent.scanDomNode() }))); } private renderSettings(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 41a27261a9..495bbccaaf 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -29,7 +29,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, registerColor, foreground } from 'vs/platform/theme/common/colorRegistry'; import { Color } from 'vs/base/common/color'; @@ -52,7 +52,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { coalesce } from 'vs/base/common/arrays'; -import { IWorkbenchThemeService, ThemeSettings, IWorkbenchFileIconTheme, IWorkbenchColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ILabelService } from 'vs/platform/label/common/label'; import { prefersExecuteOnUI, prefersExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -142,6 +142,10 @@ function getRelativeDateLabel(date: Date): string { } export abstract class ExtensionAction extends Action implements IExtensionContainer { + static readonly EXTENSION_ACTION_CLASS = 'extension-action'; + static readonly TEXT_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} text`; + static readonly LABEL_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} label`; + static readonly ICON_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} icon`; private _extension: IExtension | null = null; get extension(): IExtension | null { return this._extension; } set extension(extension: IExtension | null) { this._extension = extension; this.update(); } @@ -153,9 +157,8 @@ export class InstallAction extends ExtensionAction { private static readonly INSTALL_LABEL = localize('install', "Install"); private static readonly INSTALLING_LABEL = localize('installing', "Installing"); - private static readonly Class = 'extension-action prominent install'; - private static readonly InstallingClass = 'extension-action install installing'; - + private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; + private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install installing`; private _manifest: IExtensionManifest | null = null; set manifest(manifest: IExtensionManifest) { @@ -239,17 +242,15 @@ export class InstallAction extends ExtensionAction { if (extension && extension.local) { const runningExtension = await this.getRunningExtension(extension.local); if (runningExtension) { - const colorThemes = await this.workbenchThemeService.getColorThemes(); - const fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); - if (SetColorThemeAction.getColorThemes(colorThemes, this.extension).length) { - const action = this.instantiationService.createInstance(SetColorThemeAction, colorThemes); - action.extension = extension; - return action.run({ showCurrentTheme: true, ignoreFocusLost: true }); - } - if (SetFileIconThemeAction.getFileIconThemes(fileIconThemes, this.extension).length) { - const action = this.instantiationService.createInstance(SetFileIconThemeAction, fileIconThemes); - action.extension = extension; - return action.run({ showCurrentTheme: true, ignoreFocusLost: true }); + let action = await SetColorThemeAction.create(this.workbenchThemeService, this.instantiationService, extension) + || await SetFileIconThemeAction.create(this.workbenchThemeService, this.instantiationService, extension) + || await SetProductIconThemeAction.create(this.workbenchThemeService, this.instantiationService, extension); + if (action) { + try { + return action.run({ showCurrentTheme: true, ignoreFocusLost: true }); + } finally { + action.dispose(); + } } } } @@ -301,8 +302,8 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { protected static readonly INSTALL_LABEL = localize('install', "Install"); protected static readonly INSTALLING_LABEL = localize('installing', "Installing"); - private static readonly Class = 'extension-action prominent install'; - private static readonly InstallingClass = 'extension-action install installing'; + private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; + private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install installing`; updateWhenCounterExtensionChanges: boolean = true; @@ -394,8 +395,8 @@ export class UninstallAction extends ExtensionAction { private static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); private static readonly UninstallingLabel = localize('Uninstalling', "Uninstalling"); - private static readonly UninstallClass = 'extension-action uninstall'; - private static readonly UnInstallingClass = 'extension-action uninstall uninstalling'; + private static readonly UninstallClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall`; + private static readonly UnInstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} uninstall uninstalling`; constructor( @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService @@ -450,7 +451,7 @@ export class UninstallAction extends ExtensionAction { export class CombinedInstallAction extends ExtensionAction { - private static readonly NoExtensionClass = 'extension-action prominent install no-extension'; + private static readonly NoExtensionClass = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install no-extension`; private installAction: InstallAction; private uninstallAction: UninstallAction; @@ -517,7 +518,7 @@ export class CombinedInstallAction extends ExtensionAction { export class UpdateAction extends ExtensionAction { - private static readonly EnabledClass = 'extension-action prominent update'; + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} prominent update`; private static readonly DisabledClass = `${UpdateAction.EnabledClass} disabled`; constructor( @@ -701,7 +702,8 @@ export function getContextMenuActions(menuService: IMenuService, contextKeyServi export class ManageExtensionAction extends ExtensionDropDownAction { static readonly ID = 'extensions.manage'; - private static readonly Class = 'extension-action manage codicon-gear'; + + private static readonly Class = `${ExtensionAction.ICON_ACTION_CLASS} manage codicon-gear`; private static readonly HideManageExtensionClass = `${ManageExtensionAction.Class} hide`; constructor( @@ -719,19 +721,22 @@ export class ManageExtensionAction extends ExtensionDropDownAction { this.update(); } - getActionGroups(runningExtensions: IExtensionDescription[], colorThemes: IWorkbenchColorTheme[], fileIconThemes: IWorkbenchFileIconTheme[]): IAction[][] { + async getActionGroups(runningExtensions: IExtensionDescription[]): Promise { const groups: ExtensionAction[][] = []; if (this.extension) { - const extensionColorThemes = SetColorThemeAction.getColorThemes(colorThemes, this.extension); - const extensionFileIconThemes = SetFileIconThemeAction.getFileIconThemes(fileIconThemes, this.extension); - if (extensionColorThemes.length || extensionFileIconThemes.length) { - const themesGroup: ExtensionAction[] = []; - if (extensionColorThemes.length) { - themesGroup.push(this.instantiationService.createInstance(SetColorThemeAction, colorThemes)); - } - if (extensionFileIconThemes.length) { - themesGroup.push(this.instantiationService.createInstance(SetFileIconThemeAction, fileIconThemes)); + const actions = await Promise.all([ + SetColorThemeAction.create(this.workbenchThemeService, this.instantiationService, this.extension), + SetFileIconThemeAction.create(this.workbenchThemeService, this.instantiationService, this.extension), + SetProductIconThemeAction.create(this.workbenchThemeService, this.instantiationService, this.extension) + ]); + + const themesGroup: ExtensionAction[] = []; + for (let action of actions) { + if (action) { + themesGroup.push(action); } + } + if (themesGroup.length) { groups.push(themesGroup); } } @@ -755,9 +760,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { async run(): Promise { const runtimeExtensions = await this.extensionService.getExtensions(); - const colorThemes = await this.workbenchThemeService.getColorThemes(); - const fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); - return super.run({ actionGroups: this.getActionGroups(runtimeExtensions, colorThemes, fileIconThemes), disposeActionsOnHide: true }); + return super.run({ actionGroups: await this.getActionGroups(runtimeExtensions), disposeActionsOnHide: true }); } update(): void { @@ -972,8 +975,8 @@ export class DisableGloballyAction extends ExtensionAction { export abstract class ExtensionEditorDropDownAction extends ExtensionDropDownAction { - private static readonly EnabledClass = 'extension-action extension-editor-dropdown-action'; - private static readonly EnabledDropDownClass = 'extension-action extension-editor-dropdown-action dropdown enable'; + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} extension-editor-dropdown-action`; + private static readonly EnabledDropDownClass = `${ExtensionEditorDropDownAction.EnabledClass} dropdown enable`; private static readonly DisabledClass = `${ExtensionEditorDropDownAction.EnabledClass} disabled`; constructor( @@ -1181,7 +1184,7 @@ export class UpdateAllAction extends Action { export class ReloadAction extends ExtensionAction { - private static readonly EnabledClass = 'extension-action reload'; + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} reload`; private static readonly DisabledClass = `${ReloadAction.EnabledClass} disabled`; updateWhenCounterExtensionChanges: boolean = true; @@ -1318,22 +1321,45 @@ export class ReloadAction extends ExtensionAction { } } +function isThemeFromExtension(theme: IWorkbenchTheme, extension: IExtension | undefined | null): boolean { + return !!(extension && theme.extensionData && ExtensionIdentifier.equals(theme.extensionData.extensionId, extension.identifier.id)); +} + +function getQuickPickEntries(themes: IWorkbenchTheme[], currentTheme: IWorkbenchTheme, extension: IExtension | null | undefined, showCurrentTheme: boolean): (IQuickPickItem | IQuickPickSeparator)[] { + const picks: (IQuickPickItem | IQuickPickSeparator)[] = []; + for (const theme of themes) { + if (isThemeFromExtension(theme, extension) && !(showCurrentTheme && theme === currentTheme)) { + picks.push({ label: theme.label, id: theme.id }); + } + } + if (showCurrentTheme) { + picks.push({ type: 'separator', label: localize('current', "Current") }); + picks.push({ label: currentTheme.label, id: currentTheme.id }); + } + return picks; +} + + export class SetColorThemeAction extends ExtensionAction { - static getColorThemes(colorThemes: IWorkbenchColorTheme[], extension: IExtension): IWorkbenchColorTheme[] { - return colorThemes.filter(c => c.extensionData && ExtensionIdentifier.equals(c.extensionData.extensionId, extension.identifier.id)); - } - - private static readonly EnabledClass = 'extension-action theme'; + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`; private static readonly DisabledClass = `${SetColorThemeAction.EnabledClass} disabled`; + static async create(workbenchThemeService: IWorkbenchThemeService, instantiationService: IInstantiationService, extension: IExtension): Promise { + const themes = await workbenchThemeService.getColorThemes(); + if (themes.some(th => isThemeFromExtension(th, extension))) { + const action = instantiationService.createInstance(SetColorThemeAction, themes); + action.extension = extension; + return action; + } + return undefined; + } constructor( - private readonly colorThemes: IWorkbenchColorTheme[], + private colorThemes: IWorkbenchColorTheme[], @IExtensionService extensionService: IExtensionService, @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IConfigurationService private readonly configurationService: IConfigurationService ) { super(`extensions.colorTheme`, localize('color theme', "Set Color Theme"), SetColorThemeAction.DisabledClass, false); this._register(Event.any(extensionService.onDidChangeExtensions, workbenchThemeService.onDidColorThemeChange)(() => this.update(), this)); @@ -1341,36 +1367,21 @@ export class SetColorThemeAction extends ExtensionAction { } update(): void { - this.enabled = false; - if (this.extension) { - const isInstalled = this.extension.state === ExtensionState.Installed; - if (isInstalled) { - const extensionThemes = SetColorThemeAction.getColorThemes(this.colorThemes, this.extension); - this.enabled = extensionThemes.length > 0; - } - } + this.enabled = !!this.extension && (this.extension.state === ExtensionState.Installed) && this.colorThemes.some(th => isThemeFromExtension(th, this.extension)); this.class = this.enabled ? SetColorThemeAction.EnabledClass : SetColorThemeAction.DisabledClass; } async run({ showCurrentTheme, ignoreFocusLost }: { showCurrentTheme: boolean, ignoreFocusLost: boolean } = { showCurrentTheme: false, ignoreFocusLost: false }): Promise { + this.colorThemes = await this.workbenchThemeService.getColorThemes(); + this.update(); if (!this.enabled) { return; } - let extensionThemes = SetColorThemeAction.getColorThemes(this.colorThemes, this.extension!); - const currentTheme = this.colorThemes.filter(t => t.settingsId === this.configurationService.getValue(ThemeSettings.COLOR_THEME))[0] || this.workbenchThemeService.getColorTheme(); - showCurrentTheme = showCurrentTheme || extensionThemes.some(t => t.id === currentTheme.id); - if (showCurrentTheme) { - extensionThemes = extensionThemes.filter(t => t.id !== currentTheme.id); - } + const currentTheme = this.workbenchThemeService.getColorTheme(); const delayer = new Delayer(100); - const picks: (IQuickPickItem | IQuickPickSeparator)[] = []; - picks.push(...extensionThemes.map(theme => ({ label: theme.label, id: theme.id }))); - if (showCurrentTheme) { - picks.push({ type: 'separator', label: localize('current', "Current") }); - picks.push({ label: currentTheme.label, id: currentTheme.id }); - } + const picks = getQuickPickEntries(this.colorThemes, currentTheme, this.extension, showCurrentTheme); const pickedTheme = await this.quickInputService.pick( picks, { @@ -1378,28 +1389,30 @@ export class SetColorThemeAction extends ExtensionAction { onDidFocus: item => delayer.trigger(() => this.workbenchThemeService.setColorTheme(item.id, undefined)), ignoreFocusLost }); - let confValue = this.configurationService.inspect(ThemeSettings.COLOR_THEME); - const target = typeof confValue.workspaceValue !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; - return this.workbenchThemeService.setColorTheme(pickedTheme ? pickedTheme.id : currentTheme.id, target); + return this.workbenchThemeService.setColorTheme(pickedTheme ? pickedTheme.id : currentTheme.id, 'auto'); } } export class SetFileIconThemeAction extends ExtensionAction { - private static readonly EnabledClass = 'extension-action theme'; + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`; private static readonly DisabledClass = `${SetFileIconThemeAction.EnabledClass} disabled`; - - static getFileIconThemes(fileIconThemes: IWorkbenchFileIconTheme[], extension: IExtension): IWorkbenchFileIconTheme[] { - return fileIconThemes.filter(c => c.extensionData && ExtensionIdentifier.equals(c.extensionData.extensionId, extension.identifier.id)); + static async create(workbenchThemeService: IWorkbenchThemeService, instantiationService: IInstantiationService, extension: IExtension): Promise { + const themes = await workbenchThemeService.getFileIconThemes(); + if (themes.some(th => isThemeFromExtension(th, extension))) { + const action = instantiationService.createInstance(SetFileIconThemeAction, themes); + action.extension = extension; + return action; + } + return undefined; } constructor( - private readonly fileIconThemes: IWorkbenchFileIconTheme[], + private fileIconThemes: IWorkbenchFileIconTheme[], @IExtensionService extensionService: IExtensionService, @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(`extensions.fileIconTheme`, localize('file icon theme', "Set File Icon Theme"), SetFileIconThemeAction.DisabledClass, false); this._register(Event.any(extensionService.onDidChangeExtensions, workbenchThemeService.onDidFileIconThemeChange)(() => this.update(), this)); @@ -1407,36 +1420,20 @@ export class SetFileIconThemeAction extends ExtensionAction { } update(): void { - this.enabled = false; - if (this.extension) { - const isInstalled = this.extension.state === ExtensionState.Installed; - if (isInstalled) { - const extensionThemes = SetFileIconThemeAction.getFileIconThemes(this.fileIconThemes, this.extension); - this.enabled = extensionThemes.length > 0; - } - } + this.enabled = !!this.extension && (this.extension.state === ExtensionState.Installed) && this.fileIconThemes.some(th => isThemeFromExtension(th, this.extension)); this.class = this.enabled ? SetFileIconThemeAction.EnabledClass : SetFileIconThemeAction.DisabledClass; } async run({ showCurrentTheme, ignoreFocusLost }: { showCurrentTheme: boolean, ignoreFocusLost: boolean } = { showCurrentTheme: false, ignoreFocusLost: false }): Promise { - await this.update(); + this.fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); + this.update(); if (!this.enabled) { return; } - let extensionThemes = SetFileIconThemeAction.getFileIconThemes(this.fileIconThemes, this.extension!); - const currentTheme = this.fileIconThemes.filter(t => t.settingsId === this.configurationService.getValue(ThemeSettings.ICON_THEME))[0] || this.workbenchThemeService.getFileIconTheme(); - showCurrentTheme = showCurrentTheme || extensionThemes.some(t => t.id === currentTheme.id); - if (showCurrentTheme) { - extensionThemes = extensionThemes.filter(t => t.id !== currentTheme.id); - } + const currentTheme = this.workbenchThemeService.getFileIconTheme(); const delayer = new Delayer(100); - const picks: (IQuickPickItem | IQuickPickSeparator)[] = []; - picks.push(...extensionThemes.map(theme => ({ label: theme.label, id: theme.id }))); - if (showCurrentTheme && currentTheme.label) { - picks.push({ type: 'separator', label: localize('current', "Current") }); - picks.push({ label: currentTheme.label, id: currentTheme.id }); - } + const picks = getQuickPickEntries(this.fileIconThemes, currentTheme, this.extension, showCurrentTheme); const pickedTheme = await this.quickInputService.pick( picks, { @@ -1444,9 +1441,62 @@ export class SetFileIconThemeAction extends ExtensionAction { onDidFocus: item => delayer.trigger(() => this.workbenchThemeService.setFileIconTheme(item.id, undefined)), ignoreFocusLost }); - let confValue = this.configurationService.inspect(ThemeSettings.ICON_THEME); - const target = typeof confValue.workspaceValue !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; - return this.workbenchThemeService.setFileIconTheme(pickedTheme ? pickedTheme.id : currentTheme.id, target); + return this.workbenchThemeService.setFileIconTheme(pickedTheme ? pickedTheme.id : currentTheme.id, 'auto'); + } +} + +export class SetProductIconThemeAction extends ExtensionAction { + + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`; + private static readonly DisabledClass = `${SetProductIconThemeAction.EnabledClass} disabled`; + + static async create(workbenchThemeService: IWorkbenchThemeService, instantiationService: IInstantiationService, extension: IExtension): Promise { + const themes = await workbenchThemeService.getProductIconThemes(); + if (themes.some(th => isThemeFromExtension(th, extension))) { + const action = instantiationService.createInstance(SetProductIconThemeAction, themes); + action.extension = extension; + return action; + } + return undefined; + } + + constructor( + private productIconThemes: IWorkbenchProductIconTheme[], + @IExtensionService extensionService: IExtensionService, + @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, + @IQuickInputService private readonly quickInputService: IQuickInputService + ) { + super(`extensions.productIconTheme`, localize('product icon theme', "Set Product Icon Theme"), SetProductIconThemeAction.DisabledClass, false); + this._register(Event.any(extensionService.onDidChangeExtensions, workbenchThemeService.onDidProductIconThemeChange)(() => this.update(), this)); + this.enabled = true; // enabled by default + this.class = SetProductIconThemeAction.EnabledClass; + // this.update(); + } + + update(): void { + this.enabled = !!this.extension && (this.extension.state === ExtensionState.Installed) && this.productIconThemes.some(th => isThemeFromExtension(th, this.extension)); + this.class = this.enabled ? SetProductIconThemeAction.EnabledClass : SetProductIconThemeAction.DisabledClass; + } + + async run({ showCurrentTheme, ignoreFocusLost }: { showCurrentTheme: boolean, ignoreFocusLost: boolean } = { showCurrentTheme: false, ignoreFocusLost: false }): Promise { + this.productIconThemes = await this.workbenchThemeService.getProductIconThemes(); + this.update(); + if (!this.enabled) { + return; + } + + const currentTheme = this.workbenchThemeService.getProductIconTheme(); + + const delayer = new Delayer(100); + const picks = getQuickPickEntries(this.productIconThemes, currentTheme, this.extension, showCurrentTheme); + const pickedTheme = await this.quickInputService.pick( + picks, + { + placeHolder: localize('select product icon theme', "Select Product Icon Theme"), + onDidFocus: item => delayer.trigger(() => this.workbenchThemeService.setProductIconTheme(item.id, undefined)), + ignoreFocusLost + }); + return this.workbenchThemeService.setProductIconTheme(pickedTheme ? pickedTheme.id : currentTheme.id, 'auto'); } } @@ -1811,7 +1861,6 @@ export class UndoIgnoreExtensionRecommendationAction extends Action { } } - export class ShowRecommendedKeymapExtensionsAction extends Action { static readonly ID = 'workbench.extensions.action.showRecommendedKeymapExtensions'; @@ -2205,7 +2254,6 @@ export class ConfigureWorkspaceRecommendedExtensionsAction extends AbstractConfi static readonly ID = 'workbench.extensions.action.configureWorkspaceRecommendedExtensions'; static readonly LABEL = localize('configureWorkspaceRecommendedExtensions', "Configure Recommended Extensions (Workspace)"); - constructor( id: string, label: string, @@ -2241,7 +2289,6 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac static readonly ID = 'workbench.extensions.action.configureWorkspaceFolderRecommendedExtensions'; static readonly LABEL = localize('configureWorkspaceFolderRecommendedExtensions', "Configure Recommended Extensions (Workspace Folder)"); - constructor( id: string, label: string, @@ -2432,7 +2479,7 @@ export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecomm export class StatusLabelAction extends Action implements IExtensionContainer { - private static readonly ENABLED_CLASS = 'extension-status-label'; + private static readonly ENABLED_CLASS = `${ExtensionAction.TEXT_ACTION_CLASS} extension-status-label`; private static readonly DISABLED_CLASS = `${StatusLabelAction.ENABLED_CLASS} hide`; private initialStatus: ExtensionState | null = null; @@ -2534,7 +2581,7 @@ export class StatusLabelAction extends Action implements IExtensionContainer { export class MaliciousStatusLabelAction extends ExtensionAction { - private static readonly Class = 'malicious-status'; + private static readonly Class = `${ExtensionAction.TEXT_ACTION_CLASS} malicious-status`; constructor(long: boolean) { const tooltip = localize('malicious tooltip', "This extension was reported to be problematic."); @@ -2556,9 +2603,38 @@ export class MaliciousStatusLabelAction extends ExtensionAction { } } +export class SyncIgnoredIconAction extends ExtensionAction { + + private static readonly ENABLE_CLASS = `${ExtensionAction.ICON_ACTION_CLASS} codicon-eye-closed`; + private static readonly DISABLE_CLASS = `${SyncIgnoredIconAction.ENABLE_CLASS} hide`; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super('extensions.syncignore', '', SyncIgnoredIconAction.DISABLE_CLASS, false); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectedKeys.includes('sync.ignoredExtensions'))(() => this.update())); + this.update(); + this.tooltip = localize('syncingore.label', "This extension is ignored during sync."); + } + + update(): void { + this.class = SyncIgnoredIconAction.DISABLE_CLASS; + if (this.extension) { + const ignoredExtensions = this.configurationService.getValue('sync.ignoredExtensions') || []; + if (ignoredExtensions.some(id => areSameExtensions({ id }, this.extension!.identifier))) { + this.class = SyncIgnoredIconAction.ENABLE_CLASS; + } + } + } + + run(): Promise { + return Promise.resolve(); + } +} + export class ExtensionToolTipAction extends ExtensionAction { - private static readonly Class = 'disable-status'; + private static readonly Class = `${ExtensionAction.TEXT_ACTION_CLASS} disable-status`; updateWhenCounterExtensionChanges: boolean = true; private _runningExtensions: IExtensionDescription[] | null = null; @@ -2635,7 +2711,7 @@ export class ExtensionToolTipAction extends ExtensionAction { export class SystemDisabledWarningAction extends ExtensionAction { - private static readonly CLASS = 'system-disable'; + private static readonly CLASS = `${ExtensionAction.ICON_ACTION_CLASS} system-disable`; private static readonly WARNING_CLASS = `${SystemDisabledWarningAction.CLASS} codicon-warning`; private static readonly INFO_CLASS = `${SystemDisabledWarningAction.CLASS} codicon-info`; @@ -2726,7 +2802,6 @@ export class DisableAllAction extends Action { static readonly ID = 'workbench.extensions.action.disableAll'; static readonly LABEL = localize('disableAll', "Disable All Installed Extensions"); - constructor( id: string = DisableAllAction.ID, label: string = DisableAllAction.LABEL, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @@ -2751,7 +2826,6 @@ export class DisableAllWorkspaceAction extends Action { static readonly ID = 'workbench.extensions.action.disableAllWorkspace'; static readonly LABEL = localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"); - constructor( id: string = DisableAllWorkspaceAction.ID, label: string = DisableAllWorkspaceAction.LABEL, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -2778,7 +2852,6 @@ export class EnableAllAction extends Action { static readonly ID = 'workbench.extensions.action.enableAll'; static readonly LABEL = localize('enableAll', "Enable All Extensions"); - constructor( id: string = EnableAllAction.ID, label: string = EnableAllAction.LABEL, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @@ -2803,7 +2876,6 @@ export class EnableAllWorkspaceAction extends Action { static readonly ID = 'workbench.extensions.action.enableAllWorkspace'; static readonly LABEL = localize('enableAllWorkspace', "Enable All Extensions for this Workspace"); - constructor( id: string = EnableAllWorkspaceAction.ID, label: string = EnableAllWorkspaceAction.LABEL, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -3262,49 +3334,49 @@ export const extensionButtonProminentHoverBackground = registerColor('extensionB registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const foregroundColor = theme.getColor(foreground); if (foregroundColor) { - collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); + collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); } const buttonBackgroundColor = theme.getColor(buttonBackground); if (buttonBackgroundColor) { - collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action { background-color: ${buttonBackgroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action { background-color: ${buttonBackgroundColor}; }`); + collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label { background-color: ${buttonBackgroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.label { background-color: ${buttonBackgroundColor}; }`); } const buttonForegroundColor = theme.getColor(buttonForeground); if (buttonForegroundColor) { - collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action { color: ${buttonForegroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action { color: ${buttonForegroundColor}; }`); + collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label { color: ${buttonForegroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.label { color: ${buttonForegroundColor}; }`); } const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground); if (buttonHoverBackgroundColor) { - collector.addRule(`.extension .monaco-action-bar .action-item:hover .action-label.extension-action { background-color: ${buttonHoverBackgroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item:hover .action-label.extension-action { background-color: ${buttonHoverBackgroundColor}; }`); - } - - const contrastBorderColor = theme.getColor(contrastBorder); - if (contrastBorderColor) { - collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action { border: 1px solid ${contrastBorderColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action { border: 1px solid ${contrastBorderColor}; }`); + collector.addRule(`.extension-list-item .monaco-action-bar .action-item:hover .action-label.extension-action.label { background-color: ${buttonHoverBackgroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item:hover .action-label.extension-action.label { background-color: ${buttonHoverBackgroundColor}; }`); } const extensionButtonProminentBackgroundColor = theme.getColor(extensionButtonProminentBackground); if (extensionButtonProminentBackground) { - collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); + collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); } const extensionButtonProminentForegroundColor = theme.getColor(extensionButtonProminentForeground); if (extensionButtonProminentForeground) { - collector.addRule(`.extension .monaco-action-bar .action-item .action-label.extension-action.prominent { color: ${extensionButtonProminentForegroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.prominent { color: ${extensionButtonProminentForegroundColor}; }`); + collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label.prominent { color: ${extensionButtonProminentForegroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.label.prominent { color: ${extensionButtonProminentForegroundColor}; }`); } const extensionButtonProminentHoverBackgroundColor = theme.getColor(extensionButtonProminentHoverBackground); if (extensionButtonProminentHoverBackground) { - collector.addRule(`.extension .monaco-action-bar .action-item:hover .action-label.extension-action.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item:hover .action-label.extension-action.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); + collector.addRule(`.extension-list-item .monaco-action-bar .action-item:hover .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item:hover .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); + } + + const contrastBorderColor = theme.getColor(contrastBorder); + if (contrastBorderColor) { + collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action { border: 1px solid ${contrastBorderColor}; }`); + collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action { border: 1px solid ${contrastBorderColor}; }`); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index a848cb48df..418aa2cfa1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; +import 'vs/css!./media/extension'; import { append, $, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { IAction } from 'vs/base/common/actions'; @@ -14,15 +14,13 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, SyncIgnoredIconAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { Label, RatingsWidget, /*InstallCountWidget,*/ RecommendationWidget, RemoteBadgeWidget, TooltipWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; +import { Label, RatingsWidget, /*InstallCountWidget, */RecommendationWidget, RemoteBadgeWidget, TooltipWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { isLanguagePackExtension, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { CodiconLabel } from 'vs/base/browser/ui/codiconLabel/codiconLabel'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; export interface IExtensionsViewState { onFocus: Event; @@ -38,7 +36,6 @@ export interface ITemplateData { //installCount: HTMLElement; //ratings: HTMLElement; author: HTMLElement; - syncIgnored: HTMLElement; description: HTMLElement; extension: IExtension | null; disposables: IDisposable[]; @@ -62,17 +59,17 @@ export class Renderer implements IPagedRenderer { @IExtensionService private readonly extensionService: IExtensionService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IConfigurationService private readonly configurationService: IConfigurationService, ) { } get templateId() { return 'extension'; } renderTemplate(root: HTMLElement): ITemplateData { - const recommendationWidget = this.instantiationService.createInstance(RecommendationWidget, root); - const element = append(root, $('.extension')); + const recommendationWidget = this.instantiationService.createInstance(RecommendationWidget, append(root, $('.extension-bookmark-container'))); + const element = append(root, $('.extension-list-item')); const iconContainer = append(element, $('.icon-container')); const icon = append(iconContainer, $('img.icon')); const iconRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, iconContainer, false); + const extensionPackBadgeWidget = this.instantiationService.createInstance(ExtensionPackBadgeWidget, iconContainer); const details = append(element, $('.details')); const headerContainer = append(details, $('.header-container')); const header = append(headerContainer, $('.header')); @@ -84,9 +81,6 @@ export class Renderer implements IPagedRenderer { const description = append(details, $('.description.ellipsis')); const footer = append(details, $('.footer')); const author = append(footer, $('.author.ellipsis')); - const syncIgnored = append(footer, $('.sync-ignored.ellipsis')); - const syncIgnoredLabel = new CodiconLabel(syncIgnored); - syncIgnoredLabel.text = '$(eye-closed) ' + localize('extensionSyncIgnoredLabel', 'Sync: Ignored'); const actionbar = new ActionBar(footer, { animated: false, actionViewItemProvider: (action: IAction) => { @@ -102,6 +96,7 @@ export class Renderer implements IPagedRenderer { const reloadAction = this.instantiationService.createInstance(ReloadAction); const actions = [ this.instantiationService.createInstance(StatusLabelAction), + this.instantiationService.createInstance(SyncIgnoredIconAction), this.instantiationService.createInstance(UpdateAction), reloadAction, this.instantiationService.createInstance(InstallAction), @@ -116,6 +111,7 @@ export class Renderer implements IPagedRenderer { const widgets = [ recommendationWidget, iconRemoteBadgeWidget, + extensionPackBadgeWidget, headerRemoteBadgeWidget, tooltipWidget, this.instantiationService.createInstance(Label, version, (e: IExtension) => e.version), @@ -129,7 +125,7 @@ export class Renderer implements IPagedRenderer { const disposables = combinedDisposable(...actions, ...widgets, actionbar, extensionContainers, extensionTooltipAction); return { - root, element, icon, name, /*installCount,*/ syncIgnored, /*ratings,*/ author, description, disposables: [disposables], actionbar, + root, element, icon, name, /*installCount, ratings, */author, description, disposables: [disposables], actionbar, extensionDisposables: [], set extension(extension: IExtension) { extensionContainers.extension = extension; @@ -210,24 +206,9 @@ export class Renderer implements IPagedRenderer { } }, this, data.extensionDisposables); - - this.updateExtensionIgnored(extension, data); - this.configurationService.onDidChangeConfiguration(e => { - if (e.affectedKeys.includes('sync.ignoredExtensions')) { - this.updateExtensionIgnored(extension, data); - } - }, data.extensionDisposables); } disposeTemplate(data: ITemplateData): void { data.disposables = dispose(data.disposables); } - - private updateExtensionIgnored(extension: IExtension, data: ITemplateData): void { - data.syncIgnored.style.display = this.extensionIsIgnored(extension.identifier) ? 'block' : 'none'; - } - - private extensionIsIgnored(identifier: IExtensionIdentifier): boolean { - return (this.configurationService.getValue('sync.ignoredExtensions') || []).some(id => areSameExtensions({ id }, identifier)); // {{SQL CARBON EDIT}} Temporary fix until VS Code fix is merged in - } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 96e97a1844..07ddbbc7a1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Action } from 'vs/base/common/actions'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { Event } from 'vs/base/common/event'; @@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IListService, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; @@ -22,6 +22,64 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { CancellationToken } from 'vs/base/common/cancellation'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IColorMapping } from 'vs/platform/theme/common/styler'; +import { Renderer, Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList'; +import { listFocusForeground, listFocusBackground } from 'vs/platform/theme/common/colorRegistry'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; + +export class ExtensionsGridView extends Disposable { + + readonly element: HTMLElement; + private readonly renderer: Renderer; + private readonly delegate: Delegate; + private readonly disposableStore: DisposableStore; + + constructor( + parent: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + this.element = dom.append(parent, dom.$('.extensions-grid-view')); + this.renderer = this.instantiationService.createInstance(Renderer, { onFocus: Event.None, onBlur: Event.None }); + this.delegate = new Delegate(); + this.disposableStore = new DisposableStore(); + } + + setExtensions(extensions: IExtension[]): void { + this.disposableStore.clear(); + extensions.forEach((e, index) => this.renderExtension(e, index)); + } + + private renderExtension(extension: IExtension, index: number): void { + const extensionContainer = dom.append(this.element, dom.$('.extension-container')); + extensionContainer.style.height = `${this.delegate.getHeight()}px`; + extensionContainer.style.width = `350px`; + extensionContainer.setAttribute('tabindex', '0'); + + const template = this.renderer.renderTemplate(extensionContainer); + this.disposableStore.add(toDisposable(() => this.renderer.disposeTemplate(template))); + + const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction); + openExtensionAction.extension = extension; + template.name.setAttribute('tabindex', '0'); + + const handleEvent = (e: StandardMouseEvent | StandardKeyboardEvent) => { + if (e instanceof StandardKeyboardEvent && e.keyCode !== KeyCode.Enter) { + return; + } + openExtensionAction.run(e.ctrlKey || e.metaKey); + e.stopPropagation(); + e.preventDefault(); + }; + + this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.CLICK, (e: MouseEvent) => handleEvent(new StandardMouseEvent(e)))); + this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => handleEvent(new StandardKeyboardEvent(e)))); + this.disposableStore.add(dom.addDisposableListener(extensionContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => handleEvent(new StandardKeyboardEvent(e)))); + + this.renderer.renderElement(extension, index, template); + } +} export interface IExtensionTemplateData { icon: HTMLImageElement; @@ -101,7 +159,7 @@ export class ExtensionRenderer implements IListRenderer { - if (this._extensionData) { - return this.extensionsWorkdbenchService.open(this._extensionData.extension, { sideByside }); + if (this._extension) { + return this.extensionsWorkdbenchService.open(this._extension, { sideByside }); } return Promise.resolve(); } @@ -246,24 +304,40 @@ export class ExtensionData implements IExtensionData { async getChildren(): Promise { if (this.hasChildren) { - const localById = this.extensionsWorkbenchService.local.reduce((result, e) => { result.set(e.identifier.id.toLowerCase(), e); return result; }, new Map()); - const result: IExtension[] = []; - const toQuery: string[] = []; - for (const extensionId of this.childrenExtensionIds) { - const id = extensionId.toLowerCase(); - const local = localById.get(id); - if (local) { - result.push(local); - } else { - toQuery.push(id); - } - } - if (toQuery.length) { - const galleryResult = await this.extensionsWorkbenchService.queryGallery({ names: toQuery, pageSize: toQuery.length }, CancellationToken.None); - result.push(...galleryResult.firstPage); - } + const result: IExtension[] = await getExtensions(this.childrenExtensionIds, this.extensionsWorkbenchService); return result.map(extension => new ExtensionData(extension, this, this.getChildrenExtensionIds, this.extensionsWorkbenchService)); } return null; } } + +export async function getExtensions(extensions: string[], extensionsWorkbenchService: IExtensionsWorkbenchService): Promise { + const localById = extensionsWorkbenchService.local.reduce((result, e) => { result.set(e.identifier.id.toLowerCase(), e); return result; }, new Map()); + const result: IExtension[] = []; + const toQuery: string[] = []; + for (const extensionId of extensions) { + const id = extensionId.toLowerCase(); + const local = localById.get(id); + if (local) { + result.push(local); + } else { + toQuery.push(id); + } + } + if (toQuery.length) { + const galleryResult = await extensionsWorkbenchService.queryGallery({ names: toQuery, pageSize: toQuery.length }, CancellationToken.None); + result.push(...galleryResult.firstPage); + } + return result; +} + +registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + const focusBackground = theme.getColor(listFocusBackground); + if (focusBackground) { + collector.addRule(`.extensions-grid-view .extension-container:focus { background-color: ${focusBackground}; outline: none; }`); + } + const focusForeground = theme.getColor(listFocusForeground); + if (focusForeground) { + collector.addRule(`.extensions-grid-view .extension-container:focus { color: ${focusForeground}; }`); + } +}); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 67413de721..a52a512718 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -41,7 +41,6 @@ import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IAction, Action } from 'vs/base/common/actions'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; -import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; @@ -106,7 +105,6 @@ export class ExtensionsListView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExperimentService private readonly experimentService: IExperimentService, - @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @@ -233,12 +231,10 @@ export class ExtensionsListView extends ViewPane { private async onContextMenu(e: IListContextMenuEvent): Promise { if (e.element) { const runningExtensions = await this.extensionService.getExtensions(); - const colorThemes = await this.workbenchThemeService.getColorThemes(); - const fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); const manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); manageExtensionAction.extension = e.element; if (manageExtensionAction.enabled) { - const groups = manageExtensionAction.getActionGroups(runningExtensions, colorThemes, fileIconThemes); + const groups = await manageExtensionAction.getActionGroups(runningExtensions); let actions: IAction[] = []; for (const menuActions of groups) { actions = [...actions, ...menuActions, new Separator()]; @@ -965,7 +961,6 @@ export class ServerExtensionsView extends ExtensionsListView { @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IExperimentService experimentService: IExperimentService, - @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, @@ -976,7 +971,7 @@ export class ServerExtensionsView extends ExtensionsListView { @IPreferencesService preferencesService: IPreferencesService, ) { options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index fb324ae70d..4bea4f0d22 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/extensionsWidgets'; import { Disposable, toDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { IExtension, IExtensionsWorkbenchService, IExtensionContainer } from 'vs/workbench/contrib/extensions/common/extensions'; -import { append, $, addClass } from 'vs/base/browser/dom'; +import { append, $, addClass, removeNode } from 'vs/base/browser/dom'; import * as platform from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { IExtensionTipsService, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -16,6 +16,7 @@ import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeServic import { EXTENSION_BADGE_REMOTE_BACKGROUND, EXTENSION_BADGE_REMOTE_FOREGROUND } from 'vs/workbench/common/theme'; import { Emitter, Event } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -222,7 +223,7 @@ export class RecommendationWidget extends ExtensionWidget { } const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); if (extRecommendations[this.extension.identifier.id.toLowerCase()]) { - this.element = append(this.parent, $('div.bookmark')); + this.element = append(this.parent, $('div.extension-bookmark')); const recommendation = append(this.element, $('.recommendation')); append(recommendation, $('span.codicon.codicon-star')); const applyBookmarkStyle = (theme: IColorTheme) => { @@ -285,7 +286,7 @@ class RemoteBadge extends Disposable { @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService ) { super(); - this.element = $('div.extension-remote-badge'); + this.element = $('div.extension-badge.extension-remote-badge'); this.render(); } @@ -315,3 +316,32 @@ class RemoteBadge extends Disposable { } } } + +export class ExtensionPackCountWidget extends ExtensionWidget { + + private element: HTMLElement | undefined; + + constructor( + private readonly parent: HTMLElement, + ) { + super(); + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + if (this.element) { + removeNode(this.element); + } + } + + render(): void { + this.clear(); + if (!this.extension || !this.extension.extensionPack.length) { + return; + } + this.element = append(this.parent, $('.extension-badge.extension-pack-badge')); + const countBadge = new CountBadge(this.element); + countBadge.setCount(this.extension.extensionPack.length); + } +} diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css new file mode 100644 index 0000000000..60b8ca0c51 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.extension-bookmark-container { + position: relative; +} + +.extension-bookmark-container > .extension-bookmark { + position: absolute; +} + +.extension-list-item { + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 0 0 0 16px; + overflow: hidden; + display: flex; +} + +.extension-list-item > .icon-container { + position: relative; +} + +.extension-list-item > .icon-container > .icon { + width: 42px; + height: 42px; + padding: 10px 14px 10px 0; + flex-shrink: 0; + object-fit: contain; +} + +.extension-list-item > .icon-container .extension-badge { + position: absolute; + bottom: 5px; + width: 22px; + height: 22px; + line-height: 22px; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.extension-list-item > .icon-container .extension-badge.extension-remote-badge { + right: 5px; +} + +.extension-list-item > .icon-container .extension-remote-badge .codicon { + color: currentColor; +} + +.extension-list-item > .details { + flex: 1; + padding: 4px 0; + overflow: hidden; +} + +.extension-list-item > .details > .header-container { + height: 19px; + display: flex; + overflow: hidden; + padding-right: 11px; +} + +.extension-list-item > .details > .header-container > .header { + display: flex; + align-items: baseline; + flex-wrap: nowrap; + overflow: hidden; + flex: 1; + min-width: 0; +} + +.extension-list-item > .details > .header-container > .header > .name { + font-weight: bold; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.extension-list-item > .details > .header-container > .header > .version { + opacity: 0.85; + font-size: 80%; + padding-left: 6px; + min-width: fit-content; + min-width: -moz-fit-content; +} + +.extension-list-item > .details > .header-container > .header > .version { + flex: 1; +} + +.extension-list-item > .details > .header-container > .header > .install-count, +.extension-list-item > .details > .header-container > .header > .ratings { + display: flex; + align-items: center; +} + +.extension-list-item > .details > .header-container > .header > .install-count:not(:empty) { + font-size: 80%; + margin: 0 6px; +} + +.extension-list-item > .details > .header-container > .header .codicon { + font-size: 120%; + margin-right: 2px; + -webkit-mask: inherit; +} + +.extension-list-item > .details > .header-container > .header > .ratings { + text-align: right; +} + +.extension-list-item > .details > .header-container > .header > .extension-remote-badge-container { + margin-left: 6px; + display: none; +} + +.extension-list-item > .details > .header-container > .header .extension-remote-badge .codicon { + margin-right: 0; +} + +.extension-list-item > .details > .header-container > .header .extension-remote-badge { + width: 14px; + height: 14px; + line-height: 14px; + border-radius: 20px; + text-align: center; + display: flex; + align-items: center; + justify-content: center; +} + +.extension-list-item > .details > .header-container > .header .extension-remote-badge > .codicon { + font-size: 12px; + color: currentColor; +} + +.extension-list-item > .details > .description { + padding-right: 11px; +} + +.extension-list-item > .details > .footer { + display: flex; + justify-content: flex-end; + padding-right: 7px; + height: 18px; + overflow: hidden; +} + +.extension-list-item > .details > .footer > .author { + flex: 1; + font-size: 90%; + opacity: 0.9; + font-weight: 600; +} + +.extension-list-item .ellipsis { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.extension-list-item > .details > .footer > .monaco-action-bar > .actions-container { + flex-wrap: wrap-reverse; +} + +.extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .extension-action.label { + max-width: 150px; +} diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index bb9fbd49ab..f284f62df9 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -12,6 +12,17 @@ text-overflow: ellipsis; } +.monaco-action-bar .action-item .action-label.extension-action.text, +.monaco-action-bar .action-item .action-label.extension-action.label { + line-height: 14px; + margin-top: 2px; +} + +.monaco-action-bar .action-item .action-label.extension-action.icon { + height: 18px; + width: 10px; +} + .monaco-action-bar .action-item .action-label.extension-action.multiserver.install:after, .monaco-action-bar .action-item .action-label.extension-action.multiserver.update:after, .monaco-action-bar .action-item .action-label.extension-action.extension-editor-dropdown-action.dropdown:after { @@ -20,6 +31,7 @@ font-size: 80%; } +.monaco-action-bar .action-item.disabled .action-label.extension-action.hide, .monaco-action-bar .action-item.disabled .action-label.extension-action.ignore, .monaco-action-bar .action-item.disabled .action-label.extension-action.undo-ignore, .monaco-action-bar .action-item.disabled .action-label.extension-action.install:not(.installing), @@ -31,6 +43,7 @@ .monaco-action-bar .action-item.disabled .action-label.disable-status.hide, .monaco-action-bar .action-item.disabled .action-label.system-disable.hide, .monaco-action-bar .action-item.disabled .action-label.extension-status-label.hide, +.monaco-action-bar .action-item .action-label.extension-action.manage.hide, .monaco-action-bar .action-item.disabled .action-label.malicious-status.not-malicious { display: none; } @@ -40,70 +53,11 @@ padding-right: 4px; } -.monaco-action-bar .action-item .action-label.disable-status, -.monaco-action-bar .action-item .action-label.malicious-status, -.monaco-action-bar .action-item.disabled .action-label.extension-status-label { - opacity: 0.9; - line-height: initial; - padding: 0 5px; +.monaco-action-bar .action-item.disabled .action-label.extension-action { + opacity: 1; } -.monaco-action-bar .action-item .action-label.disable-status, -.monaco-action-bar .action-item .action-label.malicious-status { - border-radius: 4px; - color: inherit; - background-color: transparent; +.monaco-action-bar .action-item.disabled .action-label.extension-action.text { + opacity: 0.9; font-style: italic; } - -.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.disable-status { - margin-left: 0; - margin-top: 6px; - padding-left: 0; -} - -.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.system-disable { - margin-right: 0.15em; -} - -.extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.system-disable.codicon-info, -.extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.system-disable.codicon-warning { - margin-top: 0.25em; - margin-left: 0.1em; -} - -.monaco-action-bar .action-item .action-label.system-disable.codicon { - opacity: 1; - height: 18px; - width: 10px; -} - -.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.extension-status-label, -.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.disable-status, -.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.malicious-status { - font-weight: normal; -} - -.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.extension-status-label:hover, -.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.disable-status:hover, -.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.malicious-status:hover { - opacity: 0.9; -} - -.extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.extension-action.manage.hide { - display: none; -} - -.extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.extension-action.manage { - height: 18px; - width: 10px; - border: none; - color: inherit; - background: none; - outline-offset: 0px; - margin-top: 0.25em; -} - -.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .actions-container { - justify-content: flex-start; -} diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 408c8942b1..733951193f 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -154,16 +154,32 @@ justify-content: flex-start; } -.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label { - font-weight: 600; - margin: 4px 8px 4px 0px; - padding: 1px 6px; +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action { + margin-right: 8px; } -.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action { +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.label { + font-weight: 600; + padding: 1px 6px; max-width: 300px; } +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.system-disable { + margin-right: 0; +} + +.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.extension-status-label, +.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.disable-status, +.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.malicious-status { + font-weight: normal; +} + +.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.extension-status-label:hover, +.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.disable-status:hover, +.extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.malicious-status:hover { + opacity: 0.9; +} + .extension-editor > .header > .details > .subtext-container { display: block; float: left; @@ -183,6 +199,7 @@ margin-top: 5px; margin-right: 4px; } + .extension-editor > .header > .details > .subtext-container > .monaco-action-bar .action-label { margin-top: 4px; margin-left: 4px; @@ -190,6 +207,10 @@ padding-bottom: 2px; } +.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .actions-container { + justify-content: flex-start; +} + .extension-editor > .body { flex: 1; overflow: hidden; @@ -247,6 +268,52 @@ margin-left: 20px; } +.extension-editor > .body > .content > .extension-pack-readme { + height: 100%; +} + +.extension-editor > .body > .content > .extension-pack-readme > .extension-pack { + height: 224px; + padding-left: 20px; +} + +.extension-editor > .body > .content > .extension-pack-readme.narrow > .extension-pack { + height: 142px; +} + +.extension-editor > .body > .content > .extension-pack-readme > .readme-content { + height: calc(100% - 224px); +} + +.extension-editor > .body > .content > .extension-pack-readme.narrow > .readme-content { + height: calc(100% - 142px); +} + +.extension-editor > .body > .content > .extension-pack-readme > .extension-pack > .header, +.extension-editor > .body > .content > .extension-pack-readme > .extension-pack > .footer { + margin-bottom: 10px; + margin-right: 30px; + font-weight: bold; + font-size: 120%; + border-bottom: 1px solid rgba(128, 128, 128, 0.22); + padding: 4px 6px; + line-height: 22px; +} + +.extension-editor > .body > .content > .extension-pack-readme > .extension-pack > .extension-pack-content { + height: calc(100% - 60px); +} + +.extension-editor > .body > .content > .extension-pack-readme > .extension-pack > .extension-pack-content > .monaco-scrollable-element { + height: 100%; +} + +.extension-editor > .body > .content .extension-pack-readme > .extension-pack > .extension-pack-content > .monaco-scrollable-element > .subcontent { + height: 100%; + overflow-y: scroll; + box-sizing: border-box; +} + .extension-editor > .body > .content > .monaco-scrollable-element > .subcontent { height: 100%; padding: 20px; @@ -398,3 +465,25 @@ font-weight: 600; opacity: 0.6; } + +.extension-editor .extensions-grid-view { + display: flex; + flex-wrap: wrap; +} + +.extension-editor .extensions-grid-view > .extension-container { + margin: 0 10px 20px 0; +} + +.extension-editor .extensions-grid-view .extension-list-item { + cursor: default; +} + +.extension-editor .extensions-grid-view .extension-list-item > .details .header > .name { + cursor: pointer; +} + +.extension-editor .extensions-grid-view > .extension-container:focus > .extension-list-item > .details .header > .name, +.extension-editor .extensions-grid-view > .extension-container:focus > .extension-list-item > .details .header > .name:hover { + text-decoration: underline; +} diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index 5f7466e43f..2f9d902154 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -74,241 +74,47 @@ flex-shrink: 0; } -.extensions-viewlet > .extensions .monaco-list-row > .bookmark { - display: inline-block; - height: 20px; - width: 20px; -} - -.extensions-viewlet > .extensions .monaco-list-row > .bookmark > .recommendation { - border-right: 20px solid transparent; - border-top: 20px solid; - box-sizing: border-box; -} - -.extensions-viewlet > .extensions .monaco-list-row > .bookmark > .recommendation > .codicon { +.extensions-viewlet > .extensions .extension-list-item { position: absolute; - top: 1px; - left: 1px; - color: inherit; - font-size: 80%; } -.extensions-viewlet > .extensions .extension { - box-sizing: border-box; - width: 100%; - height: 100%; - padding: 0 0 0 16px; - overflow: hidden; - display: flex; - position: absolute; - top: 0; -} - -.extensions-viewlet > .extensions .extension.loading { +.extensions-viewlet > .extensions .extension-list-item.loading { background: url('loading.svg') center center no-repeat; } -.extensions-viewlet > .extensions .monaco-list-row > .extension > .icon-container { - position: relative; -} - -.extensions-viewlet > .extensions .extension > .icon-container > .icon { - width: 42px; - height: 42px; - padding: 10px 14px 10px 0; - flex-shrink: 0; - object-fit: contain; -} - -.extensions-viewlet > .extensions .monaco-list-row > .extension > .icon-container .extension-remote-badge { - position: absolute; - right: 5px; - bottom: 5px; - width: 22px; - height: 22px; - line-height: 22px; - border-radius: 20px; - display: flex; - align-items: center; - justify-content: center; -} - -.extensions-viewlet > .extensions .monaco-list-row > .extension > .icon-container .extension-remote-badge .codicon { - color: currentColor; -} - -.extensions-viewlet > .extensions .monaco-list-row > .extension > .details > .header-container > .header > .extension-remote-badge-container { - margin-left: 6px; -} - -.extensions-viewlet > .extensions .monaco-list-row > .extension > .details > .header-container > .header .extension-remote-badge { - width: 14px; - height: 14px; - line-height: 14px; - border-radius: 20px; - text-align: center; - display: flex; - align-items: center; - justify-content: center; -} - -.extensions-viewlet > .extensions .monaco-list-row > .extension > .details > .header-container > .header .extension-remote-badge > .codicon { - font-size: 12px; - color: currentColor; -} - -.extensions-viewlet.narrow > .extensions .extension > .icon-container, -.extensions-viewlet > .extensions .extension.loading > .icon-container { +.extensions-viewlet.narrow > .extensions .extension-list-item > .icon-container, +.extensions-viewlet > .extensions .extension-list-item.loading > .icon-container { display: none; } -.extensions-viewlet > .extensions .extension > .details { - flex: 1; - padding: 4px 0; - overflow: hidden; -} - -.extensions-viewlet > .extensions .extension > .details > .header-container { - height: 19px; - display: flex; - overflow: hidden; - padding-right: 11px; -} - -.extensions-viewlet > .extensions .extension > .details > .header-container > .header { - display: flex; - align-items: baseline; - flex-wrap: nowrap; - overflow: hidden; - flex: 1; - min-width: 0; -} - -.extensions-viewlet > .extensions .extension > .details > .header-container > .header > .name { - font-weight: bold; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -} - -.extensions-viewlet > .extensions .extension > .details > .header-container > .header > .version { - opacity: 0.85; - font-size: 80%; - padding-left: 6px; - min-width: fit-content; - min-width: -moz-fit-content; -} - -.extensions-viewlet:not(.narrow) > .extensions .extension > .details > .header-container > .header > .version { - flex: 1; -} - -.extensions-viewlet > .extensions .extension > .details > .header-container > .header > .install-count, -.extensions-viewlet > .extensions .extension > .details > .header-container > .header > .ratings { - display: flex; - align-items: center; -} - -.extensions-viewlet > .extensions .extension > .details > .header-container > .header > .install-count:not(:empty) { - font-size: 80%; - margin: 0 6px; -} - -.extensions-viewlet > .extensions .extension > .details > .header-container > .header .codicon { - font-size: 120%; - margin-right: 2px; - -webkit-mask: inherit; -} - -.extensions-viewlet>.extensions .extension>.details>.header-container>.header .extension-remote-badge .codicon { - margin-right: 0; -} - -.extensions-viewlet > .extensions .extension > .details > .header-container > .header > .ratings { - text-align: right; -} - -.extensions-viewlet:not(.narrow) > .extensions .extension > .details > .header-container > .header > .extension-remote-badge-container, -.extensions-viewlet.narrow > .extensions .extension > .details > .header-container > .header > .ratings, -.extensions-viewlet.narrow > .extensions .extension > .details > .header-container > .header > .install-count { +.extensions-viewlet:not(.narrow) > .extensions .extension-list-item > .details > .header-container > .header > .extension-remote-badge-container, +.extensions-viewlet.narrow > .extensions .extension-list-item > .details > .header-container > .header > .ratings, +.extensions-viewlet.narrow > .extensions .extension-list-item > .details > .header-container > .header > .install-count { display: none; } -.extensions-viewlet > .extensions .extension > .details > .description { - padding-right: 11px; -} - -.extensions-viewlet > .extensions .extension > .details > .footer { - display: flex; - justify-content: flex-end; - padding-right: 7px; - height: 24px; - overflow: hidden; -} - -.extensions-viewlet > .extensions .extension > .details > .footer > .author { - flex: 1; - font-size: 90%; - opacity: 0.9; - font-weight: 600; -} - -.extensions-viewlet > .extensions .extension > .details > .footer > .sync-ignored { - font-size: 11px; -} - -.extensions-viewlet > .extensions .extension > .details > .footer > .sync-ignored > .codicon { - font-size: 14px; - vertical-align: text-top; -} - -.extensions-viewlet > .extensions .selected .extension > .details > .footer > .author, -.extensions-viewlet > .extensions .selected.focused .extension > .details > .footer > .author { +.extensions-viewlet > .extensions .selected .extension-list-item > .details > .footer > .author, +.extensions-viewlet > .extensions .selected.focused .extension-list-item > .details > .footer > .author { opacity: 1; } -.extensions-viewlet > .extensions .extension > .details > .footer > .monaco-action-bar > .actions-container { - flex-wrap: wrap-reverse; -} - -.extensions-viewlet > .extensions .extension > .details > .footer > .monaco-action-bar > .actions-container .extension-action { - max-width: 150px; -} - -.extensions-viewlet.narrow > .extensions .extension > .details > .footer > .monaco-action-bar > .actions-container .extension-action { +.extensions-viewlet.narrow > .extensions .extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .extension-action { max-width: 100px; } -.extensions-viewlet > .extensions .extension > .details > .footer > .monaco-action-bar .action-label { - margin-top: 0.3em; - margin-left: 0.3em; - line-height: 14px; -} - -.extensions-viewlet > .extensions .extension > .details > .footer > .monaco-action-bar .action-label:not(:empty) { - opacity: 0.9; -} - .vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, .vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .icon-container > .icon, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .icon-container > .icon, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .header-container, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .header-container, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .description, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .description, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .footer > .author, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .details > .footer > .author { +.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .icon-container > .icon, +.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .icon-container > .icon, +.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .header-container, +.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .header-container, +.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .description, +.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .description, +.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .footer > .author, +.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .footer > .author { opacity: 0.5; } -.extensions-viewlet > .extensions .extension .ellipsis { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - .extensions-badge.progress-badge > .badge-content { background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4="); background-position: center center; diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css index f2db18e4fa..be7974523a 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css @@ -32,3 +32,24 @@ .extension-ratings .codicon-star-empty { opacity: .4; } + +.extension-bookmark { + display: inline-block; + height: 20px; + width: 20px; +} + +.extension-bookmark > .recommendation { + border-right: 20px solid transparent; + border-top: 20px solid; + box-sizing: border-box; + position: relative; +} + +.extension-bookmark > .recommendation > .codicon { + position: absolute; + bottom: 9px; + left: 1px; + color: inherit; + font-size: 80%; +} diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 15c4d453e9..e60e696d12 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -130,7 +130,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = paged.firstPage[0]; assert.ok(!testObject.enabled); assert.equal('Install', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); }); }); }); @@ -148,7 +148,7 @@ suite('ExtensionsActions Test', () => { assert.ok(!testObject.enabled); assert.equal('Installing', testObject.label); - assert.equal('extension-action install installing', testObject.class); + assert.equal('extension-action label install installing', testObject.class); }); }); @@ -215,7 +215,7 @@ suite('ExtensionsActions Test', () => { uninstallEvent.fire(local.identifier); assert.ok(!testObject.enabled); assert.equal('Uninstalling', testObject.label); - assert.equal('extension-action uninstall uninstalling', testObject.class); + assert.equal('extension-action label uninstall uninstalling', testObject.class); }); }); @@ -230,7 +230,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Uninstall', testObject.label); - assert.equal('extension-action uninstall', testObject.class); + assert.equal('extension-action label uninstall', testObject.class); }); }); @@ -245,7 +245,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(!testObject.enabled); assert.equal('Uninstall', testObject.label); - assert.equal('extension-action uninstall', testObject.class); + assert.equal('extension-action label uninstall', testObject.class); }); }); @@ -281,7 +281,7 @@ suite('ExtensionsActions Test', () => { assert.ok(testObject.enabled); assert.equal('Uninstall', testObject.label); - assert.equal('extension-action uninstall', testObject.class); + assert.equal('extension-action label uninstall', testObject.class); }); }); @@ -290,7 +290,7 @@ suite('ExtensionsActions Test', () => { instantiationService.createInstance(ExtensionContainers, [testObject]); assert.ok(!testObject.enabled); - assert.equal('extension-action prominent install no-extension', testObject.class); + assert.equal('extension-action label prominent install no-extension', testObject.class); }); test('Test CombinedInstallAction when extension is system extension', () => { @@ -303,7 +303,7 @@ suite('ExtensionsActions Test', () => { .then(extensions => { testObject.extension = extensions[0]; assert.ok(!testObject.enabled); - assert.equal('extension-action prominent install no-extension', testObject.class); + assert.equal('extension-action label prominent install no-extension', testObject.class); }); }); @@ -319,7 +319,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = paged.firstPage[0]; assert.ok(testObject.enabled); assert.equal('Install', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); }); }); @@ -334,7 +334,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Uninstall', testObject.label); - assert.equal('extension-action uninstall', testObject.class); + assert.equal('extension-action label uninstall', testObject.class); }); }); @@ -351,7 +351,7 @@ suite('ExtensionsActions Test', () => { assert.ok(!testObject.enabled); assert.equal('Installing', testObject.label); - assert.equal('extension-action install installing', testObject.class); + assert.equal('extension-action label install installing', testObject.class); }); }); @@ -370,7 +370,7 @@ suite('ExtensionsActions Test', () => { installEvent.fire({ identifier: gallery.identifier, gallery }); assert.ok(!testObject.enabled); assert.equal('Installing', testObject.label); - assert.equal('extension-action install installing', testObject.class); + assert.equal('extension-action label install installing', testObject.class); }); }); @@ -386,7 +386,7 @@ suite('ExtensionsActions Test', () => { uninstallEvent.fire(local.identifier); assert.ok(!testObject.enabled); assert.equal('Uninstalling', testObject.label); - assert.equal('extension-action uninstall uninstalling', testObject.class); + assert.equal('extension-action label uninstall uninstalling', testObject.class); }); }); @@ -498,7 +498,7 @@ suite('ExtensionsActions Test', () => { .then(extensions => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); - assert.equal('extension-action manage codicon-gear', testObject.class); + assert.equal('extension-action icon manage codicon-gear', testObject.class); assert.equal('', testObject.tooltip); }); }); @@ -513,7 +513,7 @@ suite('ExtensionsActions Test', () => { .then(page => { testObject.extension = page.firstPage[0]; assert.ok(!testObject.enabled); - assert.equal('extension-action manage codicon-gear hide', testObject.class); + assert.equal('extension-action icon manage codicon-gear hide', testObject.class); assert.equal('', testObject.tooltip); }); }); @@ -530,7 +530,7 @@ suite('ExtensionsActions Test', () => { installEvent.fire({ identifier: gallery.identifier, gallery }); assert.ok(!testObject.enabled); - assert.equal('extension-action manage codicon-gear hide', testObject.class); + assert.equal('extension-action icon manage codicon-gear hide', testObject.class); assert.equal('', testObject.tooltip); }); }); @@ -548,7 +548,7 @@ suite('ExtensionsActions Test', () => { didInstallEvent.fire({ identifier: gallery.identifier, gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }); assert.ok(testObject.enabled); - assert.equal('extension-action manage codicon-gear', testObject.class); + assert.equal('extension-action icon manage codicon-gear', testObject.class); assert.equal('', testObject.tooltip); }); }); @@ -563,7 +563,7 @@ suite('ExtensionsActions Test', () => { .then(extensions => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); - assert.equal('extension-action manage codicon-gear', testObject.class); + assert.equal('extension-action icon manage codicon-gear', testObject.class); assert.equal('', testObject.tooltip); }); }); @@ -580,7 +580,7 @@ suite('ExtensionsActions Test', () => { uninstallEvent.fire(local.identifier); assert.ok(!testObject.enabled); - assert.equal('extension-action manage codicon-gear', testObject.class); + assert.equal('extension-action icon manage codicon-gear', testObject.class); assert.equal('Uninstalling', testObject.tooltip); }); }); @@ -1549,7 +1549,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install in remote', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); }); test('Test remote install action when installing local workspace extension', async () => { @@ -1575,12 +1575,12 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install in remote', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, gallery }); assert.ok(testObject.enabled); assert.equal('Installing', testObject.label); - assert.equal('extension-action install installing', testObject.class); + assert.equal('extension-action label install installing', testObject.class); }); test('Test remote install action when installing local workspace extension is finished', async () => { @@ -1608,12 +1608,12 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install in remote', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); onInstallExtension.fire({ identifier: localWorkspaceExtension.identifier, gallery }); assert.ok(testObject.enabled); assert.equal('Installing', testObject.label); - assert.equal('extension-action install installing', testObject.class); + assert.equal('extension-action label install installing', testObject.class); const installedExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); onDidInstallEvent.fire({ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install }); @@ -1639,7 +1639,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install in remote', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); }); test('Test remote install action is disabled when extension is not set', async () => { @@ -1856,7 +1856,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install in remote', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); }); test('Test remote install action is disabled if local language pack extension is uninstalled', async () => { @@ -1902,7 +1902,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install Locally', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); }); test('Test local install action when installing remote ui extension', async () => { @@ -1928,12 +1928,12 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install Locally', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); onInstallExtension.fire({ identifier: remoteUIExtension.identifier, gallery }); assert.ok(testObject.enabled); assert.equal('Installing', testObject.label); - assert.equal('extension-action install installing', testObject.class); + assert.equal('extension-action label install installing', testObject.class); }); test('Test local install action when installing remote ui extension is finished', async () => { @@ -1961,12 +1961,12 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install Locally', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); onInstallExtension.fire({ identifier: remoteUIExtension.identifier, gallery }); assert.ok(testObject.enabled); assert.equal('Installing', testObject.label); - assert.equal('extension-action install installing', testObject.class); + assert.equal('extension-action label install installing', testObject.class); const installedExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); onDidInstallEvent.fire({ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install }); @@ -1992,7 +1992,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install Locally', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); }); test('Test local install action is disabled when extension is not set', async () => { @@ -2212,7 +2212,7 @@ suite('ExtensionsActions Test', () => { testObject.extension = extensions[0]; assert.ok(testObject.enabled); assert.equal('Install Locally', testObject.label); - assert.equal('extension-action prominent install', testObject.class); + assert.equal('extension-action label prominent install', testObject.class); }); test('Test local install action is disabled if remote language pack extension is uninstalled', async () => { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 979909aaaa..a58188f9d6 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -241,9 +241,13 @@ class ResolveSaveConflictAction extends Action { @IEditorService private readonly editorService: IEditorService, @INotificationService private readonly notificationService: INotificationService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare")); + + // opt-in to syncing + storageKeysSyncRegistryService.registerStorageKey({ key: LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, version: 1 }); } async run(): Promise { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index e7fd65dde8..c755ff94a6 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -180,9 +180,9 @@ const hotExitConfiguration: IConfigurationPropertySchema = platform.isNative ? 'enum': [HotExitConfiguration.OFF, HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE], 'default': HotExitConfiguration.ON_EXIT, 'markdownEnumDescriptions': [ - nls.localize('hotExit.off', 'Disable hot exit.'), - nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows with backups will be restored upon next launch.'), - nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. To restore folder windows as they were before shutdown set `#window.restoreWindows#` to `all`.') + nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with dirty files.'), + nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of workspaces with unsaved files can be accessed via `File > Open Recent > More...`'), + nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of workspaces with unsaved files can be accessed via `File > Open Recent > More...`') ], 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) } : { @@ -191,7 +191,7 @@ const hotExitConfiguration: IConfigurationPropertySchema = platform.isNative ? 'enum': [HotExitConfiguration.OFF, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE], 'default': HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, 'markdownEnumDescriptions': [ - nls.localize('hotExit.off', 'Disable hot exit.'), + nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with dirty files.'), nls.localize('hotExit.onExitAndWindowCloseBrowser', 'Hot exit will be triggered when the browser quits or the window or tab is closed.') ], 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index cf387c2fc1..feecfe0134 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -46,6 +46,7 @@ import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/w import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; const $ = dom.$; @@ -415,8 +416,8 @@ export class OpenEditorsView extends ViewPane { private updateSize(): void { // Adjust expanded body size - this.minimumBodySize = this.getMinExpandedBodySize(); - this.maximumBodySize = this.getMaxExpandedBodySize(); + this.minimumBodySize = this.orientation === Orientation.VERTICAL ? this.getMinExpandedBodySize() : 170; + this.maximumBodySize = this.orientation === Orientation.VERTICAL ? this.getMaxExpandedBodySize() : Number.POSITIVE_INFINITY; } private updateDirtyIndicator(workingCopy?: IWorkingCopy): void { diff --git a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts index fb8f86430f..7699f6fadd 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts @@ -50,6 +50,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo super(); storageKeysSyncRegistryService.registerStorageKey({ key: LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY, version: 1 }); + storageKeysSyncRegistryService.registerStorageKey({ key: 'langugage.update.donotask', version: 1 }); this.checkAndInstall(); this._register(this.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e))); } diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index f9f1697cc8..8553e9ae7c 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -18,24 +18,33 @@ export const COPY_CELL_UP_COMMAND_ID = 'workbench.notebook.cell.copyUp'; export const COPY_CELL_DOWN_COMMAND_ID = 'workbench.notebook.cell.copyDown'; export const EXECUTE_CELL_COMMAND_ID = 'workbench.notebook.cell.execute'; +export const EXECUTE_ACTIVE_CELL_COMMAND_ID = 'workbench.notebook.cell.executeActive'; +export const CANCEL_CELL_COMMAND_ID = 'workbench.notebook.cell.cancelExecution'; +export const EXECUTE_NOTEBOOK_COMMAND_ID = 'workbench.notebook.executeNotebook'; +export const CANCEL_NOTEBOOK_COMMAND_ID = 'workbench.notebook.cancelExecution'; // Cell sizing related export const CELL_MARGIN = 20; export const CELL_RUN_GUTTER = 32; // TODO should be dynamic based on execution order width, and runnable enablement -export const EDITOR_TOOLBAR_HEIGHT = 22; +export const EDITOR_TOOLBAR_HEIGHT = 20; +export const BOTTOM_CELL_TOOLBAR_HEIGHT = 32; // Top margin of editor -export const EDITOR_TOP_MARGIN = 8; +export const EDITOR_TOP_MARGIN = 0; // Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` export const EDITOR_TOP_PADDING = 8; export const EDITOR_BOTTOM_PADDING = 8; // Cell context keys + +export const NOTEBOOK_VIEW_TYPE = 'notebookViewType'; export const NOTEBOOK_CELL_TYPE_CONTEXT_KEY = 'notebookCellType'; // code, markdown export const NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY = 'notebookCellEditable'; // bool export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY = 'notebookCellMarkdownEditMode'; // bool +export const NOTEBOOK_CELL_RUN_STATE_CONTEXT_KEY = 'notebookCellRunState'; // idle, running // Notebook context keys export const NOTEBOOK_EDITABLE_CONTEXT_KEY = 'notebookEditable'; +export const NOTEBOOK_EXECUTING_KEY = 'notebookExecuting'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts index db11fdd926..6617b46003 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; import { Action2, IAction2Options, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -11,12 +12,10 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { InputFocusedContext, InputFocusedContextKey, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { COPY_CELL_DOWN_COMMAND_ID, COPY_CELL_UP_COMMAND_ID, DELETE_CELL_COMMAND_ID, EDIT_CELL_COMMAND_ID, EXECUTE_CELL_COMMAND_ID, INSERT_CODE_CELL_ABOVE_COMMAND_ID, INSERT_CODE_CELL_BELOW_COMMAND_ID, INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, MOVE_CELL_DOWN_COMMAND_ID, MOVE_CELL_UP_COMMAND_ID, SAVE_CELL_COMMAND_ID, NOTEBOOK_CELL_TYPE_CONTEXT_KEY, NOTEBOOK_EDITABLE_CONTEXT_KEY, NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellRenderTemplate, CellEditState, ICellViewModel, INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, CellRunState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { CANCEL_CELL_COMMAND_ID, CANCEL_NOTEBOOK_COMMAND_ID, COPY_CELL_DOWN_COMMAND_ID, COPY_CELL_UP_COMMAND_ID, DELETE_CELL_COMMAND_ID, EDIT_CELL_COMMAND_ID, EXECUTE_ACTIVE_CELL_COMMAND_ID, EXECUTE_CELL_COMMAND_ID, EXECUTE_NOTEBOOK_COMMAND_ID, INSERT_CODE_CELL_ABOVE_COMMAND_ID, INSERT_CODE_CELL_BELOW_COMMAND_ID, INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, MOVE_CELL_DOWN_COMMAND_ID, MOVE_CELL_UP_COMMAND_ID, NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY, NOTEBOOK_CELL_TYPE_CONTEXT_KEY, NOTEBOOK_EDITABLE_CONTEXT_KEY, NOTEBOOK_EXECUTING_KEY, SAVE_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/constants'; +import { BaseCellRenderTemplate, CellEditState, CellRunState, ICellViewModel, INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; const enum CellToolbarOrder { MoveCellUp, @@ -56,6 +55,27 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: CANCEL_CELL_COMMAND_ID, + title: localize('notebookActions.cancel', "Cancel Execution"), + icon: { id: 'codicon/stop' } + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + return context.notebookEditor.cancelNotebookCellExecution(context.cell); + } +}); + export class ExecuteCellAction extends MenuItemAction { constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -74,6 +94,25 @@ export class ExecuteCellAction extends MenuItemAction { } } +export class CancelCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: CANCEL_CELL_COMMAND_ID, + title: localize('notebookActions.CancelCell', "Cancel Execution"), + icon: { id: 'codicon/stop' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + + registerAction2(class extends Action2 { constructor() { super({ @@ -146,34 +185,45 @@ registerAction2(class extends Action2 { registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.executeNotebook', + id: EXECUTE_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.executeNotebook', "Execute Notebook") }); } async run(accessor: ServicesAccessor): Promise { - let editorService = accessor.get(IEditorService); - let notebookService = accessor.get(INotebookService); - - let resource = editorService.activeEditor?.resource; - - if (!resource) { + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { return; } - let notebookProviders = notebookService.getContributedNotebookProviders(resource!); - - if (notebookProviders.length > 0) { - let viewType = notebookProviders[0].id; - notebookService.executeNotebook(viewType, resource); - } + return editor.executeNotebook(); } }); registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.executeNotebookCell', + id: CANCEL_NOTEBOOK_COMMAND_ID, + title: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution") + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + return editor.cancelNotebookExecution(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: EXECUTE_ACTIVE_CELL_COMMAND_ID, title: localize('notebookActions.executeNotebookCell', "Execute Notebook Active Cell") }); } @@ -269,21 +319,32 @@ registerAction2(class extends Action2 { MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { - id: 'workbench.action.executeNotebook', + id: EXECUTE_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.menu.executeNotebook', "Execute Notebook (Run all cells)"), - icon: { id: 'codicon/debug-start' } + icon: { id: 'codicon/run-all' } }, order: -1, group: 'navigation', - when: NOTEBOOK_EDITOR_FOCUSED + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(NOTEBOOK_EXECUTING_KEY)) +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: CANCEL_NOTEBOOK_COMMAND_ID, + title: localize('notebookActions.menu.cancelNotebook', "Cancel Notebook Execution"), + icon: { id: 'codicon/stop' } + }, + order: -1, + group: 'navigation', + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK) }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { - id: 'workbench.action.executeNotebookCell', + id: EXECUTE_ACTIVE_CELL_COMMAND_ID, title: localize('notebookActions.menu.execute', "Execute Notebook Cell"), - icon: { id: 'codicon/debug-continue' } + icon: { id: 'codicon/run' } }, order: -1, group: 'navigation', @@ -389,7 +450,7 @@ async function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor } export interface INotebookCellActionContext { - cellTemplate?: CellRenderTemplate; + cellTemplate?: BaseCellRenderTemplate; cell: ICellViewModel; notebookEditor: INotebookEditor; } @@ -446,6 +507,24 @@ registerAction2(class extends InsertCellCommand { } }); +export class InsertCodeCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: INSERT_CODE_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"), + // icon: { id: 'codicon/add' }, + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + registerAction2(class extends InsertCellCommand { constructor() { super( @@ -481,6 +560,23 @@ registerAction2(class extends InsertCellCommand { } }); +export class InsertMarkdownCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below") + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + registerAction2(class extends InsertCellCommand { constructor() { super( @@ -570,7 +666,7 @@ registerAction2(class extends Action2 { order: CellToolbarOrder.DeleteCell, when: ContextKeyExpr.equals(NOTEBOOK_EDITABLE_CONTEXT_KEY, true) }, - icon: { id: 'codicon/x' } + icon: { id: 'codicon/trash' } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.css b/src/vs/workbench/contrib/notebook/browser/notebook.css index b3952122ca..65b3451375 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/notebook.css @@ -140,7 +140,7 @@ visibility: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon-play { +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { margin-right: 8px; } @@ -202,6 +202,42 @@ visibility: visible; } +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { + position: absolute; + display: flex; + opacity: 0; +} + +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { + opacity: 1; +} + +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator { + height: 1px; + flex-grow: 1; + align-self: center; +} + +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator-short { + height: 1px; + width: 16px; + align-self: center; +} + +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .button { + display: flex; + margin: 0 8px; + align-self: center; + align-items: center; + white-space: pre; +} + +.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { + text-align: center; + font-size: 14px; + color: inherit; +} + .notebook-webview { position: absolute; z-index: 1000000; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 1d75ceb764..f406a19b9f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -6,6 +6,7 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -13,15 +14,16 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch } from 'vs/editor/common/model'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { NOTEBOOK_EDITABLE_CONTEXT_KEY, NOTEBOOK_EXECUTING_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { IModelDecorationsChangeAccessor, NotebookViewModel, CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellKind, IOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_EDITABLE_CONTEXT_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey(NOTEBOOK_EDITABLE_CONTEXT_KEY, true); +export const NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK = new RawContextKey(NOTEBOOK_EXECUTING_KEY, false); export interface NotebookLayoutInfo { width: number; @@ -43,6 +45,7 @@ export interface CodeCellLayoutInfo { readonly outputContainerOffset: number; readonly outputTotalHeight: number; readonly indicatorHeight: number; + readonly bottomToolbarOffset: number; } export interface CodeCellLayoutChangeEvent { @@ -69,7 +72,8 @@ export interface ICellViewModel { uri: URI; cellKind: CellKind; editState: CellEditState; - runState: CellRunState; + readonly runState: CellRunState; + currentTokenSource: CancellationTokenSource | undefined; focusMode: CellFocusMode; getText(): string; metadata: NotebookCellMetadata | undefined; @@ -147,6 +151,21 @@ export interface INotebookEditor { */ executeNotebookCell(cell: ICellViewModel): Promise; + /** + * Cancel the cell execution + */ + cancelNotebookCellExecution(cell: ICellViewModel): void; + + /** + * Executes all notebook cells in order + */ + executeNotebook(): Promise; + + /** + * Cancel the notebook execution + */ + cancelNotebookExecution(): void; + /** * Get current active cell */ @@ -243,21 +262,28 @@ export interface INotebookEditor { hideFind(): void; } -export interface CellRenderTemplate { +export interface BaseCellRenderTemplate { container: HTMLElement; cellContainer: HTMLElement; - editorContainer?: HTMLElement; toolbar: ToolBar; - focusIndicator?: HTMLElement; - runToolbar?: ToolBar; - runButtonContainer?: HTMLElement; - executionOrderLabel?: HTMLElement; - editingContainer?: HTMLElement; - outputContainer?: HTMLElement; - editor?: CodeEditorWidget; - progressBar?: ProgressBar; + focusIndicator: HTMLElement; disposables: DisposableStore; - toJSON(): void; + toJSON: () => any; +} + +export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { + editingContainer: HTMLElement; +} + +export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { + editorContainer: HTMLElement; + runToolbar: ToolBar; + runButtonContainer: HTMLElement; + executionOrderLabel: HTMLElement; + outputContainer: HTMLElement; + editor: CodeEditorWidget; + progressBar: ProgressBar; + betweenCellContainer: HTMLElement; } export interface IOutputTransformContribution { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 54b6d6b0e2..25514ff4ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -3,46 +3,46 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Color, RGBA } from 'vs/base/common/color'; +import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./notebook'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { Range } from 'vs/editor/common/core/range'; +import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; +import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { contrastBorder, editorBackground, focusBorder, foreground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions, IEditorMemento, IEditorCloseEvent } from 'vs/workbench/common/editor'; -import { INotebookEditor, NotebookLayoutInfo, CellEditState, NOTEBOOK_EDITOR_FOCUSED, CellFocusMode, ICellViewModel, CellRunState, NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { EditorOptions, IEditorCloseEvent, IEditorMemento } from 'vs/workbench/common/editor'; +import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_TOP_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget'; +import { CellEditState, CellFocusMode, ICellViewModel, INotebookEditor, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorInput, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; -import { IOutput, CellKind, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellKind, CellUri, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditor, ICompositeCodeEditor } from 'vs/editor/common/editorCommon'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; -import { Emitter, Event } from 'vs/base/common/event'; -import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; -import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget'; -import { NotebookViewModel, INotebookEditorViewState, IModelDecorationsChangeAccessor, CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { Range } from 'vs/editor/common/core/range'; -import { CELL_MARGIN, CELL_RUN_GUTTER } from 'vs/workbench/contrib/notebook/browser/constants'; -import { Color, RGBA } from 'vs/base/common/color'; -import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -103,6 +103,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { private dimension: DOM.Dimension | null = null; private editorFocus: IContextKey | null = null; private editorEditable: IContextKey | null = null; + private editorExecutingNotebook: IContextKey | null = null; private outputRenderer: OutputRenderer; private findWidget: NotebookFindWidget; @@ -115,6 +116,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { @IEditorGroupsService editorGroupService: IEditorGroupsService, @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + // @IEditorProgressService private readonly progressService: IEditorProgressService, ) { super(NotebookEditor.ID, telemetryService, themeService, storageService); @@ -148,6 +150,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.createBody(this.rootElement); this.generateFontInfo(); this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); + this.editorFocus.set(true); this._register(this.onDidFocus(() => { this.editorFocus?.set(true); })); @@ -158,6 +161,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); this.editorEditable.set(true); + this.editorExecutingNotebook = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); } private generateFontInfo(): void { @@ -610,12 +614,20 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { async moveCellDown(cell: ICellViewModel): Promise { const index = this.notebookViewModel!.getViewCellIndex(cell); + if (index === this.notebookViewModel!.viewCells.length - 1) { + return; + } + const newIdx = index + 1; return this.moveCellToIndex(index, newIdx); } async moveCellUp(cell: ICellViewModel): Promise { const index = this.notebookViewModel!.getViewCellIndex(cell); + if (index === 0) { + return; + } + const newIdx = index - 1; return this.moveCellToIndex(index, newIdx); } @@ -654,19 +666,74 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return undefined; } - async executeNotebookCell(cell: ICellViewModel): Promise { + cancelNotebookExecution(): void { + if (!this.notebookViewModel!.currentTokenSource) { + throw new Error('Notebook is not executing'); + } + + + this.notebookViewModel!.currentTokenSource.cancel(); + this.notebookViewModel!.currentTokenSource = undefined; + } + + async executeNotebook(): Promise { + // return this.progressService.showWhile(this._executeNotebook()); + return this._executeNotebook(); + } + + async _executeNotebook(): Promise { + if (this.notebookViewModel!.currentTokenSource) { + return; + } + + const tokenSource = new CancellationTokenSource(); try { - cell.runState = CellRunState.Running; + this.editorExecutingNotebook!.set(true); + this.notebookViewModel!.currentTokenSource = tokenSource; + + for (let cell of this.notebookViewModel!.viewCells) { + if (cell.cellKind === CellKind.Code) { + await this._executeNotebookCell(cell, tokenSource); + } + } + } finally { + this.editorExecutingNotebook!.set(false); + this.notebookViewModel!.currentTokenSource = undefined; + tokenSource.dispose(); + } + } + + cancelNotebookCellExecution(cell: ICellViewModel): void { + if (!cell.currentTokenSource) { + throw new Error('Cell is not executing'); + } + + cell.currentTokenSource.cancel(); + cell.currentTokenSource = undefined; + } + + async executeNotebookCell(cell: ICellViewModel): Promise { + const tokenSource = new CancellationTokenSource(); + try { + this._executeNotebookCell(cell, tokenSource); + } finally { + tokenSource.dispose(); + } + } + + async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { + try { + cell.currentTokenSource = tokenSource; const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; if (provider) { const viewType = provider.id; const notebookUri = CellUri.parse(cell.uri)?.notebook; if (notebookUri) { - return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle); + return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, tokenSource.token); } } } finally { - cell.runState = CellRunState.Idle; + cell.currentTokenSource = undefined; } } @@ -778,6 +845,12 @@ export const notebookOutputContainerColor = registerColor('notebook.outputContai } , nls.localize('notebook.outputContainerBackgroundColor', "The Color of the notebook output container background.")); +export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeperator', { + dark: Color.fromHex('#808080').transparent(0.35), + light: Color.fromHex('#808080').transparent(0.35), + hc: contrastBorder +}, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); + registerThemingParticipant((theme, collector) => { const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); @@ -824,9 +897,16 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.selected .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); } + const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); + if (cellToolbarSeperator) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container .seperator { background-color: ${cellToolbarSeperator} }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container .seperator-short { background-color: ${cellToolbarSeperator} }`); + } + // Cell Margin - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { margin: 8px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { margin: ${EDITOR_TOP_MARGIN}px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-container { width: calc(100% - ${CELL_RUN_GUTTER}px); }`); collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .markdown-editor-container { margin-left: ${CELL_RUN_GUTTER}px; }`); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts index 39606fc8bf..ef4d4e32d0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -15,6 +15,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { Iterable } from 'vs/base/common/iterator'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -24,9 +25,9 @@ export const INotebookService = createDecorator('notebookServi export interface IMainNotebookController { resolveNotebook(viewType: string, uri: URI): Promise; - executeNotebook(viewType: string, uri: URI): Promise; + executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise; onDidReceiveMessage(uri: URI, message: any): void; - executeNotebookCell(uri: URI, handle: number): Promise; + executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise; destoryNotebookDocument(notebook: INotebookTextModel): Promise; save(uri: URI): Promise; } @@ -42,7 +43,7 @@ export interface INotebookService { getRendererInfo(handle: number): INotebookRendererInfo | undefined; resolveNotebook(viewType: string, uri: URI): Promise; executeNotebook(viewType: string, uri: URI): Promise; - executeNotebookCell(viewType: string, uri: URI, handle: number): Promise; + executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise; getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; getNotebookProviderResourceRoots(): URI[]; @@ -246,16 +247,16 @@ export class NotebookService extends Disposable implements INotebookService { let provider = this._notebookProviders.get(viewType); if (provider) { - return provider.controller.executeNotebook(viewType, uri); + return provider.controller.executeNotebook(viewType, uri, new CancellationTokenSource().token); // Cancellation for notebooks - TODO } return; } - async executeNotebookCell(viewType: string, uri: URI, handle: number): Promise { + async executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise { const provider = this._notebookProviders.get(viewType); if (provider) { - await provider.controller.executeNotebookCell(uri, handle); + await provider.controller.executeNotebookCell(uri, handle, token); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index a1ecd52860..6f6193f6ca 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -5,7 +5,6 @@ import * as DOM from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; -import * as path from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -18,6 +17,7 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { CELL_MARGIN, CELL_RUN_GUTTER } from 'vs/workbench/contrib/notebook/browser/constants'; import { Emitter, Event } from 'vs/base/common/event'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; export interface IDimentionMessage { __vscode_notebook_message: boolean; @@ -103,7 +103,8 @@ export class BackLayerWebView extends Disposable { this.element.style.position = 'absolute'; this.element.style.margin = `0px 0 0px ${CELL_MARGIN}px`; - const loader = URI.file(path.join(environmentSerice.appRoot, '/out/vs/loader.js')).with({ scheme: WebviewResourceScheme }); + const pathsPath = getPathFromAmdModule(require, 'vs/loader.js'); + const loader = URI.file(pathsPath).with({ scheme: WebviewResourceScheme }); const outputNodePadding = 8; let content = /* html */` @@ -174,7 +175,7 @@ export class BackLayerWebView extends Disposable { type: 'dimension', id: id, data: { - height: entry.contentRect.height + ${outputNodePadding} + height: entry.contentRect.height + ${outputNodePadding} * 2 } }); } 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 1f72fe5239..25eb530ed9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -8,7 +8,8 @@ import * as DOM from 'vs/base/browser/dom'; 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'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, ActionRunner } from 'vs/base/common/actions'; +import { escape } from 'vs/base/common/strings'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { deepClone } from 'vs/base/common/objects'; import 'vs/css!vs/workbench/contrib/notebook/browser/notebook'; @@ -19,14 +20,14 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY, NOTEBOOK_CELL_TYPE_CONTEXT_KEY, EDITOR_TOP_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; -import { ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/notebookActions'; -import { CellEditState, CellRenderTemplate, CellRunState, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE_CONTEXT_KEY, NOTEBOOK_CELL_TYPE_CONTEXT_KEY, NOTEBOOK_CELL_RUN_STATE_CONTEXT_KEY, NOTEBOOK_VIEW_TYPE, BOTTOM_CELL_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import { ExecuteCellAction, INotebookCellActionContext, CancelCellAction, InsertCodeCellAction, InsertMarkdownCellAction } from 'vs/workbench/contrib/notebook/browser/contrib/notebookActions'; +import { BaseCellRenderTemplate, CellEditState, CellRunState, CodeCellRenderTemplate, ICellViewModel, INotebookEditor, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; @@ -34,12 +35,12 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { renderCodicons } from 'vs/base/common/codicons'; const $ = DOM.$; export class NotebookCellListDelegate implements IListVirtualDelegate { private _lineHeight: number; - private _toolbarHeight = EDITOR_TOOLBAR_HEIGHT; constructor( @IConfigurationService private readonly configurationService: IConfigurationService @@ -49,7 +50,7 @@ export class NotebookCellListDelegate implements IListVirtualDelegate { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; + } + + return undefined; + } + }); + + toolbar.getContainer().style.height = `${BOTTOM_CELL_TOOLBAR_HEIGHT}px`; + return toolbar; + } + protected createToolbar(container: HTMLElement): ToolBar { const toolbar = new ToolBar(container, this.contextMenuService, { actionViewItemProvider: action => { @@ -131,7 +164,7 @@ abstract class AbstractCellRenderer { return actions; } - protected setupCellToolbarActions(scopedContextKeyService: IContextKeyService, templateData: CellRenderTemplate, disposables: DisposableStore): void { + protected setupCellToolbarActions(scopedContextKeyService: IContextKeyService, templateData: BaseCellRenderTemplate, disposables: DisposableStore): void { const cellMenu = this.instantiationService.createInstance(CellMenus); const menu = disposables.add(cellMenu.getCellTitleMenu(scopedContextKeyService)); @@ -156,7 +189,7 @@ abstract class AbstractCellRenderer { } } -export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer { +export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'markdown_cell'; private disposables: Map = new Map(); @@ -176,11 +209,11 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR return MarkdownCellRenderer.TEMPLATE_ID; } - renderTemplate(container: HTMLElement): CellRenderTemplate { + renderTemplate(container: HTMLElement): MarkdownCellRenderTemplate { const codeInnerContent = document.createElement('div'); DOM.addClasses(codeInnerContent, 'cell', 'code'); - const editorContainer = DOM.append(codeInnerContent, $('.markdown-editor-container')); - editorContainer.style.display = 'none'; + const editingContainer = DOM.append(codeInnerContent, $('.markdown-editor-container')); + editingContainer.style.display = 'none'; const disposables = new DisposableStore(); const toolbar = this.createToolbar(container); @@ -195,9 +228,9 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const focusIndicator = DOM.append(container, DOM.$('.notebook-cell-focus-indicator')); return { - container: container, + container, cellContainer: innerContent, - editingContainer: editorContainer, + editingContainer, focusIndicator, disposables, toolbar, @@ -205,7 +238,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR }; } - renderElement(element: MarkdownCellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + renderElement(element: MarkdownCellViewModel, index: number, templateData: MarkdownCellRenderTemplate, height: number | undefined): void { templateData.editingContainer!.style.display = 'none'; templateData.cellContainer.innerHTML = ''; let renderedHTML = element.getHTML(); @@ -224,6 +257,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const contextKeyService = this.contextKeyService.createScoped(templateData.container); contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'markdown'); + contextKeyService.createKey(NOTEBOOK_VIEW_TYPE, element.viewType); const cellEditableKey = contextKeyService.createKey(NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, !!(element.metadata?.editable)); elementDisposable.add(element.onDidChangeMetadata(() => { cellEditableKey.set(!!element.metadata?.editable); @@ -237,28 +271,29 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR this.setupCellToolbarActions(contextKeyService, templateData, elementDisposable); } - templateData.toolbar!.context = { + templateData.toolbar.context = { cell: element, notebookEditor: this.notebookEditor, $mid: 12 }; } - disposeTemplate(templateData: CellRenderTemplate): void { - // throw nerendererw Error('Method not implemented.'); - + disposeTemplate(templateData: MarkdownCellRenderTemplate): void { + templateData.disposables.clear(); } - disposeElement(element: ICellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + disposeElement(element: ICellViewModel, index: number, templateData: MarkdownCellRenderTemplate, height: number | undefined): void { if (height) { this.disposables.get(element)?.clear(); } } } -export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { +export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'code_cell'; private disposables: Map = new Map(); + private actionRunner = new ActionRunner(); + constructor( protected notebookEditor: INotebookEditor, @@ -277,7 +312,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende return CodeCellRenderer.TEMPLATE_ID; } - renderTemplate(container: HTMLElement): CellRenderTemplate { + renderTemplate(container: HTMLElement): CodeCellRenderTemplate { const disposables = new DisposableStore(); const toolbar = this.createToolbar(container); disposables.add(toolbar); @@ -285,11 +320,10 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const cellContainer = DOM.append(container, $('.cell.code')); const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); const runToolbar = this.createToolbar(runButtonContainer); - runToolbar.setActions([ - this.instantiationService.createInstance(ExecuteCellAction) - ])(); disposables.add(runToolbar); + const betweenCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); + const executionOrderLabel = DOM.append(runButtonContainer, $('div.execution-count-label')); const editorContainer = DOM.append(cellContainer, $('.cell-editor-container')); @@ -324,18 +358,67 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende outputContainer, editor, disposables, + betweenCellContainer: betweenCellContainer, toJSON: () => { return {}; } }; } - renderElement(element: CodeCellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + protected setupBetweenCellToolbarActions(element: CodeCellViewModel, templateData: CodeCellRenderTemplate, disposables: DisposableStore, context: INotebookCellActionContext): void { + const container = templateData.betweenCellContainer; + container.innerHTML = ''; + container.style.height = `${BOTTOM_CELL_TOOLBAR_HEIGHT}px`; + + DOM.append(container, $('.seperator')); + const addCodeCell = DOM.append(container, $('span.button')); + addCodeCell.innerHTML = renderCodicons(escape(`$(add) Code `)); + const insertCellBelow = this.instantiationService.createInstance(InsertCodeCellAction); + + disposables.add(DOM.addDisposableListener(addCodeCell, DOM.EventType.CLICK, () => { + this.actionRunner.run(insertCellBelow, context); + })); + + DOM.append(container, $('.seperator-short')); + const addMarkdownCell = DOM.append(container, $('span.button')); + addMarkdownCell.innerHTML = renderCodicons(escape('$(add) Markdown ')); + const insertMarkdownBelow = this.instantiationService.createInstance(InsertMarkdownCellAction); + disposables.add(DOM.addDisposableListener(addMarkdownCell, DOM.EventType.CLICK, () => { + this.actionRunner.run(insertMarkdownBelow, context); + })); + + DOM.append(container, $('.seperator')); + + const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; + container.style.top = `${bottomToolbarOffset}px`; + + disposables.add(element.onDidChangeLayout(() => { + const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; + container.style.top = `${bottomToolbarOffset}px`; + })); + } + + private updateForRunState(element: CodeCellViewModel, templateData: CodeCellRenderTemplate, runStateKey: IContextKey): void { + runStateKey.set(CellRunState[element.runState]); + if (element.runState === CellRunState.Running) { + templateData.progressBar.infinite().show(500); + + templateData.runToolbar.setActions([ + this.instantiationService.createInstance(CancelCellAction) + ])(); + } else { + templateData.progressBar.hide(); + + templateData.runToolbar.setActions([ + this.instantiationService.createInstance(ExecuteCellAction) + ])(); + } + } + + renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { if (height === undefined) { return; } - if (templateData.outputContainer) { - templateData.outputContainer!.innerHTML = ''; - } + templateData.outputContainer.innerHTML = ''; this.disposables.get(element)?.clear(); if (!this.disposables.has(element)) { @@ -348,38 +431,37 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.renderedEditors.set(element, templateData.editor); elementDisposable.add(element.onDidChangeLayout(() => { - templateData.focusIndicator!.style.height = `${element.layoutInfo.indicatorHeight}px`; + templateData.focusIndicator.style.height = `${element.layoutInfo.indicatorHeight}px`; })); - elementDisposable.add(element.onDidChangeCellRunState(() => { - if (element.runState === CellRunState.Running) { - templateData.progressBar?.infinite().show(500); - } else { - templateData.progressBar?.hide(); - } - })); - - function renderExecutionOrder() { - const executionOrdeerLabel = typeof element.metadata?.executionOrder === 'number' ? `[ ${element.metadata.executionOrder} ]` : - '[ ]'; - templateData.executionOrderLabel!.innerText = executionOrdeerLabel; - } - const contextKeyService = this.contextKeyService.createScoped(templateData.container); - contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'code'); - const cellEditableKey = contextKeyService.createKey(NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, !!(element.metadata?.editable)); + const runStateKey = contextKeyService.createKey(NOTEBOOK_CELL_RUN_STATE_CONTEXT_KEY, CellRunState[element.runState]); + this.updateForRunState(element, templateData, runStateKey); + elementDisposable.add(element.onDidChangeCellRunState(() => this.updateForRunState(element, templateData, runStateKey))); + + const renderExecutionOrder = () => { + const hasExecutionOrder = this.notebookEditor.viewModel!.notebookDocument.metadata?.hasExecutionOrder; + if (hasExecutionOrder) { + const executionOrdeerLabel = typeof element.metadata?.executionOrder === 'number' ? `[ ${element.metadata.executionOrder} ]` : + '[ ]'; + templateData.executionOrderLabel.innerText = executionOrdeerLabel; + } else { + templateData.executionOrderLabel.innerText = ''; + } + }; + + contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'code'); + contextKeyService.createKey(NOTEBOOK_VIEW_TYPE, element.viewType); + const cellEditableKey = contextKeyService.createKey(NOTEBOOK_CELL_EDITABLE_CONTEXT_KEY, !!(element.metadata?.editable)); const updateForMetadata = () => { const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel!.notebookDocument.metadata); DOM.toggleClass(templateData.cellContainer, 'runnable', !!metadata.runnable); renderExecutionOrder(); cellEditableKey.set(!!metadata.editable); }; - updateForMetadata(); - elementDisposable.add(element.onDidChangeMetadata(() => { - updateForMetadata(); - })); + elementDisposable.add(element.onDidChangeMetadata(() => updateForMetadata())); this.setupCellToolbarActions(contextKeyService, templateData, elementDisposable); @@ -390,16 +472,18 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende $mid: 12 }; templateData.toolbar.context = toolbarContext; - templateData.runToolbar!.context = toolbarContext; + templateData.runToolbar.context = toolbarContext; + + this.setupBetweenCellToolbarActions(element, templateData, elementDisposable, toolbarContext); } - disposeTemplate(templateData: CellRenderTemplate): void { + disposeTemplate(templateData: CodeCellRenderTemplate): void { templateData.disposables.clear(); } - disposeElement(element: ICellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + disposeElement(element: ICellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { this.disposables.get(element)?.clear(); this.renderedEditors.delete(element); - templateData.focusIndicator!.style.height = 'initial'; + templateData.focusIndicator.style.height = 'initial'; } } 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 814dfc1a54..ab7a46ae2b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.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 * as DOM from 'vs/base/browser/dom'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; -import { IOutput, ITransformedDisplayOutputDto, IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CellRenderTemplate, INotebookEditor, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { debounce } from 'vs/base/common/decorators'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import * as nls from 'vs/nls'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellFocusMode, CodeCellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { CellOutputKind, IOutput, IRenderOutput, ITransformedDisplayOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -28,7 +28,7 @@ export class CodeCell extends Disposable { constructor( private notebookEditor: INotebookEditor, private viewCell: CodeCellViewModel, - private templateData: CellRenderTemplate, + private templateData: CodeCellRenderTemplate, @INotebookService private notebookService: INotebookService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IModeService private readonly _modeService: IModeService @@ -93,24 +93,26 @@ export class CodeCell extends Disposable { templateData.editor?.getModel()?.setMode(mode.languageIdentifier); })); - let cellWidthResizeObserver = getResizesObserver(templateData.editorContainer!, { - width: width, - height: totalHeight - }, () => { - let newWidth = cellWidthResizeObserver.getWidth(); - let realContentHeight = templateData.editor!.getContentHeight(); - templateData.editor?.layout( - { - width: newWidth, - height: realContentHeight - } - ); + this._register(viewCell.onDidChangeLayout((e) => { + if (e.outerWidth === undefined) { + return; + } - viewCell.editorHeight = realContentHeight; - }); + const layoutInfo = templateData.editor!.getLayoutInfo(); + const realContentHeight = templateData.editor!.getContentHeight(); - cellWidthResizeObserver.startObserving(); - this._register(cellWidthResizeObserver); + if (layoutInfo.width !== viewCell.layoutInfo.editorWidth) { + templateData.editor?.layout( + { + width: viewCell.layoutInfo.editorWidth, + height: realContentHeight + } + ); + + viewCell.editorHeight = realContentHeight; + this.relayoutCell(); + } + })); this._register(templateData.editor!.onDidContentSizeChange((e) => { if (e.contentHeightChanged) { @@ -149,6 +151,8 @@ export class CodeCell extends Disposable { return; } + const previousOutputHeight = this.viewCell.layoutInfo.outputTotalHeight; + if (this.viewCell.outputs.length) { this.templateData.outputContainer!.style.display = 'block'; } else { @@ -197,7 +201,13 @@ export class CodeCell extends Disposable { let editorHeight = templateData.editor!.getContentHeight(); viewCell.editorHeight = editorHeight; - this.relayoutCellDebounced(); + + if (previousOutputHeight === 0 || this.viewCell.outputs.length === 0) { + // first execution or removing all outputs + this.relayoutCell(); + } else { + this.relayoutCellDebounced(); + } })); if (viewCell.outputs.length > 0) { 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 8a4cd03f97..976b79ef0f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -8,7 +8,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; -import { INotebookEditor, CellRenderTemplate, CellFocusMode, CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, MarkdownCellRenderTemplate, CellFocusMode, CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { raceCancellation } from 'vs/base/common/async'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; @@ -26,7 +26,7 @@ export class StatefullMarkdownCell extends Disposable { constructor( private notebookEditor: INotebookEditor, private viewCell: MarkdownCellViewModel, - private templateData: CellRenderTemplate, + private templateData: MarkdownCellRenderTemplate, editorOptions: IEditorOptions, instantiationService: IInstantiationService ) { @@ -47,7 +47,7 @@ export class StatefullMarkdownCell extends Disposable { if (this.editor) { // not first time, we don't need to create editor or bind listeners - this.editingContainer!.style.display = 'block'; + this.editingContainer!.style.display = 'flex'; viewCell.attachTextEditor(this.editor!); if (notebookEditor.getActiveCell() === viewCell) { this.editor!.focus(); @@ -55,7 +55,7 @@ export class StatefullMarkdownCell extends Disposable { this.bindEditorListeners(this.editor!.getModel()!); } else { - this.editingContainer!.style.display = 'block'; + this.editingContainer!.style.display = 'flex'; this.editingContainer!.innerHTML = ''; this.editor = instantiationService.createInstance(CodeEditorWidget, this.editingContainer!, { ...editorOptions, diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 1d05fe0d0d..49d62635eb 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -3,23 +3,24 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; -import { Range } from 'vs/editor/common/core/range'; -import { ICell, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { CursorAtBoundary, CellFocusMode, CellEditState, CellRunState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; +import { EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFocusMode, CellRunState, CursorAtBoundary, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind, ICell, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export const NotebookCellMetadataDefaults = { editable: true, runnable: true }; -export abstract class BaseCellViewModel extends Disposable { +export abstract class BaseCellViewModel extends Disposable implements ICellViewModel { protected readonly _onDidDispose = new Emitter(); readonly onDidDispose = this._onDidDispose.event; protected readonly _onDidChangeCellEditState = new Emitter(); @@ -49,6 +50,8 @@ export abstract class BaseCellViewModel extends Disposable { return this.cell.metadata; } + abstract cellKind: CellKind; + private _editState: CellEditState = CellEditState.Preview; get editState(): CellEditState { @@ -63,20 +66,21 @@ export abstract class BaseCellViewModel extends Disposable { this._editState = newState; this._onDidChangeCellEditState.fire(); } - private _runState: CellRunState = CellRunState.Idle; - get runState(): CellRunState { - return this._runState; - } - - set runState(newState: CellRunState) { - if (newState === this._runState) { - return; - } - - this._runState = newState; + private _currentTokenSource: CancellationTokenSource | undefined; + public set currentTokenSource(v: CancellationTokenSource | undefined) { + this._currentTokenSource = v; this._onDidChangeCellRunState.fire(); } + + public get currentTokenSource(): CancellationTokenSource | undefined { + return this._currentTokenSource; + } + + get runState(): CellRunState { + return this._currentTokenSource ? CellRunState.Running : CellRunState.Idle; + } + private _focusMode: CellFocusMode = CellFocusMode.Container; get focusMode() { return this._focusMode; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index ab89c486b5..d1c0b60d9b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -8,12 +8,11 @@ import * as UUID from 'vs/base/common/uuid'; import * as model from 'vs/editor/common/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_TOP_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_TOP_MARGIN, BOTTOM_CELL_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellEditState, ICellViewModel, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, ICell, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { BaseCellViewModel } from './baseCellViewModel'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { debounce } from 'vs/base/common/decorators'; export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel { cellKind: CellKind.Code = CellKind.Code; @@ -37,7 +36,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); public readonly onDidChangeContent: Event = this._onDidChangeContent.event; - protected readonly _onDidChangeLayout = new Emitter(); + protected readonly _onDidChangeLayout = new Emitter(); readonly onDidChangeLayout = this._onDidChangeLayout.event; private _editorHeight = 0; @@ -84,11 +83,12 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this._layoutInfo = { fontInfo: initialNotebookLayoutInfo?.fontInfo || null, editorHeight: 0, - editorWidth: 0, + editorWidth: initialNotebookLayoutInfo ? initialNotebookLayoutInfo!.width - CELL_MARGIN * 2 - CELL_RUN_GUTTER : 0, outputContainerOffset: 0, outputTotalHeight: 0, totalHeight: 0, - indicatorHeight: 0 + indicatorHeight: 0, + bottomToolbarOffset: 0 }; this._register(eventDispatcher.onDidChangeLayout((e) => { @@ -108,10 +108,11 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod // recompute this._ensureOutputsTop(); const outputTotalHeight = this._outputsTop!.getTotalValue(); - const totalHeight = EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_MARGIN + 16 + outputTotalHeight; + const totalHeight = EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_MARGIN + outputTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT; const indicatorHeight = this.editorHeight + outputTotalHeight; - const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + this.editorHeight; - const editorWidth = state.outerWidth !== undefined ? state.outerWidth - CELL_MARGIN * 2 - CELL_RUN_GUTTER : 0; + const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + this.editorHeight; + const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT; + const editorWidth = state.outerWidth !== undefined ? state.outerWidth - CELL_MARGIN * 2 - CELL_RUN_GUTTER : this._layoutInfo?.editorWidth; this._layoutInfo = { fontInfo: state.font || null, editorHeight: this._editorHeight, @@ -119,19 +120,19 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod outputContainerOffset, outputTotalHeight, totalHeight, - indicatorHeight + indicatorHeight, + bottomToolbarOffset: bottomToolbarOffset }; if (state.editorHeight || state.outputHeight) { state.totalHeight = true; } - this._fireOnDidChangeLayout(); + this._fireOnDidChangeLayout(state); } - @debounce(100) - private _fireOnDidChangeLayout() { - this._onDidChangeLayout.fire(); + private _fireOnDidChangeLayout(state: CodeCellLayoutChangeEvent) { + this._onDidChangeLayout.fire(state); } hasDynamicHeight() { @@ -152,7 +153,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } getHeight(lineHeight: number) { - return this.lineCount * lineHeight + 16 + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + return EDITOR_TOOLBAR_HEIGHT + EDITOR_TOP_MARGIN + this.lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + BOTTOM_CELL_TOOLBAR_HEIGHT; } save() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 499059130d..61491846e6 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -21,6 +21,7 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellKind, ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; @@ -60,6 +61,16 @@ export class NotebookViewModel extends Disposable { private _localStore: DisposableStore = this._register(new DisposableStore()); private _viewCells: CellViewModel[] = []; + private _currentTokenSource: CancellationTokenSource | undefined; + + get currentTokenSource(): CancellationTokenSource | undefined { + return this._currentTokenSource; + } + + set currentTokenSource(v: CancellationTokenSource | undefined) { + this._currentTokenSource = v; + } + get viewCells(): ICellViewModel[] { return this._viewCells; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index a3f1f95b53..39a7423dc5 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookTextModel extends Disposable implements INotebookTextModel { private static _cellhandlePool: number = 0; @@ -26,7 +26,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel private _cellListeners: Map = new Map(); cells: NotebookCellTextModel[]; languages: string[] = []; - metadata: NotebookDocumentMetadata | undefined = { editable: true }; + metadata: NotebookDocumentMetadata = notebookDocumentMetadataDefaults; renderers = new Set(); private _isUntitled: boolean | undefined = undefined; private _versionId = 0; @@ -116,8 +116,9 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._isUntitled = true; this.cells = [cell]; + this._mapping.set(cell.handle, cell); - let dirtyStateListener = cell.onDidChangeContent(() => { + let dirtyStateListener = Event.any(cell.onDidChangeContent, cell.onDidChangeOutputs)(() => { this._isUntitled = false; this._onDidChangeContent.fire(); }); @@ -151,7 +152,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel for (let i = 0; i < cells.length; i++) { this._mapping.set(cells[i].handle, cells[i]); - let dirtyStateListener = cells[i].onDidChangeContent(() => { + let dirtyStateListener = Event.any(cells[i].onDidChangeContent, cells[i].onDidChangeOutputs)(() => { this._onDidChangeContent.fire(); }); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 553a825cf0..45e6ecbe1c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -36,10 +36,18 @@ export const NOTEBOOK_DISPLAY_ORDER = [ 'text/plain' ]; +export const notebookDocumentMetadataDefaults: NotebookDocumentMetadata = { + editable: true, + cellEditable: true, + cellRunnable: true, + hasExecutionOrder: true +}; + export interface NotebookDocumentMetadata { editable: boolean; - cellEditable?: boolean; - cellRunnable?: boolean; + cellEditable: boolean; + cellRunnable: boolean; + hasExecutionOrder: boolean; } export interface NotebookCellMetadata { diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 93dd50ac4e..a6751de9ca 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -97,7 +97,7 @@ suite('NotebookViewModel', () => { [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { - viewModel.notebookDocument.metadata = { editable: true, cellRunnable: true, cellEditable: true }; + viewModel.notebookDocument.metadata = { editable: true, cellRunnable: true, cellEditable: true, hasExecutionOrder: true }; assert.deepEqual(viewModel.viewCells[0].getEvaluatedMetadata(viewModel.metadata), { editable: true, @@ -124,7 +124,7 @@ suite('NotebookViewModel', () => { runnable: false }); - viewModel.notebookDocument.metadata = { editable: true, cellRunnable: false, cellEditable: true }; + viewModel.notebookDocument.metadata = { editable: true, cellRunnable: false, cellEditable: true, hasExecutionOrder: true }; assert.deepEqual(viewModel.viewCells[0].getEvaluatedMetadata(viewModel.metadata), { editable: true, @@ -151,7 +151,7 @@ suite('NotebookViewModel', () => { runnable: false }); - viewModel.notebookDocument.metadata = { editable: true, cellRunnable: false, cellEditable: false }; + viewModel.notebookDocument.metadata = { editable: true, cellRunnable: false, cellEditable: false, hasExecutionOrder: true }; assert.deepEqual(viewModel.viewCells[0].getEvaluatedMetadata(viewModel.metadata), { editable: false, diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 071f491f7d..bf25b9a820 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -81,6 +81,18 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + cancelNotebookCellExecution(cell: ICellViewModel): void { + throw new Error('Method not implemented.'); + } + + executeNotebook(): Promise { + throw new Error('Method not implemented.'); + } + + cancelNotebookExecution(): void { + throw new Error('Method not implemented.'); + } + executeNotebookCell(cell: ICellViewModel): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index ddb24a09b6..64e3154deb 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -51,6 +51,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; class RequestState { @@ -176,6 +177,13 @@ class OutlineViewState { private readonly _onDidChange = new Emitter<{ followCursor?: boolean, sortBy?: boolean, filterOnType?: boolean }>(); readonly onDidChange = this._onDidChange.event; + constructor( + @IStorageService private readonly _storageService: IStorageService, + @IStorageKeysSyncRegistryService storageKeysSyncService: IStorageKeysSyncRegistryService + ) { + storageKeysSyncService.registerStorageKey({ key: 'outline/state', version: 1 }); + } + set followCursor(value: boolean) { if (value !== this._followCursor) { this._followCursor = value; @@ -209,16 +217,16 @@ class OutlineViewState { return this._sortBy; } - persist(storageService: IStorageService): void { - storageService.store('outline/state', JSON.stringify({ + persist(): void { + this._storageService.store('outline/state', JSON.stringify({ followCursor: this.followCursor, sortBy: this.sortBy, filterOnType: this.filterOnType, }), StorageScope.WORKSPACE); } - restore(storageService: IStorageService): void { - let raw = storageService.get('outline/state', StorageScope.WORKSPACE); + restore(): void { + let raw = this._storageService.get('outline/state', StorageScope.WORKSPACE); if (!raw) { return; } @@ -241,7 +249,7 @@ export class OutlinePane extends ViewPane { private _disposables = new Array(); private _editorDisposables = new DisposableStore(); - private _outlineViewState = new OutlineViewState(); + private _outlineViewState: OutlineViewState; private _requestOracle?: RequestOracle; private _domNode!: HTMLElement; private _message!: HTMLDivElement; @@ -262,7 +270,6 @@ export class OutlinePane extends ViewPane { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IThemeService private readonly _themeService: IThemeService, - @IStorageService private readonly _storageService: IStorageService, @ICodeEditorService private readonly _editorService: ICodeEditorService, @IMarkerDecorationsService private readonly _markerDecorationService: IMarkerDecorationsService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -274,7 +281,7 @@ export class OutlinePane extends ViewPane { @ITelemetryService telemetryService: ITelemetryService, ) { super(options, keybindingService, contextMenuService, _configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); - this._outlineViewState.restore(this._storageService); + this._outlineViewState = this.instantiationService.createInstance(OutlineViewState); this._contextKeyFocused = OutlineViewFocused.bindTo(contextKeyService); this._contextKeyFiltered = OutlineViewFiltered.bindTo(contextKeyService); this._disposables.push(this.onDidFocus(_ => this._contextKeyFocused.set(true))); @@ -434,7 +441,7 @@ export class OutlinePane extends ViewPane { } private _onDidChangeUserState(e: { followCursor?: boolean, sortBy?: boolean, filterOnType?: boolean }) { - this._outlineViewState.persist(this._storageService); + this._outlineViewState.persist(); if (e.followCursor) { // todo@joh update immediately } diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index 627a770189..cc9b7a0704 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -219,12 +219,10 @@ export class QuickAccessViewPickerAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const keys = this.keybindingService.lookupKeybindings(this.id); this.quickInputService.quickAccess.show(ViewQuickAccessProvider.PREFIX, { quickNavigateConfiguration: { keybindings: keys } }); - - return Promise.resolve(true); } } diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 836d04d81b..ef8ac90114 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -402,7 +402,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider this.createAnythingPick(editor, configuration)); } @@ -448,9 +448,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY)); + private readonly fileQueryDelayer = this._register(new ThrottledDelayer(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY)); - private fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); + private readonly fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); private createFileQueryCache(): FileQueryCacheState { return new FileQueryCacheState( @@ -462,7 +462,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken): Promise> { - if (!query.value) { + if (!query.normalized) { return []; } @@ -692,9 +692,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> { const configuration = this.configuration; if ( - !query.value || // we need a value for search for - !configuration.includeSymbols || // we need to enable symbols in search - this.pickState.lastRange // a range is an indicator for just searching for files + !query.normalized || // we need a value for search for + !configuration.includeSymbols || // we need to enable symbols in search + this.pickState.lastRange // a range is an indicator for just searching for files ) { return []; } diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 112acd0713..a0910e0223 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -258,7 +258,7 @@ } /* Adjusts spacing in high contrast mode so that actions are vertically centered */ -.hc-black .monaco-list .monaco-list-row .monaco-action-bar .action-label { +.hc-black .search-view .monaco-list .monaco-list-row .monaco-action-bar .action-label { margin-top: 2px; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index f378991795..733ad8c669 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -101,7 +101,6 @@ export class SearchEditorInput extends EditorInput { this.contentsModel = modelLoader.then(({ contentsModel }) => contentsModel); this.headerModel = modelLoader.then(({ headerModel }) => headerModel); - const input = this; const workingCopyAdapter = new class implements IWorkingCopy { readonly resource = input.resource; @@ -115,7 +114,7 @@ export class SearchEditorInput extends EditorInput { revert(options?: IRevertOptions): Promise { return input.revert(0, options); } }; - this.workingCopyService.registerWorkingCopy(workingCopyAdapter); + this._register(this.workingCopyService.registerWorkingCopy(workingCopyAdapter)); } async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 42f683bbab..19c6efbdac 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -1240,7 +1240,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private getResourceForTask(task: CustomTask | ContributedTask): URI { + private getResourceForTask(task: CustomTask | ConfiguringTask | ContributedTask): URI { if (CustomTask.is(task)) { let uri = this.getResourceForKind(task._source.kind); if (!uri) { @@ -1257,7 +1257,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - public openConfig(task: CustomTask | undefined): Promise { + public openConfig(task: CustomTask | ConfiguringTask | undefined): Promise { let resource: URI | undefined; if (task) { resource = this.getResourceForTask(task); @@ -2596,6 +2596,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return result; } + private configHasTasks(taskConfig?: TaskConfig.ExternalTaskRunnerConfiguration): boolean { + return !!taskConfig && !!taskConfig.tasks && taskConfig.tasks.length > 0; + } + private openTaskFile(resource: URI, taskSource: string) { let configFileCreated = false; this.fileService.resolve(resource).then((stat) => stat, () => undefined).then(async (stat) => { @@ -2604,9 +2608,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer let tasksExistInFile: boolean; let target: ConfigurationTarget; switch (taskSource) { - case TaskSourceKind.User: tasksExistInFile = !!configValue.userValue; target = ConfigurationTarget.USER; break; - case TaskSourceKind.WorkspaceFile: tasksExistInFile = !!configValue.workspaceValue; target = ConfigurationTarget.WORKSPACE; break; - default: tasksExistInFile = !!configValue.value; target = ConfigurationTarget.WORKSPACE_FOLDER; + case TaskSourceKind.User: tasksExistInFile = this.configHasTasks(configValue.userValue); target = ConfigurationTarget.USER; break; + case TaskSourceKind.WorkspaceFile: tasksExistInFile = this.configHasTasks(configValue.workspaceValue); target = ConfigurationTarget.WORKSPACE; break; + default: tasksExistInFile = this.configHasTasks(configValue.value); target = ConfigurationTarget.WORKSPACE_FOLDER; } let content; if (!tasksExistInFile) { diff --git a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts index 60fc63e36c..d566d1fa8d 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts @@ -62,11 +62,7 @@ export class TaskQuickPick extends Disposable { } private createTaskEntry(task: Task | ConfiguringTask): TaskTwoLevelQuickPickEntry { - let entryLabel = this.guessTaskLabel(task); - if (!ConfiguringTask.is(task) && task.instance) { - entryLabel += + ' (' + task.instance + ')'; - } - const entry: TaskTwoLevelQuickPickEntry = { label: entryLabel, description: this.taskService.getTaskDescription(task), task, detail: this.showDetail() ? task.configurationProperties.detail : undefined }; + const entry: TaskTwoLevelQuickPickEntry = { label: this.guessTaskLabel(task), description: this.taskService.getTaskDescription(task), task, detail: this.showDetail() ? task.configurationProperties.detail : undefined }; entry.buttons = [{ iconClass: 'codicon-gear', tooltip: nls.localize('configureTask', "Configure Task") }]; return entry; } @@ -101,36 +97,50 @@ export class TaskQuickPick extends Disposable { return tasks; } - private dedupeConfiguredAndRecent(recentTasks: (Task | ConfiguringTask)[], configuredTasks: (Task | ConfiguringTask)[]): (Task | ConfiguringTask)[] { + private dedupeConfiguredAndRecent(recentTasks: (Task | ConfiguringTask)[], configuredTasks: (Task | ConfiguringTask)[]): { configuredTasks: (Task | ConfiguringTask)[], recentTasks: (Task | ConfiguringTask)[] } { let dedupedConfiguredTasks: (Task | ConfiguringTask)[] = []; + const foundRecentTasks: boolean[] = Array(recentTasks.length).fill(false); for (let j = 0; j < configuredTasks.length; j++) { const workspaceFolder = configuredTasks[j].getWorkspaceFolder()?.uri.toString(); const definition = configuredTasks[j].getDefinition()?._key; const recentKey = configuredTasks[j].getRecentlyUsedKey(); - if (!recentTasks.find((value) => { + const findIndex = recentTasks.findIndex((value) => { return (workspaceFolder && definition && value.getWorkspaceFolder()?.uri.toString() === workspaceFolder && value.getDefinition()?._key === definition) || (recentKey && value.getRecentlyUsedKey() === recentKey); - })) { + }); + if (findIndex === -1) { dedupedConfiguredTasks.push(configuredTasks[j]); + } else { + recentTasks[findIndex] = configuredTasks[j]; + foundRecentTasks[findIndex] = true; } } dedupedConfiguredTasks = dedupedConfiguredTasks.sort((a, b) => this.sorter.compare(a, b)); - return dedupedConfiguredTasks; + const prunedRecentTasks: (Task | ConfiguringTask)[] = []; + for (let i = 0; i < recentTasks.length; i++) { + if (foundRecentTasks[i] || ConfiguringTask.is(recentTasks[i])) { + prunedRecentTasks.push(recentTasks[i]); + } + } + return { configuredTasks: dedupedConfiguredTasks, recentTasks: prunedRecentTasks }; } public async getTopLevelEntries(defaultEntry?: TaskQuickPickEntry): Promise<{ entries: QuickPickInput[], isSingleConfigured?: Task | ConfiguringTask }> { if (this.topLevelEntries !== undefined) { return { entries: this.topLevelEntries }; } - const recentTasks: (Task | ConfiguringTask)[] = (await this.taskService.readRecentTasks()).reverse(); + let recentTasks: (Task | ConfiguringTask)[] = (await this.taskService.readRecentTasks()).reverse(); const configuredTasks: (Task | ConfiguringTask)[] = this.handleFolderTaskResult(await this.taskService.getWorkspaceTasks()); const extensionTaskTypes = this.taskService.taskTypes(); this.topLevelEntries = []; + // Dedupe will update recent tasks if they've changed in tasks.json. + const dedupeAndPrune = this.dedupeConfiguredAndRecent(recentTasks, configuredTasks); + let dedupedConfiguredTasks: (Task | ConfiguringTask)[] = dedupeAndPrune.configuredTasks; + recentTasks = dedupeAndPrune.recentTasks; if (recentTasks.length > 0) { this.createEntriesForGroup(this.topLevelEntries, recentTasks, nls.localize('recentlyUsed', 'recently used')); } if (configuredTasks.length > 0) { - let dedupedConfiguredTasks: (Task | ConfiguringTask)[] = this.dedupeConfiguredAndRecent(recentTasks, configuredTasks); if (dedupedConfiguredTasks.length > 0) { this.createEntriesForGroup(this.topLevelEntries, dedupedConfiguredTasks, nls.localize('configured', 'configured')); } @@ -159,7 +169,7 @@ export class TaskQuickPick extends Disposable { this.quickInputService.cancel(); if (ContributedTask.is(task)) { this.taskService.customize(task, undefined, true); - } else if (CustomTask.is(task)) { + } else if (CustomTask.is(task) || ConfiguringTask.is(task)) { this.taskService.openConfig(task); } }); @@ -190,6 +200,7 @@ export class TaskQuickPick extends Disposable { picker.dispose(); return this.toTask(firstLevelTask); } else { + picker.dispose(); return undefined; // {{SQL CARBON EDIT}} strict-null-checks } } while (1); @@ -230,12 +241,12 @@ export class TaskQuickPick extends Disposable { if (tasks.length > 0) { taskQuickPickEntries = tasks.map(task => this.createTaskEntry(task)); taskQuickPickEntries.unshift({ - label: nls.localize('TaskQuickPick.goBack', 'Go back...'), + label: nls.localize('TaskQuickPick.goBack', 'Go back ↩'), task: null }); } else { taskQuickPickEntries = [{ - label: nls.localize('TaskQuickPick.noTasksForType', 'No {0} tasks found. Go back...', type), + label: nls.localize('TaskQuickPick.noTasksForType', 'No {0} tasks found. Go back ↩', type), task: null }]; } diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index f865095065..eecd26877b 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -85,7 +85,7 @@ export interface ITaskService { getTaskDescription(task: Task | ConfiguringTask): string | undefined; canCustomize(task: ContributedTask | CustomTask): boolean; customize(task: ContributedTask | CustomTask, properties?: {}, openConfig?: boolean): Promise; - openConfig(task: CustomTask | undefined): Promise; + openConfig(task: CustomTask | ConfiguringTask | undefined): Promise; registerTaskProvider(taskProvider: ITaskProvider, type: string): IDisposable; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index a3af40a1b4..2641ae0263 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -21,6 +21,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IProductService } from 'vs/platform/product/common/productService'; import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; const MINIMUM_FONT_SIZE = 6; const MAXIMUM_FONT_SIZE = 25; @@ -47,7 +48,8 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { @IStorageService private readonly _storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { this._updateConfig(); this._configurationService.onDidChangeConfiguration(e => { @@ -55,6 +57,9 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { this._updateConfig(); } }); + + // opt-in to syncing + storageKeysSyncRegistryService.registerStorageKey({ key: 'terminalConfigHelper/launchRecommendationsIgnore', version: 1 }); } private _updateConfig(): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 33b53b690e..5397c73610 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -178,19 +178,14 @@ export class TerminalService implements ITerminalService { this._extHostsReady[remoteAuthority] = { promise, resolve }; } - private async _onBeforeShutdown(): Promise { + private _onBeforeShutdown(): boolean | Promise { if (this.terminalInstances.length === 0) { // No terminal instances, don't veto return false; } if (this.configHelper.config.confirmOnExit) { - // veto if configured to show confirmation and the user choosed not to exit - const veto = await this._showTerminalCloseConfirmation(); - if (!veto) { - this._isShuttingDown = true; - } - return veto; + return this._onBeforeShutdownAsync(); } this._isShuttingDown = true; @@ -198,6 +193,15 @@ export class TerminalService implements ITerminalService { return false; } + private async _onBeforeShutdownAsync(): Promise { + // veto if configured to show confirmation and the user choosed not to exit + const veto = await this._showTerminalCloseConfirmation(); + if (!veto) { + this._isShuttingDown = true; + } + return veto; + } + private _onShutdown(): void { // Dispose of all instances this.terminalInstances.forEach(instance => instance.dispose(true)); 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 6c743f2736..163db90e62 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts @@ -8,6 +8,7 @@ import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/term import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal'; +import { StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} skip suite let fixture: HTMLElement; @@ -29,7 +30,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Fedora, configurationService, null!, null!, null!, null!, null!, null!); + const configHelper = new TerminalConfigHelper(LinuxDistro.Fedora, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, '\'DejaVu Sans Mono\', monospace', 'Fedora should have its font overridden when terminal.integrated.fontFamily not set'); }); @@ -38,7 +39,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!); + const configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, '\'Ubuntu Mono\', monospace', 'Ubuntu should have its font overridden when terminal.integrated.fontFamily not set'); }); @@ -47,7 +48,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk const configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + const configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontFamily, 'foo', 'editor.fontFamily should be the fallback when terminal.integrated.fontFamily not set'); }); @@ -65,7 +66,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk fontSize: 10 } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 10, 'terminal.integrated.fontSize should be selected over editor.fontSize'); @@ -78,11 +79,11 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk fontSize: 0 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 8, 'The minimum terminal font size (with adjustment) should be used when terminal.integrated.fontSize less than it'); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 6, 'The minimum terminal font size should be used when terminal.integrated.fontSize less than it'); @@ -95,7 +96,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk fontSize: 1500 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, 25, 'The maximum terminal font size should be used when terminal.integrated.fontSize more than it'); @@ -108,11 +109,11 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk fontSize: null } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(LinuxDistro.Ubuntu, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize + 2, 'The default editor font size (with adjustment) should be used when terminal.integrated.fontSize is not set'); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize, 'The default editor font size should be used when terminal.integrated.fontSize is not set'); }); @@ -130,7 +131,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk lineHeight: 2 } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().lineHeight, 2, 'terminal.integrated.lineHeight should be selected over editor.lineHeight'); @@ -144,7 +145,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk lineHeight: 0 } }); - configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().lineHeight, 1, 'editor.lineHeight should be 1 when terminal.integrated.lineHeight not set'); }); @@ -157,7 +158,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -169,7 +170,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk fontFamily: 'sans-serif' } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -181,7 +182,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk fontFamily: 'serif' } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); @@ -197,7 +198,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -213,7 +214,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -229,7 +230,7 @@ suite.skip('Workbench - TerminalConfigHelper', () => { // {{SQL CARBON EDIT}} sk } }); - let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(LinuxDistro.Unknown, configurationService, null!, null!, null!, null!, null!, null!, new StorageKeysSyncRegistryService()); configHelper.panelContainer = fixture; assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 55dcc7c3f6..7d73bb9fbe 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -10,18 +10,19 @@ import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { IWorkbenchThemeService, ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IWorkbenchTheme } 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 { 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'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { LIGHT, DARK, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; import { colorThemeSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData'; export class SelectColorThemeAction extends Action { @@ -34,8 +35,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, - @IConfigurationService private readonly configurationService: IConfigurationService + @IViewletService private readonly viewletService: IViewletService ) { super(id, label); } @@ -61,13 +61,8 @@ export class SelectColorThemeAction extends Action { selectThemeTimeout = window.setTimeout(() => { selectThemeTimeout = undefined; const themeId = theme && theme.id !== undefined ? theme.id : currentTheme.id; - let target: ConfigurationTarget | undefined = undefined; - if (applyTheme) { - const confValue = this.configurationService.inspect(ThemeSettings.COLOR_THEME); - target = typeof confValue.workspaceValue !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; - } - this.themeService.setColorTheme(themeId, target).then(undefined, + this.themeService.setColorTheme(themeId, applyTheme ? 'auto' : undefined).then(undefined, err => { onUnexpectedError(err); this.themeService.setColorTheme(currentTheme.id, undefined); @@ -109,7 +104,83 @@ export class SelectColorThemeAction extends Action { } } -class SelectIconThemeAction extends Action { +abstract class AbstractIconThemeAction extends Action { + constructor( + id: string, + label: string, + private readonly quickInputService: IQuickInputService, + private readonly extensionGalleryService: IExtensionGalleryService, + private readonly viewletService: IViewletService + + ) { + super(id, label); + } + + protected abstract get builtInEntry(): QuickPickInput; + protected abstract get installMessage(): string | undefined; + protected abstract get placeholderMessage(): string; + protected abstract get marketplaceTag(): string; + + protected abstract setTheme(id: string, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise; + + protected pick(themes: IWorkbenchTheme[], currentTheme: IWorkbenchTheme) { + let picks: QuickPickInput[] = [this.builtInEntry]; + picks = picks.concat( + toEntries(themes), + configurationEntries(this.extensionGalleryService, this.installMessage) + ); + + let selectThemeTimeout: number | undefined; + + const selectTheme = (theme: ThemeItem, applyTheme: boolean) => { + if (selectThemeTimeout) { + clearTimeout(selectThemeTimeout); + } + selectThemeTimeout = window.setTimeout(() => { + selectThemeTimeout = undefined; + const themeId = theme && theme.id !== undefined ? theme.id : currentTheme.id; + this.setTheme(themeId, applyTheme ? 'auto' : undefined).then(undefined, + err => { + onUnexpectedError(err); + this.setTheme(currentTheme.id, undefined); + } + ); + }, applyTheme ? 0 : 200); + }; + + return new Promise((s, _) => { + let isCompleted = false; + + const autoFocusIndex = firstIndex(picks, p => isItem(p) && p.id === currentTheme.id); + const quickpick = this.quickInputService.createQuickPick(); + quickpick.items = picks; + quickpick.placeholder = this.placeholderMessage; + quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem]; + quickpick.canSelectMany = false; + quickpick.onDidAccept(_ => { + const theme = quickpick.activeItems[0]; + if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry + openExtensionViewlet(this.viewletService, `${this.marketplaceTag} ${quickpick.value}`); + } else { + selectTheme(theme, true); + } + isCompleted = true; + quickpick.hide(); + s(); + }); + quickpick.onDidChangeActive(themes => selectTheme(themes[0], false)); + quickpick.onDidHide(() => { + if (!isCompleted) { + selectTheme(currentTheme, true); + s(); + } + }); + quickpick.show(); + }); + } +} + +class SelectFileIconThemeAction extends AbstractIconThemeAction { static readonly ID = 'workbench.action.selectIconTheme'; static readonly LABEL = localize('selectIconTheme.label', "File Icon Theme"); @@ -117,85 +188,61 @@ class SelectIconThemeAction extends Action { constructor( id: string, label: string, - @IQuickInputService private readonly quickInputService: IQuickInputService, + @IQuickInputService quickInputService: IQuickInputService, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, - // @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, {{SQL CARBON EDIT}} no unused - @IViewletService private readonly viewletService: IViewletService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, + @IViewletService viewletService: IViewletService ) { - super(id, label); + super(id, label, quickInputService, extensionGalleryService, viewletService); } - run(): Promise { - return this.themeService.getFileIconThemes().then(themes => { - const currentTheme = this.themeService.getFileIconTheme(); + protected builtInEntry: QuickPickInput = { id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') }; + protected installMessage = localize('installIconThemes', "Install Additional File Icon Themes..."); + protected placeholderMessage = localize('themes.selectIconTheme', "Select File Icon Theme"); + protected marketplaceTag = 'tag:icon-theme'; + protected setTheme(id: string, settingsTarget: ConfigurationTarget | undefined | 'auto') { + return this.themeService.setFileIconTheme(id, settingsTarget); + } - let picks: QuickPickInput[] = [{ id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') }]; - picks = picks.concat( - toEntries(themes), - // {{SQL CARBON EDIT}} - // configurationEntries(this.extensionGalleryService, localize('installIconThemes', "Install Additional File Icon Themes...")) - ); - - let selectThemeTimeout: number | undefined; - - const selectTheme = (theme: ThemeItem, applyTheme: boolean) => { - if (selectThemeTimeout) { - clearTimeout(selectThemeTimeout); - } - selectThemeTimeout = window.setTimeout(() => { - selectThemeTimeout = undefined; - const themeId = theme && theme.id !== undefined ? theme.id : currentTheme.id; - let target: ConfigurationTarget | undefined = undefined; - if (applyTheme) { - const confValue = this.configurationService.inspect(ThemeSettings.ICON_THEME); - target = typeof confValue.workspaceValue !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; - } - this.themeService.setFileIconTheme(themeId, target).then(undefined, - err => { - onUnexpectedError(err); - this.themeService.setFileIconTheme(currentTheme.id, undefined); - } - ); - }, applyTheme ? 0 : 200); - }; - - return new Promise((s, _) => { - let isCompleted = false; - - const autoFocusIndex = firstIndex(picks, p => isItem(p) && p.id === currentTheme.id); - const quickpick = this.quickInputService.createQuickPick(); - quickpick.items = picks; - quickpick.placeholder = localize('themes.selectIconTheme', "Select File Icon Theme"); - quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem]; - quickpick.canSelectMany = false; - quickpick.onDidAccept(_ => { - const theme = quickpick.activeItems[0]; - if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry - openExtensionViewlet(this.viewletService, `tag:icon-theme ${quickpick.value}`); - } else { - selectTheme(theme, true); - } - isCompleted = true; - quickpick.hide(); - s(); - }); - quickpick.onDidChangeActive(themes => selectTheme(themes[0], false)); - quickpick.onDidHide(() => { - if (!isCompleted) { - selectTheme(currentTheme, true); - s(); - } - }); - quickpick.show(); - }); - }); + async run(): Promise { + this.pick(await this.themeService.getFileIconThemes(), this.themeService.getFileIconTheme()); } } -/*function configurationEntries(extensionGalleryService: IExtensionGalleryService, label: string): QuickPickInput[] { {{SQL CARBON EDIT}} comment out function for no unused - if (extensionGalleryService.isEnabled()) { + +class SelectProductIconThemeAction extends AbstractIconThemeAction { + + static readonly ID = 'workbench.action.selectProductIconTheme'; + static readonly LABEL = localize('selectProductIconTheme.label', "Product Icon Theme"); + + constructor( + id: string, + label: string, + @IQuickInputService quickInputService: IQuickInputService, + @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, + @IViewletService viewletService: IViewletService + + ) { + super(id, label, quickInputService, extensionGalleryService, viewletService); + } + + protected builtInEntry: QuickPickInput = { id: DEFAULT_PRODUCT_ICON_THEME_ID, label: localize('defaultProductIconThemeLabel', 'Default') }; + protected installMessage = undefined; //localize('installProductIconThemes', "Install Additional Product Icon Themes..."); + protected placeholderMessage = localize('themes.selectProductIconTheme', "Select Product Icon Theme"); + protected marketplaceTag = 'tag:product-icon-theme'; + protected setTheme(id: string, settingsTarget: ConfigurationTarget | undefined | 'auto') { + return this.themeService.setProductIconTheme(id, settingsTarget); + } + + async run(): Promise { + this.pick(await this.themeService.getProductIconThemes(), this.themeService.getProductIconTheme()); + } +} + +function configurationEntries(extensionGalleryService: IExtensionGalleryService, label: string | undefined): QuickPickInput[] { + if (extensionGalleryService.isEnabled() && label !== undefined) { return [ { type: 'separator' @@ -208,7 +255,7 @@ class SelectIconThemeAction extends Action { ]; } return []; -}*/ +} function openExtensionViewlet(viewletService: IViewletService, query: string) { return viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { @@ -229,8 +276,8 @@ function isItem(i: QuickPickInput): i is ThemeItem { return (i)['type'] !== 'separator'; } -function toEntries(themes: Array, label?: string): QuickPickInput[] { - const toEntry = (theme: IWorkbenchColorTheme | IWorkbenchFileIconTheme): ThemeItem => ({ id: theme.id, label: theme.label, description: theme.description }); +function toEntries(themes: Array, label?: string): QuickPickInput[] { + const toEntry = (theme: IWorkbenchTheme): ThemeItem => ({ id: theme.id, label: theme.label, description: theme.description }); const sorter = (t1: ThemeItem, t2: ThemeItem) => t1.label.localeCompare(t2.label); let entries: QuickPickInput[] = themes.map(toEntry).sort(sorter); if (entries.length > 0 && label) { @@ -290,8 +337,11 @@ const category = localize('preferences', "Preferences"); const colorThemeDescriptor = SyncActionDescriptor.create(SelectColorThemeAction, SelectColorThemeAction.ID, SelectColorThemeAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_T) }); Registry.as(Extensions.WorkbenchActions).registerWorkbenchAction(colorThemeDescriptor, 'Preferences: Color Theme', category); -const iconThemeDescriptor = SyncActionDescriptor.create(SelectIconThemeAction, SelectIconThemeAction.ID, SelectIconThemeAction.LABEL); -Registry.as(Extensions.WorkbenchActions).registerWorkbenchAction(iconThemeDescriptor, 'Preferences: File Icon Theme', category); +const fileIconThemeDescriptor = SyncActionDescriptor.create(SelectFileIconThemeAction, SelectFileIconThemeAction.ID, SelectFileIconThemeAction.LABEL); +Registry.as(Extensions.WorkbenchActions).registerWorkbenchAction(fileIconThemeDescriptor, 'Preferences: File Icon Theme', category); + +const productIconThemeDescriptor = SyncActionDescriptor.create(SelectProductIconThemeAction, SelectProductIconThemeAction.ID, SelectProductIconThemeAction.LABEL); +Registry.as(Extensions.WorkbenchActions).registerWorkbenchAction(productIconThemeDescriptor, 'Preferences: Product Icon Theme', category); const developerCategory = localize('developer', "Developer"); @@ -311,7 +361,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { group: '4_themes', command: { - id: SelectIconThemeAction.ID, + id: SelectFileIconThemeAction.ID, title: localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme") }, order: 2 @@ -329,7 +379,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '4_themes', command: { - id: SelectIconThemeAction.ID, + id: SelectFileIconThemeAction.ID, title: localize('themes.selectIconTheme.label', "File Icon Theme") }, order: 2 diff --git a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts index 786fe7666b..0c70787ab5 100644 --- a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts +++ b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts @@ -16,7 +16,6 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'v import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; -import product from 'vs/platform/product/common/product'; import { ExplorerFolderContext } from 'vs/workbench/contrib/files/common/files'; import { ResourceContextKey } from 'vs/workbench/common/resources'; @@ -24,7 +23,6 @@ export class TimelinePaneDescriptor implements IViewDescriptor { readonly id = TimelinePaneId; readonly name = TimelinePane.TITLE; readonly ctorDescriptor = new SyncDescriptor(TimelinePane); - readonly when = ContextKeyExpr.equals('config.timeline.showView', true); readonly order = 2; readonly weight = 30; readonly collapsed = true; @@ -43,11 +41,6 @@ configurationRegistry.registerConfiguration({ title: localize('timelineConfigurationTitle', "Timeline"), type: 'object', properties: { - 'timeline.showView': { - type: 'boolean', - description: localize('timeline.showView', "Experimental: When enabled, shows a Timeline view in the Explorer sidebar."), - default: product.quality !== 'stable' - }, 'timeline.excludeSources': { type: 'array', description: localize('timeline.excludeSources', "Experimental: An array of Timeline sources that should be excluded from the Timeline view"), diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index f702114a4d..4aaf15704d 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -29,6 +29,7 @@ import { ShowCurrentReleaseNotesActionId, CheckForVSCodeUpdateActionId } from 'v import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; export const CONTEXT_UPDATE_STATE = new RawContextKey('updateState', StateType.Idle); @@ -179,12 +180,16 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu @IActivityService private readonly activityService: IActivityService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IProductService private readonly productService: IProductService, - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super(); this.state = updateService.state; this.updateStateContextKey = CONTEXT_UPDATE_STATE.bindTo(this.contextKeyService); + // opt-in to syncing + storageKeysSyncRegistryService.registerStorageKey({ key: 'neverShowAgain:update/win32-fast-updates', version: 1 }); + this._register(updateService.onStateChange(this.onUpdateStateChange, this)); this.onUpdateStateChange(this.updateService.state); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index f805993943..628a82ac77 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -100,7 +100,7 @@ const getIdentityTitle = (label: string, authenticationProviderId: string, accou }; const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Preferences Sync: Turn on...") }; const signInCommand = { id: 'workbench.userData.actions.signin', title: localize('sign in', "Preferences Sync: Sign in to sync") }; -const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Preferences Sync: Turn off"), authenticationProviderId, account, authenticationService); } }; +const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Preferences Sync: Turn Off"), authenticationProviderId, account, authenticationService); } }; const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Preferences Sync: Show Settings Conflicts") }; const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") }; const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") }; @@ -123,6 +123,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private readonly badgeDisposable = this._register(new MutableDisposable()); private readonly signInNotificationDisposable = this._register(new MutableDisposable()); private _activeAccount: AuthenticationSession | undefined; + private loginInProgress: boolean = false; constructor( @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @@ -279,22 +280,18 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async onDidChangeSessions(e: { providerId: string, event: AuthenticationSessionsChangeEvent }): Promise { const { providerId, event } = e; if (providerId === this.userDataSyncStore!.authenticationProviderId) { + if (this.loginInProgress) { + return; + } + if (this.activeAccount) { if (event.removed.length) { const activeWasRemoved = !!event.removed.find(removed => removed === this.activeAccount!.id); - - // If the current account was removed, check if another account can be used, otherwise offer to turn off sync + // If the current account was removed, offer to turn off sync if (activeWasRemoved) { - const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []); - if (accounts.length) { - // Show switch dialog here - await this.showSwitchAccountPicker(accounts); - } else { - await this.turnOff(); - this.setActiveAccount(undefined); - return; - } - + await this.turnOff(); + this.setActiveAccount(undefined); + return; } } @@ -815,7 +812,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async signIn(): Promise { try { + this.loginInProgress = true; await this.setActiveAccount(await this.authenticationService.login(this.userDataSyncStore!.authenticationProviderId, ['https://management.core.windows.net/.default', 'offline_access'])); + this.loginInProgress = false; } catch (e) { this.notificationService.error(localize('loginFailed', "Logging in failed: {0}", e)); throw e; @@ -1034,7 +1033,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: 'workbench.userData.actions.syncStatus', - title: localize('sync is on', "Preferences sync is on"), + title: localize('sync is on', "Preferences Sync is On"), menu: [ { id: MenuId.GlobalActivity, diff --git a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts index 6a46abce92..65f4e0004b 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { equals } from 'vs/base/common/arrays'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { memoize } from 'vs/base/common/decorators'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; import { Iterable } from 'vs/base/common/iterator'; import { Lazy } from 'vs/base/common/lazy'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -82,7 +84,7 @@ export interface IWebviewWorkbenchService { resolveWebview( webview: WebviewInput, - ): Promise; + ): CancelablePromise; } export interface WebviewResolver { @@ -117,12 +119,26 @@ export class LazilyResolvedWebviewEditorInput extends WebviewInput { } #resolved = false; + #resolvePromise?: CancelablePromise; + + dispose() { + super.dispose(); + this.#resolvePromise?.cancel(); + this.#resolvePromise = undefined; + } @memoize public async resolve() { if (!this.#resolved) { this.#resolved = true; - await this._webviewWorkbenchService.resolveWebview(this); + this.#resolvePromise = this._webviewWorkbenchService.resolveWebview(this); + try { + await this.#resolvePromise; + } catch (e) { + if (!isPromiseCanceledError(e)) { + throw e; + } + } } return super.resolve(); } @@ -238,10 +254,13 @@ export class WebviewEditorService implements IWebviewWorkbenchService { reviver: WebviewResolver ): IDisposable { this._revivers.add(reviver); - this._revivalPool.reviveFor(reviver, CancellationToken.None); + + const cts = new CancellationTokenSource(); + this._revivalPool.reviveFor(reviver, cts.token); return toDisposable(() => { this._revivers.delete(reviver); + cts.dispose(true); }); } @@ -258,28 +277,31 @@ export class WebviewEditorService implements IWebviewWorkbenchService { } private async tryRevive( - webview: WebviewInput + webview: WebviewInput, + cancellation: CancellationToken, ): Promise { for (const reviver of this._revivers.values()) { if (canRevive(reviver, webview)) { - await reviver.resolveWebview(webview, CancellationToken.None); + await reviver.resolveWebview(webview, cancellation); return true; } } return false; } - public async resolveWebview( + public resolveWebview( webview: WebviewInput, - ): Promise { - const didRevive = await this.tryRevive(webview); - if (!didRevive) { - // A reviver may not be registered yet. Put into pool and resolve promise when we can revive - let resolve: () => void; - const promise = new Promise(r => { resolve = r; }); - this._revivalPool.add(webview, resolve!); - return promise; - } + ): CancelablePromise { + return createCancelablePromise(async (cancellation) => { + const didRevive = await this.tryRevive(webview, cancellation); + if (!didRevive) { + // A reviver may not be registered yet. Put into pool and resolve promise when we can revive + let resolve: () => void; + const promise = new Promise(r => { resolve = r; }); + this._revivalPool.add(webview, resolve!); + return promise; + } + }); } private createWebviewElement( diff --git a/src/vs/workbench/electron-browser/actions/media/actions.css b/src/vs/workbench/electron-browser/actions/media/actions.css new file mode 100644 index 0000000000..9ec163bd7b --- /dev/null +++ b/src/vs/workbench/electron-browser/actions/media/actions.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-window::before { + content: "\ea76"; /* Close icon flips between black dot and "X" for dirty windows */ +} diff --git a/src/vs/workbench/electron-browser/actions/windowActions.ts b/src/vs/workbench/electron-browser/actions/windowActions.ts index 1297d15104..b58e2c3327 100644 --- a/src/vs/workbench/electron-browser/actions/windowActions.ts +++ b/src/vs/workbench/electron-browser/actions/windowActions.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/actions'; + import { URI } from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; import * as nls from 'vs/nls'; @@ -157,6 +159,12 @@ export abstract class BaseSwitchWindow extends Action { tooltip: nls.localize('close', "Close Window") }; + private readonly closeDirtyWindowAction: IQuickInputButton = { + iconClass: 'dirty-window codicon-circle-filled', + tooltip: nls.localize('close', "Close Window"), + alwaysVisible: true + }; + constructor( id: string, label: string, @@ -185,7 +193,7 @@ export abstract class BaseSwitchWindow extends Action { label: win.title, iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : undefined, - buttons: (!this.isQuickNavigate() && currentWindowId !== win.id) ? [this.closeWindowAction] : undefined + buttons: currentWindowId !== win.id ? win.dirty ? [this.closeDirtyWindowAction] : [this.closeWindowAction] : undefined }; }); const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 03d618c06b..324ef196a8 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -269,19 +269,17 @@ export class NativeWindow extends Disposable { })); } - // Document edited (macOS only): indicate for dirty working copies - if (isMacintosh) { - this._register(this.workingCopyService.onDidChangeDirty(workingCopy => { - const gotDirty = workingCopy.isDirty(); - if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - return; // do not indicate dirty of working copies that are auto saved after short delay - } + // Document edited: indicate for dirty working copies + this._register(this.workingCopyService.onDidChangeDirty(workingCopy => { + const gotDirty = workingCopy.isDirty(); + if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + return; // do not indicate dirty of working copies that are auto saved after short delay + } - this.updateDocumentEdited(gotDirty); - })); + this.updateDocumentEdited(gotDirty); + })); - this.updateDocumentEdited(); - } + this.updateDocumentEdited(); // Detect minimize / maximize this._register(Event.any( diff --git a/src/vs/workbench/services/backup/common/backupFileService.ts b/src/vs/workbench/services/backup/common/backupFileService.ts index 4e52ba0a5e..f7b278897d 100644 --- a/src/vs/workbench/services/backup/common/backupFileService.ts +++ b/src/vs/workbench/services/backup/common/backupFileService.ts @@ -14,7 +14,7 @@ import { IResolvedBackup, IBackupFileService } from 'vs/workbench/services/backu import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { ITextSnapshot } from 'vs/editor/common/model'; import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; -import { keys, ResourceMap } from 'vs/base/common/map'; +import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -434,7 +434,7 @@ export class InMemoryBackupFileService implements IBackupFileService { } async getBackups(): Promise { - return keys(this.backups).map(key => URI.parse(key)); + return Array.from(this.backups.keys()).map(key => URI.parse(key)); } async discardBackup(resource: URI): Promise { diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 3441f2ad98..9e808ec6d4 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -203,7 +203,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return; } } catch (e) { /* Ignore */ } - storedFoldersToAdd.push(getStoredWorkspaceFolder(folderURI, folderToAdd.name, workspaceConfigFolder, slashForPath)); + storedFoldersToAdd.push(getStoredWorkspaceFolder(folderURI, false, folderToAdd.name, workspaceConfigFolder, slashForPath)); })); // Apply to array of newStoredFolders diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index ceaef8e43b..ff48d58321 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -20,7 +20,7 @@ import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, Gr import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { coalesce, distinct } from 'vs/base/common/arrays'; +import { coalesce, distinct, insert } from 'vs/base/common/arrays'; import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -476,14 +476,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { private readonly openEditorHandlers: IOpenEditorOverrideHandler[] = []; overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { - this.openEditorHandlers.push(handler); + const remove = insert(this.openEditorHandlers, handler); - return toDisposable(() => { - const index = this.openEditorHandlers.indexOf(handler); - if (index >= 0) { - this.openEditorHandlers.splice(index, 1); - } - }); + return toDisposable(() => remove()); } private onGroupWillOpenEditor(group: IEditorGroup, event: IEditorOpeningEvent): void { diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 22939154fe..d3b58d7353 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -12,7 +12,6 @@ import { IPath, IWindowConfiguration } from 'vs/platform/windows/common/windows' import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import product from 'vs/platform/product/common/product'; -import { serializableToMap } from 'vs/base/common/map'; import { memoize } from 'vs/base/common/decorators'; export class BrowserWindowConfiguration implements IWindowConfiguration { @@ -69,6 +68,7 @@ interface IExtensionHostDebugEnvironment { isExtensionDevelopment: boolean; extensionDevelopmentLocationURI?: URI[]; extensionTestsLocationURI?: URI; + extensionEnabledProposedApi?: string[]; } export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironmentService { @@ -156,6 +156,14 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment return this._extensionHostDebugEnvironment.extensionTestsLocationURI; } + get extensionEnabledProposedApi(): string[] | undefined { + if (!this._extensionHostDebugEnvironment) { + this._extensionHostDebugEnvironment = this.resolveExtensionHostDebugEnvironment(); + } + + return this._extensionHostDebugEnvironment.extensionEnabledProposedApi; + } + @memoize get webviewExternalEndpoint(): string { // TODO: get fallback from product.json @@ -184,7 +192,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment constructor(readonly options: IBrowserWorkbenchEnvironmentConstructionOptions) { if (options.workspaceProvider && Array.isArray(options.workspaceProvider.payload)) { - this.payload = serializableToMap(options.workspaceProvider.payload); + this.payload = new Map(options.workspaceProvider.payload); } } @@ -216,6 +224,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment extensionHostDebugEnvironment.params.port = parseInt(value); extensionHostDebugEnvironment.params.break = true; break; + case 'enableProposedApi': + extensionHostDebugEnvironment.extensionEnabledProposedApi = []; + break; } } } diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 36b19d1b88..59825d762d 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -457,12 +457,12 @@ class ProposedApiController { @IProductService productService: IProductService ) { // Make enabled proposed API be lowercase for case insensitive comparison - this.enableProposedApiFor = (environmentService.args['enable-proposed-api'] || []).map(id => id.toLowerCase()); + this.enableProposedApiFor = (environmentService.extensionEnabledProposedApi || []).map(id => id.toLowerCase()); this.enableProposedApiForAll = !environmentService.isBuilt || // always allow proposed API when running out of sources (!!environmentService.extensionDevelopmentLocationURI && productService.quality !== 'stable') || // do not allow proposed API against stable builds when developing an extension - (this.enableProposedApiFor.length === 0 && 'enable-proposed-api' in environmentService.args); // always allow proposed API if --enable-proposed-api is provided without extension ID + (this.enableProposedApiFor.length === 0 && Array.isArray(environmentService.extensionEnabledProposedApi)); // always allow proposed API if --enable-proposed-api is provided without extension ID this.productAllowProposedApi = new Set(); if (isNonEmptyArray(productService.extensionAllowedProposedApi)) { diff --git a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts index 9e6d36f48f..b8de7fdfdc 100644 --- a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts +++ b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts @@ -20,7 +20,6 @@ export interface IRPCProtocol { assertRegistered(identifiers: ProxyIdentifier[]): void; } -// @ts-ignore export class ProxyIdentifier { public static count = 0; _proxyIdentifierBrand: void; diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index 072d3a3cd9..50101d7519 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -5,7 +5,7 @@ import * as nativeWatchdog from 'native-watchdog'; import * as net from 'net'; -import * as minimist from 'vscode-minimist'; +import * as minimist from 'minimist'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index be78cb390e..e9627cfb24 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -17,7 +17,6 @@ import { trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { mapToSerializable } from 'vs/base/common/map'; /** * A workspace to open in the workbench can either be: @@ -142,7 +141,7 @@ export class BrowserHostService extends Disposable implements IHostService { const environment = new Map(); environment.set('openFile', openable.fileUri.toString()); - this.workspaceProvider.open(undefined, { payload: mapToSerializable(environment) }); + this.workspaceProvider.open(undefined, { payload: Array.from(environment.entries()) }); } } } diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index 0ee91b2af7..39c91ae229 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -14,7 +14,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { isEqual, basenameOrAuthority, basename, joinPath, dirname } from 'vs/base/common/resources'; import { tildify, getPathLabel } from 'vs/base/common/labels'; -import { ltrim, endsWith } from 'vs/base/common/strings'; +import { ltrim } from 'vs/base/common/strings'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION, toWorkspaceIdentifier, isWorkspaceIdentifier, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting, IFormatterChangeEvent } from 'vs/platform/label/common/label'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -200,7 +200,7 @@ export class LabelService extends Disposable implements ILabelService { // Workspace: Saved let filename = basename(workspace.configPath); - if (endsWith(filename, WORKSPACE_EXTENSION)) { + if (filename.endsWith(WORKSPACE_EXTENSION)) { filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } let label; @@ -275,7 +275,7 @@ export class LabelService extends Disposable implements ILabelService { private appendSeparatorIfMissing(label: string, formatting: ResourceLabelFormatting): string { let appendedLabel = label; - if (!endsWith(label, formatting.separator)) { + if (!label.endsWith(formatting.separator)) { appendedLabel += formatting.separator; } return appendedLabel; diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index d1b73158e3..0142a2c152 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -11,7 +11,6 @@ import { Event } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IAction, Action } from 'vs/base/common/actions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; export class NotificationService extends Disposable implements INotificationService { @@ -21,8 +20,7 @@ export class NotificationService extends Disposable implements INotificationServ get model(): INotificationsModel { return this._model; } constructor( - @IStorageService private readonly storageService: IStorageService, - @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService + @IStorageService private readonly storageService: IStorageService ) { super(); } @@ -70,11 +68,6 @@ export class NotificationService extends Disposable implements INotificationServ const scope = notification.neverShowAgain.scope === NeverShowAgainScope.WORKSPACE ? StorageScope.WORKSPACE : StorageScope.GLOBAL; const id = notification.neverShowAgain.id; - // opt-in to syncing if global - if (scope === StorageScope.GLOBAL) { - this.storageKeysSyncRegistryService.registerStorageKey({ key: id, version: 1 }); - } - // If the user already picked to not show the notification // again, we return with a no-op notification here if (this.storageService.getBoolean(id, scope)) { @@ -126,11 +119,6 @@ export class NotificationService extends Disposable implements INotificationServ const scope = options.neverShowAgain.scope === NeverShowAgainScope.WORKSPACE ? StorageScope.WORKSPACE : StorageScope.GLOBAL; const id = options.neverShowAgain.id; - // opt-in to syncing if global - if (scope === StorageScope.GLOBAL) { - this.storageKeysSyncRegistryService.registerStorageKey({ key: id, version: 1 }); - } - // If the user already picked to not show the notification // again, we return with a no-op notification here if (this.storageService.getBoolean(id, scope)) { diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 365998dcd3..1bc3d13a59 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -77,7 +77,7 @@ export class FileWalker { this.errors = []; if (this.filePattern) { - this.normalizedFilePatternLowercase = prepareQuery(this.filePattern).valueLowercase; + this.normalizedFilePatternLowercase = prepareQuery(this.filePattern).normalizedLowercase; } this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern); diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 18c7bef1eb..873bb70eba 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -312,7 +312,7 @@ export class SearchService implements IRawSearchService { // Pattern match on results const results: IRawFileMatch[] = []; - const normalizedSearchValueLowercase = prepareQuery(searchValue).valueLowercase; + const normalizedSearchValueLowercase = prepareQuery(searchValue).normalizedLowercase; for (const entry of cachedEntries) { // Check if this entry is a match for the search value diff --git a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts index 9a996e7e9f..724ef5331d 100644 --- a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts +++ b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts @@ -11,6 +11,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { ITextFileSaveParticipant, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { SaveReason } from 'vs/workbench/common/editor'; import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { insert } from 'vs/base/common/arrays'; export class TextFileSaveParticipant extends Disposable { @@ -24,9 +25,9 @@ export class TextFileSaveParticipant extends Disposable { } addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable { - this.saveParticipants.push(participant); + const remove = insert(this.saveParticipants, participant); - return toDisposable(() => this.saveParticipants.splice(this.saveParticipants.indexOf(participant), 1)); + return toDisposable(() => remove()); } participate(model: ITextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 4ef7ad3df1..ae580b89ba 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -40,6 +40,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; export class NativeTextFileService extends AbstractTextFileService { @@ -58,7 +59,8 @@ export class NativeTextFileService extends AbstractTextFileService { @ITextModelService textModelService: ITextModelService, @ICodeEditorService codeEditorService: ICodeEditorService, @IRemotePathService remotePathService: IRemotePathService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @ILogService private readonly logService: ILogService ) { super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, remotePathService, workingCopyFileService); } @@ -298,8 +300,16 @@ export class NativeTextFileService extends AbstractTextFileService { sudoCommand.push('--file-write', `"${source}"`, `"${target}"`); sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => { - if (error || stderr) { - reject(error || stderr); + if (stdout) { + this.logService.trace(`[sudo-prompt] received stdout: ${stdout}`); + } + + if (stderr) { + this.logService.trace(`[sudo-prompt] received stderr: ${stderr}`); + } + + if (error) { + reject(error); } else { resolve(undefined); } diff --git a/src/vs/workbench/services/themes/browser/productIconThemeData.ts b/src/vs/workbench/services/themes/browser/productIconThemeData.ts index 98985fcd99..b19f016f87 100644 --- a/src/vs/workbench/services/themes/browser/productIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/productIconThemeData.ts @@ -86,7 +86,7 @@ export class ProductIconThemeData implements IWorkbenchProductIconTheme { static get defaultTheme(): ProductIconThemeData { let themeData = ProductIconThemeData._defaultProductIconTheme; if (!themeData) { - themeData = ProductIconThemeData._defaultProductIconTheme = new ProductIconThemeData(DEFAULT_PRODUCT_ICON_THEME_ID, nls.localize('defaultTheme', 'Default theme'), DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE); + themeData = ProductIconThemeData._defaultProductIconTheme = new ProductIconThemeData(DEFAULT_PRODUCT_ICON_THEME_ID, nls.localize('defaultTheme', 'Default'), DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE); themeData.isLoaded = true; themeData.extensionData = undefined; themeData.watch = false; diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 766c8103b1..6d3b11694d 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -115,7 +115,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.currentFileIconTheme = FileIconThemeData.createUnloadedTheme(''); this.productIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentProductIconTheme.bind(this)); - this.productIconThemeRegistry = new ThemeRegistry(extensionService, productIconThemesExtPoint, ProductIconThemeData.fromExtensionTheme, true, ProductIconThemeData.defaultTheme); + this.productIconThemeRegistry = new ThemeRegistry(extensionService, productIconThemesExtPoint, ProductIconThemeData.fromExtensionTheme, true, ProductIconThemeData.defaultTheme, true); this.onProductIconThemeChange = new Emitter(); this.currentProductIconTheme = ProductIconThemeData.createUnloadedTheme(''); @@ -265,7 +265,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (e.affectsConfiguration(ThemeSettings.PREFERRED_HC_THEME) && this.getPreferredColorScheme() === HIGH_CONTRAST) { this.applyPreferredColorTheme(HIGH_CONTRAST); } - if (e.affectsConfiguration(ThemeSettings.ICON_THEME)) { + if (e.affectsConfiguration(ThemeSettings.FILE_ICON_THEME)) { this.restoreFileIconTheme(); } if (e.affectsConfiguration(ThemeSettings.PRODUCT_ICON_THEME)) { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index e097f61d7f..03c57ae083 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -170,25 +170,6 @@ export class ColorThemeData implements IWorkbenchColorTheme { } } } - for (const rule of tokenClassificationRegistry.getTokenStylingDefaultRules()) { - const matchScore = rule.selector.match(type, modifiers, language); - if (matchScore >= 0) { - let style: TokenStyle | undefined; - if (rule.defaults.scopesToProbe) { - style = this.resolveScopes(rule.defaults.scopesToProbe); - if (style) { - _processStyle(matchScore, style, rule.defaults.scopesToProbe); - } - } - if (!style && useDefault !== false) { - const tokenStyleValue = rule.defaults[this.type]; - style = this.resolveTokenStyleValue(tokenStyleValue); - if (style) { - _processStyle(matchScore, style, tokenStyleValue!); - } - } - } - } for (const rule of this.tokenStylingRules) { const matchScore = rule.selector.match(type, modifiers, language); if (matchScore >= 0) { @@ -201,6 +182,36 @@ export class ColorThemeData implements IWorkbenchColorTheme { _processStyle(matchScore, rule.style, rule); } } + let hasUndefinedStyleProperty = false; + for (let k in score) { + const key = k as keyof TokenStyle; + if (score[key] === -1) { + hasUndefinedStyleProperty = true; + } else { + score[key] = Number.MAX_VALUE; // set it to the max, so it won't be replaced by a default + } + } + if (hasUndefinedStyleProperty) { + for (const rule of tokenClassificationRegistry.getTokenStylingDefaultRules()) { + const matchScore = rule.selector.match(type, modifiers, language); + if (matchScore >= 0) { + let style: TokenStyle | undefined; + if (rule.defaults.scopesToProbe) { + style = this.resolveScopes(rule.defaults.scopesToProbe); + if (style) { + _processStyle(matchScore, style, rule.defaults.scopesToProbe); + } + } + if (!style && useDefault !== false) { + const tokenStyleValue = rule.defaults[this.type]; + style = this.resolveTokenStyleValue(tokenStyleValue); + if (style) { + _processStyle(matchScore, style, tokenStyleValue!); + } + } + } + } + } return TokenStyle.fromData(result); } @@ -212,8 +223,8 @@ export class ColorThemeData implements IWorkbenchColorTheme { if (tokenStyleValue === undefined) { return undefined; } else if (typeof tokenStyleValue === 'string') { - const { type, modifiers, language } = parseClassifierString(tokenStyleValue); - return this.getTokenStyle(type, modifiers, language || ''); + const { type, modifiers, language } = parseClassifierString(tokenStyleValue, ''); + return this.getTokenStyle(type, modifiers, language); } else if (typeof tokenStyleValue === 'object') { return tokenStyleValue; } @@ -248,8 +259,8 @@ export class ColorThemeData implements IWorkbenchColorTheme { } public getTokenStyleMetadata(typeWithLanguage: string, modifiers: string[], defaultLanguage: string, useDefault = true, definitions: TokenStyleDefinitions = {}): ITokenStyle | undefined { - const { type, language } = parseClassifierString(typeWithLanguage); - let style = this.getTokenStyle(type, modifiers, language || defaultLanguage, useDefault, definitions); + const { type, language } = parseClassifierString(typeWithLanguage, defaultLanguage); + let style = this.getTokenStyle(type, modifiers, language, useDefault, definitions); if (!style) { return undefined; } diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 0e29069838..512184afba 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -78,11 +78,10 @@ const colorCustomizationsSchema: IConfigurationPropertySchema = { } }] }; - const fileIconThemeSettingSchema: IConfigurationPropertySchema = { type: ['string', 'null'], default: DEFAULT_FILE_ICON_THEME_SETTING_VALUE, - description: nls.localize('iconTheme', "Specifies the icon theme used in the workbench or 'null' to not show any file icons."), + description: nls.localize('iconTheme', "Specifies the file icon theme used in the workbench or 'null' to not show any file icons."), enum: [null], enumDescriptions: [nls.localize('noIconThemeDesc', 'No file icons')], errorMessage: nls.localize('iconThemeError', "File icon theme is unknown or not installed.") @@ -106,7 +105,7 @@ const themeSettingsConfiguration: IConfigurationNode = { [ThemeSettings.PREFERRED_LIGHT_THEME]: preferredLightThemeSettingSchema, [ThemeSettings.PREFERRED_HC_THEME]: preferredHCThemeSettingSchema, [ThemeSettings.DETECT_COLOR_SCHEME]: detectColorSchemeSettingSchema, - [ThemeSettings.ICON_THEME]: fileIconThemeSettingSchema, + [ThemeSettings.FILE_ICON_THEME]: fileIconThemeSettingSchema, [ThemeSettings.COLOR_CUSTOMIZATIONS]: colorCustomizationsSchema, [ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema } @@ -212,7 +211,7 @@ export class ThemeConfiguration { } public get fileIconTheme(): string | null { - return this.configurationService.getValue(ThemeSettings.ICON_THEME); + return this.configurationService.getValue(ThemeSettings.FILE_ICON_THEME); } public get productIconTheme(): string { @@ -237,7 +236,7 @@ export class ThemeConfiguration { } public async setFileIconTheme(theme: IWorkbenchFileIconTheme, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { - await this.writeConfiguration(ThemeSettings.ICON_THEME, theme.settingsId, settingsTarget); + await this.writeConfiguration(ThemeSettings.FILE_ICON_THEME, theme.settingsId, settingsTarget); return theme; } @@ -257,6 +256,8 @@ export class ThemeConfiguration { settingsTarget = ConfigurationTarget.WORKSPACE_FOLDER; } else if (!types.isUndefined(settings.workspaceValue)) { settingsTarget = ConfigurationTarget.WORKSPACE; + } else if (!types.isUndefined(settings.userRemote)) { + settingsTarget = ConfigurationTarget.USER_REMOTE; } else { settingsTarget = ConfigurationTarget.USER; } @@ -271,12 +272,11 @@ export class ThemeConfiguration { } value = undefined; // remove configuration from user settings } - } else if (settingsTarget === ConfigurationTarget.WORKSPACE || settingsTarget === ConfigurationTarget.WORKSPACE_FOLDER) { + } else if (settingsTarget === ConfigurationTarget.WORKSPACE || settingsTarget === ConfigurationTarget.WORKSPACE_FOLDER || settingsTarget === ConfigurationTarget.USER_REMOTE) { if (value === settings.value) { return Promise.resolve(undefined); // nothing to do } } return this.configurationService.updateValue(key, value, settingsTarget); } - } diff --git a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts index 9b22efac75..5ff351facb 100644 --- a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts +++ b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts @@ -10,7 +10,7 @@ import * as resources from 'vs/base/common/resources'; import { ExtensionMessageCollector, IExtensionPoint, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionData, IThemeExtensionPoint, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; @@ -128,7 +128,8 @@ export class ThemeRegistry { private readonly themesExtPoint: IExtensionPoint, private create: (theme: IThemeExtensionPoint, themeLocation: URI, extensionData: ExtensionData) => T, private idRequired = false, - private builtInTheme: T | undefined = undefined + private builtInTheme: T | undefined = undefined, + private isProposedApi = false ) { this.extensionThemes = []; this.initialize(); @@ -144,6 +145,10 @@ export class ThemeRegistry { } this.extensionThemes.length = 0; for (let ext of extensions) { + if (this.isProposedApi) { + checkProposedApiEnabled(ext.description); + return; + } let extensionData: ExtensionData = { extensionId: ext.description.identifier.value, extensionPublisher: ext.description.publisher, diff --git a/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts b/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts index 9231380f3e..338ed6357c 100644 --- a/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts @@ -5,8 +5,7 @@ import * as nls from 'vs/nls'; import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { getTokenClassificationRegistry, ITokenClassificationRegistry, typeAndModifierIdPattern, TokenStyleDefaults, TokenStyle, fontStylePattern, selectorPattern } from 'vs/platform/theme/common/tokenClassificationRegistry'; -import { textmateColorSettingsSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema'; +import { getTokenClassificationRegistry, ITokenClassificationRegistry, typeAndModifierIdPattern } from 'vs/platform/theme/common/tokenClassificationRegistry'; interface ITokenTypeExtensionPoint { id: string; @@ -20,24 +19,10 @@ interface ITokenModifierExtensionPoint { } interface ITokenStyleDefaultExtensionPoint { - selector: string; - scope?: string[]; - light?: { - foreground?: string; - fontStyle?: string; - }; - dark?: { - foreground?: string; - fontStyle?: string; - }; - highContrast?: { - foreground?: string; - fontStyle?: string; - }; + language?: string; + scopes: { [selector: string]: string[] }; } -const colorPattern = '^#([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$'; - const tokenClassificationRegistry: ITokenClassificationRegistry = getTokenClassificationRegistry(); const tokenTypeExtPoint = ExtensionsRegistry.registerExtensionPoint({ @@ -54,6 +39,12 @@ const tokenTypeExtPoint = ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'semanticTokenStyleDefaults', + extensionPoint: 'semanticTokenScopes', jsonSchema: { - description: nls.localize('contributes.semanticTokenStyleDefaults', 'Contributes semantic token style defaults.'), + description: nls.localize('contributes.semanticTokenScopes', 'Contributes semantic token scope maps.'), type: 'array', items: { type: 'object', properties: { - selector: { - type: 'string', - description: nls.localize('contributes.semanticTokenStyleDefaults.selector', 'The selector matching token types and modifiers.'), - pattern: selectorPattern, - patternErrorMessage: nls.localize('contributes.semanticTokenStyleDefaults.selector.format', 'Selectors should be in the form (type|*)(.modifier)*(:language)?'), + language: { + description: nls.localize('contributes.semanticTokenScopes.languages', 'Lists the languge for which the defaults are.'), + type: 'string' }, - scope: { - type: 'array', - description: nls.localize('contributes.semanticTokenStyleDefaults.scope', 'A TextMate scope against the current color theme is matched to find the style for the given selector'), - items: { - type: 'string' + scopes: { + description: nls.localize('contributes.semanticTokenScopes.scopes', 'Maps a semantic token (described by semantic token selector) to one or more textMate scopes used to represent that token.'), + type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'string' + } } - }, - light: { - description: nls.localize('contributes.semanticTokenStyleDefaults.light', 'The default style used for light themes'), - $ref: textmateColorSettingsSchemaId - }, - dark: { - description: nls.localize('contributes.semanticTokenStyleDefaults.dark', 'The default style used for dark themes'), - $ref: textmateColorSettingsSchemaId - }, - highContrast: { - description: nls.localize('contributes.semanticTokenStyleDefaults.hc', 'The default style used for high contrast themes'), - $ref: textmateColorSettingsSchemaId } } } @@ -147,24 +127,6 @@ export class TokenClassificationExtensionPoints { } return true; } - function validateStyle(style: { foreground?: string; fontStyle?: string; } | undefined, extensionPoint: string, collector: ExtensionMessageCollector): TokenStyle | undefined { - if (!style) { - return undefined; - } - if (style.foreground) { - if (typeof style.foreground !== 'string' || !style.foreground.match(colorPattern)) { - collector.error(nls.localize('invalid.color', "'configuration.{0}.foreground' must follow the pattern #RRGGBB[AA]", extensionPoint)); - return undefined; - } - } - if (style.fontStyle) { - if (typeof style.fontStyle !== 'string' || !style.fontStyle.match(fontStylePattern)) { - collector.error(nls.localize('invalid.fontStyle', "'configuration.{0}.fontStyle' must be one or a combination of \'italic\', \'bold\' or \'underline\' or the empty string", extensionPoint)); - return undefined; - } - } - return TokenStyle.fromSettings(style.foreground, style.fontStyle); - } tokenTypeExtPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { @@ -216,49 +178,45 @@ export class TokenClassificationExtensionPoints { const collector = extension.collector; if (!extensionValue || !Array.isArray(extensionValue)) { - collector.error(nls.localize('invalid.semanticTokenStyleDefaultConfiguration', "'configuration.semanticTokenStyleDefaults' must be an array")); + collector.error(nls.localize('invalid.semanticTokenScopes.configuration', "'configuration.semanticTokenScopes' must be an array")); return; } for (const contribution of extensionValue) { - if (typeof contribution.selector !== 'string' || contribution.selector.length === 0) { - collector.error(nls.localize('invalid.selector', "'configuration.semanticTokenStyleDefaults.selector' must be defined and can not be empty")); + if (contribution.language && typeof contribution.language !== 'string') { + collector.error(nls.localize('invalid.semanticTokenScopes.language', "'configuration.semanticTokenScopes.language' must be a string")); continue; } - if (!contribution.selector.match(selectorPattern)) { - collector.error(nls.localize('invalid.selector.format', "'configuration.semanticTokenStyleDefaults.selector' must be in the form (type|*)(.modifier)*(:language)?")); + if (!contribution.scopes || typeof contribution.scopes !== 'object') { + collector.error(nls.localize('invalid.semanticTokenScopes.scopes', "'configuration.semanticTokenScopes.scopes' must be defined as an object")); continue; } - - const tokenStyleDefault: TokenStyleDefaults = {}; - - if (contribution.scope) { - if ((!Array.isArray(contribution.scope) || contribution.scope.some(s => typeof s !== 'string'))) { - collector.error(nls.localize('invalid.scope', "If defined, 'configuration.semanticTokenStyleDefaults.scope' must be an array of strings")); + for (let selectorString in contribution.scopes) { + const tmScopes = contribution.scopes[selectorString]; + if (!Array.isArray(tmScopes) || tmScopes.some(l => typeof l !== 'string')) { + collector.error(nls.localize('invalid.semanticTokenScopes.scopes.value', "'configuration.semanticTokenScopes.scopes' values must be an array of strings")); continue; } - tokenStyleDefault.scopesToProbe = [contribution.scope]; - } - tokenStyleDefault.light = validateStyle(contribution.light, 'semanticTokenStyleDefaults.light', collector); - tokenStyleDefault.dark = validateStyle(contribution.dark, 'semanticTokenStyleDefaults.dark', collector); - tokenStyleDefault.hc = validateStyle(contribution.highContrast, 'semanticTokenStyleDefaults.highContrast', collector); - - try { - const selector = tokenClassificationRegistry.parseTokenSelector(contribution.selector); - tokenClassificationRegistry.registerTokenStyleDefault(selector, tokenStyleDefault); - } catch (e) { - collector.error(nls.localize('invalid.selector.parsing', "configuration.semanticTokenStyleDefaults.selector': Problems parsing {0}.", contribution.selector)); - // invalid selector, ignore + try { + const selector = tokenClassificationRegistry.parseTokenSelector(selectorString, contribution.language); + tokenClassificationRegistry.registerTokenStyleDefault(selector, { scopesToProbe: tmScopes.map(s => s.split(' ')) }); + } catch (e) { + collector.error(nls.localize('invalid.semanticTokenScopes.scopes.selector', "configuration.semanticTokenScopes.scopes': Problems parsing selector {0}.", selectorString)); + // invalid selector, ignore + } } } } for (const extension of delta.removed) { const extensionValue = extension.value; for (const contribution of extensionValue) { - try { - const selector = tokenClassificationRegistry.parseTokenSelector(contribution.selector); - tokenClassificationRegistry.deregisterTokenStyleDefault(selector); - } catch (e) { - // invalid selector, ignore + for (let selectorString in contribution.scopes) { + const tmScopes = contribution.scopes[selectorString]; + try { + const selector = tokenClassificationRegistry.parseTokenSelector(selectorString, contribution.language); + tokenClassificationRegistry.registerTokenStyleDefault(selector, { scopesToProbe: tmScopes.map(s => s.split(' ')) }); + } catch (e) { + // invalid selector, ignore + } } } } diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 0cadd30902..5c9436f1a8 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -20,7 +20,8 @@ export const HC_THEME_ID = 'Default High Contrast'; export enum ThemeSettings { COLOR_THEME = 'workbench.colorTheme', - ICON_THEME = 'workbench.iconTheme', + FILE_ICON_THEME = 'workbench.iconTheme', + PRODUCT_ICON_THEME = 'workbench.productIconTheme', COLOR_CUSTOMIZATIONS = 'workbench.colorCustomizations', TOKEN_COLOR_CUSTOMIZATIONS = 'editor.tokenColorCustomizations', TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL = 'editor.tokenColorCustomizationsExperimental', @@ -29,18 +30,19 @@ export enum ThemeSettings { PREFERRED_LIGHT_THEME = 'workbench.preferredLightColorTheme', PREFERRED_HC_THEME = 'workbench.preferredHighContrastColorTheme', DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme', - DETECT_HC = 'window.autoDetectHighContrast', - - PRODUCT_ICON_THEME = 'workbench.productIconTheme' + DETECT_HC = 'window.autoDetectHighContrast' } -export interface IWorkbenchColorTheme extends IColorTheme { +export interface IWorkbenchTheme { readonly id: string; readonly label: string; - readonly settingsId: string; readonly extensionData?: ExtensionData; readonly description?: string; - readonly isLoaded: boolean; + readonly settingsId: string | null; +} + +export interface IWorkbenchColorTheme extends IWorkbenchTheme, IColorTheme { + readonly settingsId: string; readonly tokenColors: ITextMateThemingRule[]; } @@ -48,44 +50,28 @@ export interface IColorMap { [id: string]: Color; } -export interface IWorkbenchFileIconTheme extends IFileIconTheme { - readonly id: string; - readonly label: string; - readonly settingsId: string | null; - readonly description?: string; - readonly extensionData?: ExtensionData; - - readonly isLoaded: boolean; - readonly hasFileIcons: boolean; - readonly hasFolderIcons: boolean; - readonly hidesExplorerArrows: boolean; +export interface IWorkbenchFileIconTheme extends IWorkbenchTheme, IFileIconTheme { } -export interface IWorkbenchProductIconTheme { - readonly id: string; - readonly label: string; +export interface IWorkbenchProductIconTheme extends IWorkbenchTheme { readonly settingsId: string; - readonly description?: string; - readonly extensionData?: ExtensionData; - - readonly isLoaded: boolean; } export interface IWorkbenchThemeService extends IThemeService { _serviceBrand: undefined; - setColorTheme(themeId: string | undefined, settingsTarget: ConfigurationTarget | undefined): Promise; + setColorTheme(themeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise; getColorTheme(): IWorkbenchColorTheme; getColorThemes(): Promise; onDidColorThemeChange: Event; restoreColorTheme(): void; - setFileIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined): Promise; + setFileIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise; getFileIconTheme(): IWorkbenchFileIconTheme; getFileIconThemes(): Promise; onDidFileIconThemeChange: Event; - setProductIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined): Promise; + setProductIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise; getProductIconTheme(): IWorkbenchProductIconTheme; getProductIconThemes(): Promise; onDidProductIconThemeChange: Event; diff --git a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts index 5349323529..9f80711119 100644 --- a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts +++ b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts @@ -398,8 +398,11 @@ suite('Themes - TokenStyleResolving', () => { test('language - scope resolving', async () => { const registry = getTokenClassificationRegistry(); - registry.registerTokenStyleDefault(registry.parseTokenSelector('type:typescript'), { scopesToProbe: [['entity.name.type.ts']] }); + const numberOfDefaultRules = registry.getTokenStylingDefaultRules().length; + + registry.registerTokenStyleDefault(registry.parseTokenSelector('type', 'typescript1'), { scopesToProbe: [['entity.name.type.ts1']] }); + registry.registerTokenStyleDefault(registry.parseTokenSelector('type:javascript1'), { scopesToProbe: [['entity.name.type.js1']] }); try { const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test'); @@ -411,17 +414,20 @@ suite('Themes - TokenStyleResolving', () => { settings: { foreground: '#aa0000' } }, { - scope: 'entity.name.type.ts', + scope: 'entity.name.type.ts1', settings: { foreground: '#bb0000' } } ] }); - assertTokenStyles(themeData, { 'type': ts('#aa0000', undefined) }, 'javascript'); - assertTokenStyles(themeData, { 'type': ts('#bb0000', undefined) }, 'typescript'); + assertTokenStyles(themeData, { 'type': ts('#aa0000', undefined) }, 'javascript1'); + assertTokenStyles(themeData, { 'type': ts('#bb0000', undefined) }, 'typescript1'); } finally { - registry.deregisterTokenType('type/typescript'); + registry.deregisterTokenStyleDefault(registry.parseTokenSelector('type', 'typescript1')); + registry.deregisterTokenStyleDefault(registry.parseTokenSelector('type:javascript1')); + + assert.equal(registry.getTokenStylingDefaultRules().length, numberOfDefaultRules); } }); }); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 50da0f6adf..f599712d1e 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -15,6 +15,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { firstIndex } from 'vs/base/common/arrays'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; class CounterSet implements IReadableSet { @@ -219,7 +220,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { super(); @@ -452,6 +454,43 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor if (!skipCacheUpdate) { this.saveViewPositionsToCache(); + + const containerToString = (container: ViewContainer): string => { + if (container.id.startsWith(ViewDescriptorService.COMMON_CONTAINER_ID_PREFIX)) { + return 'custom'; + } + + if (!container.extensionId) { + return container.id; + } + + return 'extension'; + }; + + // Log on cache update to avoid duplicate events in other windows + const viewCount = views.length; + const fromContainer = containerToString(from); + const toContainer = containerToString(to); + const fromLocation = oldLocation === ViewContainerLocation.Panel ? 'panel' : 'sidebar'; + const toLocation = newLocation === ViewContainerLocation.Panel ? 'panel' : 'sidebar'; + + interface ViewDescriptorServiceMoveViewsEvent { + viewCount: number; + fromContainer: string; + toContainer: string; + fromLocation: string; + toLocation: string; + } + + type ViewDescriptorServiceMoveViewsClassification = { + viewCount: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + fromContainer: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + toContainer: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + fromLocation: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + toLocation: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + }; + + this.telemetryService.publicLog2('viewDescriptorService.moveViews', { viewCount, fromContainer, toContainer, fromLocation, toLocation }); } } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts index d7a3488cde..9cabda9631 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts @@ -13,6 +13,7 @@ import { IWorkingCopyFileOperationParticipant } from 'vs/workbench/services/work import { URI } from 'vs/base/common/uri'; import { FileOperation } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { insert } from 'vs/base/common/arrays'; export class WorkingCopyFileOperationParticipant extends Disposable { @@ -27,9 +28,9 @@ export class WorkingCopyFileOperationParticipant extends Disposable { } addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable { - this.participants.push(participant); + const remove = insert(this.participants, participant); - return toDisposable(() => this.participants.splice(this.participants.indexOf(participant), 1)); + return toDisposable(() => remove()); } async participate(target: URI, source: URI | undefined, operation: FileOperation): Promise { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index 9948c63f76..a0f81dc98e 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -8,7 +8,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { values, ResourceMap } from 'vs/base/common/map'; +import { ResourceMap } from 'vs/base/common/map'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { ITextSnapshot } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; // {{SQL CARBON EDIT}} @chlafreniere need to block working copies of notebook editors from being tracked @@ -185,7 +185,7 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic //#region Registry - get workingCopies(): IWorkingCopy[] { return values(this._workingCopies); } + get workingCopies(): IWorkingCopy[] { return Array.from(this._workingCopies.values()); } private _workingCopies = new Set(); private readonly mapResourceToWorkingCopy = new ResourceMap(); diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index af6d6940fe..d7cad2fe9f 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER, IEnterWorkspaceResult, hasWorkspaceFileExtension, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER, IEnterWorkspaceResult, hasWorkspaceFileExtension, WORKSPACE_EXTENSION, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -234,9 +234,11 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi return; } + const isFromUntitledWorkspace = isUntitledWorkspace(configPathURI, this.environmentService); + // Read the contents of the workspace file, update it to new location and save it. const raw = await this.fileService.readFile(configPathURI); - const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value.toString(), configPathURI, targetConfigPathURI); + const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value.toString(), configPathURI, isFromUntitledWorkspace, targetConfigPathURI); await this.textFileService.create(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true }); } diff --git a/src/vs/workbench/services/workspaces/browser/workspacesService.ts b/src/vs/workbench/services/workspaces/browser/workspacesService.ts index fe9dbe8ac7..05d42c7f96 100644 --- a/src/vs/workbench/services/workspaces/browser/workspacesService.ts +++ b/src/vs/workbench/services/workspaces/browser/workspacesService.ts @@ -139,7 +139,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; if (folders) { for (const folder of folders) { - storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, folder.name, this.environmentService.untitledWorkspacesHome)); + storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, this.environmentService.untitledWorkspacesHome)); } } @@ -165,6 +165,15 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS } //#endregion + + + //#region Dirty Workspaces + + async getDirtyWorkspaces(): Promise> { + return []; // Currently not supported in web + } + + //#endregion } registerSingleton(IWorkspacesService, BrowserWorkspacesService, true); diff --git a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts index a6754728f6..3fc4417215 100644 --- a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts @@ -140,8 +140,6 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi return false; } } - - return false; } async isValidTargetWorkspacePath(path: URI): Promise { diff --git a/src/vs/workbench/test/browser/api/extHostTypes.test.ts b/src/vs/workbench/test/browser/api/extHostTypes.test.ts index 24bb709555..25097801b6 100644 --- a/src/vs/workbench/test/browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypes.test.ts @@ -566,4 +566,64 @@ suite('ExtHostTypes', function () { assert.ok(!types.CodeActionKind.RefactorExtract.intersects(types.CodeActionKind.Empty.append('other').append('refactor'))); assert.ok(!types.CodeActionKind.RefactorExtract.intersects(types.CodeActionKind.Empty.append('refactory'))); }); + + function toArr(uint32Arr: Uint32Array): number[] { + const r = []; + for (let i = 0, len = uint32Arr.length; i < len; i++) { + r[i] = uint32Arr[i]; + } + return r; + } + + test('SemanticTokensBuilder simple', () => { + const builder = new types.SemanticTokensBuilder(); + builder.push(1, 0, 5, 1, 1); + builder.push(1, 10, 4, 2, 2); + builder.push(2, 2, 3, 2, 2); + assert.deepEqual(toArr(builder.build().data), [ + 1, 0, 5, 1, 1, + 0, 10, 4, 2, 2, + 1, 2, 3, 2, 2 + ]); + }); + + test('SemanticTokensBuilder out of order 1', () => { + const builder = new types.SemanticTokensBuilder(); + builder.push(2, 0, 5, 1, 1); + builder.push(2, 10, 1, 2, 2); + builder.push(2, 15, 2, 3, 3); + builder.push(1, 0, 4, 4, 4); + assert.deepEqual(toArr(builder.build().data), [ + 1, 0, 4, 4, 4, + 1, 0, 5, 1, 1, + 0, 10, 1, 2, 2, + 0, 5, 2, 3, 3 + ]); + }); + + test('SemanticTokensBuilder out of order 2', () => { + const builder = new types.SemanticTokensBuilder(); + builder.push(2, 10, 5, 1, 1); + builder.push(2, 2, 4, 2, 2); + assert.deepEqual(toArr(builder.build().data), [ + 2, 2, 4, 2, 2, + 0, 8, 5, 1, 1 + ]); + }); + + test('SemanticTokensBuilder with legend', () => { + const legend = new types.SemanticTokensLegend( + ['aType', 'bType', 'cType', 'dType'], + ['mod0', 'mod1', 'mod2', 'mod3', 'mod4', 'mod5'] + ); + const builder = new types.SemanticTokensBuilder(legend); + builder.push(new types.Range(1, 0, 1, 5), 'bType'); + builder.push(new types.Range(2, 0, 2, 4), 'cType', ['mod0', 'mod5']); + builder.push(new types.Range(3, 0, 3, 3), 'dType', ['mod2', 'mod4']); + assert.deepEqual(toArr(builder.build().data), [ + 1, 0, 5, 1, 0, + 1, 0, 4, 2, 1 | (1 << 5), + 1, 0, 3, 3, (1 << 2) | (1 << 4) + ]); + }); }); diff --git a/src/vs/workbench/test/common/api/semanticTokensDto.test.ts b/src/vs/workbench/test/common/api/semanticTokensDto.test.ts new file mode 100644 index 0000000000..67fd9db295 --- /dev/null +++ b/src/vs/workbench/test/common/api/semanticTokensDto.test.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 * as assert from 'assert'; +import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; + +suite('SemanticTokensDto', () => { + + function toArr(arr: Uint32Array): number[] { + const result: number[] = []; + for (let i = 0, len = arr.length; i < len; i++) { + result[i] = arr[i]; + } + return result; + } + + function assertEqualFull(actual: IFullSemanticTokensDto, expected: IFullSemanticTokensDto): void { + const convert = (dto: IFullSemanticTokensDto) => { + return { + id: dto.id, + type: dto.type, + data: toArr(dto.data) + }; + }; + assert.deepEqual(convert(actual), convert(expected)); + } + + function assertEqualDelta(actual: IDeltaSemanticTokensDto, expected: IDeltaSemanticTokensDto): void { + const convertOne = (delta: { start: number; deleteCount: number; data?: Uint32Array; }) => { + if (!delta.data) { + return delta; + } + return { + start: delta.start, + deleteCount: delta.deleteCount, + data: toArr(delta.data) + }; + }; + const convert = (dto: IDeltaSemanticTokensDto) => { + return { + id: dto.id, + type: dto.type, + deltas: dto.deltas.map(convertOne) + }; + }; + assert.deepEqual(convert(actual), convert(expected)); + } + + function testRoundTrip(value: ISemanticTokensDto): void { + const decoded = decodeSemanticTokensDto(encodeSemanticTokensDto(value)); + if (value.type === 'full' && decoded.type === 'full') { + assertEqualFull(decoded, value); + } else if (value.type === 'delta' && decoded.type === 'delta') { + assertEqualDelta(decoded, value); + } else { + assert.fail('wrong type'); + } + } + + test('full encoding', () => { + testRoundTrip({ + id: 12, + type: 'full', + data: new Uint32Array([(1 << 24) + (2 << 16) + (3 << 8) + 4]) + }); + }); + + test('delta encoding', () => { + testRoundTrip({ + id: 12, + type: 'delta', + deltas: [{ + start: 0, + deleteCount: 4, + data: undefined + }, { + start: 15, + deleteCount: 0, + data: new Uint32Array([(1 << 24) + (2 << 16) + (3 << 8) + 4]) + }, { + start: 27, + deleteCount: 5, + data: new Uint32Array([(1 << 24) + (2 << 16) + (3 << 8) + 4, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + }] + }); + }); + + test('partial array buffer', () => { + const sharedArr = new Uint32Array([ + (1 << 24) + (2 << 16) + (3 << 8) + 4, + 1, 2, 3, 4, 5, (1 << 24) + (2 << 16) + (3 << 8) + 4 + ]); + testRoundTrip({ + id: 12, + type: 'delta', + deltas: [{ + start: 0, + deleteCount: 4, + data: sharedArr.subarray(0, 1) + }, { + start: 15, + deleteCount: 0, + data: sharedArr.subarray(1, sharedArr.length) + }] + }); + }); + +}); diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index 642bfc7f8d..5f717f0b11 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -13,7 +13,7 @@ import { ISearchService } from 'vs/workbench/services/search/common/search'; import { ITelemetryService, ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import * as minimist from 'vscode-minimist'; +import * as minimist from 'minimist'; import * as path from 'vs/base/common/path'; import { LocalSearchService } from 'vs/workbench/services/search/node/searchService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 7d65987ad1..a46d975a01 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -27,7 +27,7 @@ import { IReadTextFileOptions, ITextFileStreamContent, ITextFileService } from ' import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IOpenedWindow, IOpenEmptyWindowOptions, IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/windows'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { LogLevel } from 'vs/platform/log/common/log'; +import { LogLevel, ILogService } from 'vs/platform/log/common/log'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { UTF16le, UTF16be, UTF8_with_bom } from 'vs/base/node/encoding'; @@ -74,7 +74,8 @@ export class TestTextFileService extends NativeTextFileService { @ITextModelService textModelService: ITextModelService, @ICodeEditorService codeEditorService: ICodeEditorService, @IRemotePathService remotePathService: IRemotePathService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @ILogService logService: ILogService ) { super( fileService, @@ -91,7 +92,8 @@ export class TestTextFileService extends NativeTextFileService { textModelService, codeEditorService, remotePathService, - workingCopyFileService + workingCopyFileService, + logService ); } diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index 691947ce28..d094e4528c 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -37,7 +37,7 @@ export class Extensions extends Viewlet { async installExtension(id: string, name: string): Promise { await this.searchForExtension(id); const ariaLabel = `${name}. Press enter for extension details.`; - await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${ariaLabel}"] .extension li[class='action-item'] .extension-action.install`); + await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[aria-label="${ariaLabel}"] .extension-list-item li[class='action-item'] .extension-action.install`); await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`); } } diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index d1723956fc..aa5f3cb306 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import * as cp from 'child_process'; -import * as playwright from 'playwright'; -import * as url from 'url'; -import * as tmp from 'tmp'; -import * as rimraf from 'rimraf'; +import * as path from 'path'; +import * as cp from 'child_process'; +import * as playwright from 'playwright'; +import * as url from 'url'; +import * as tmp from 'tmp'; +import * as rimraf from 'rimraf'; import { URI } from 'vscode-uri'; import * as kill from 'tree-kill'; import * as optimistLib from 'optimist'; @@ -44,7 +44,7 @@ async function runTestsInBrowser(browserType: 'chromium' | 'firefox' | 'webkit', const testFilesUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionTestsPath)).path, protocol, host, slashes: true }); const folderParam = testWorkspaceUri; - const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"]]`; + const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""]]`; await page.goto(`${endpoint.href}&folder=${folderParam}&payload=${payloadParam}`); diff --git a/test/smoke/README.md b/test/smoke/README.md index b02bd727f2..8f1d567c3e 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -7,7 +7,6 @@ Make sure you are on **Node v12.x**. ```bash # Install Dependencies and Compile yarn --cwd test/smoke -yarn --cwd test/automation # Dev (Electron) yarn smoketest diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 98acb5c60f..bd1aaf7c64 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -155,7 +155,7 @@ async function runTestsInBrowser(testModules, browserType) { }); try { - // @ts-ignore + // @ts-expect-error await page.evaluate(modules => loadAndRun(modules), testModules); } catch (err) { console.error(err); diff --git a/yarn.lock b/yarn.lock index 84da05a846..929fdef474 100644 --- a/yarn.lock +++ b/yarn.lock @@ -296,6 +296,11 @@ resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.0.tgz#ca24e6ee6d0df10c003aafe26e93113b8faf0d8e" integrity sha512-cq/NkUUy6rpWD8n7PweNQQBpw2o0cf5v6fbkUVEpOB9VzzIvyPvSEId1/goIj+MciW2v1Lw5mRimKO01XgE9EA== +"@types/minimist@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" + integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= + "@types/mocha@2.2.39": version "2.2.39" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.39.tgz#f68d63db8b69c38e9558b4073525cf96c4f7a829" @@ -6120,6 +6125,11 @@ minimist@1.2.0, minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -9898,11 +9908,6 @@ vscode-debugprotocol@1.40.0-pre.1: resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.40.0-pre.1.tgz#62c066c0520cc5e318dfc9873907574018bc8460" integrity sha512-MLlNUSoJbRPNP/7PpnNLOOubZP/X0ObDEjwC6frrn/GR+bT943S1iIdj9aMjT7i93HSpsIAx8YbwkqId7nxLgw== -vscode-minimist@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/vscode-minimist/-/vscode-minimist-1.2.2.tgz#65403f44f0c6010d259b2271d36eb5c6f4ad8aab" - integrity sha512-DXMNG2QgrXn1jOP12LzjVfvxVkzxv/0Qa27JrMBj/XP2esj+fJ/wP2T4YUH5derj73Lc96dC8F25WyfDUbTpxQ== - vscode-nls-dev@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/vscode-nls-dev/-/vscode-nls-dev-3.3.1.tgz#15fc03e0c9ca5a150abb838690d9554ac06f77e4"