From 1e308639e5717194dcaa85a64b9ba96bb66ee979 Mon Sep 17 00:00:00 2001 From: ADS Merger Date: Wed, 19 Feb 2020 03:11:35 +0000 Subject: [PATCH] Merge from vscode 9bc92b48d945144abb405b9e8df05e18accb9148 --- .eslintrc.json | 2 +- .vscode/launch.json | 2 - .vscode/tasks.json | 14 - .../darwin/product-build-darwin.yml | 2 +- extensions/git/package.json | 203 ++-- extensions/git/package.nls.json | 7 +- extensions/git/src/api/git.d.ts | 1 + extensions/git/src/git.ts | 21 +- extensions/git/src/main.ts | 3 +- extensions/git/src/test/git.test.ts | 6 + extensions/git/yarn.lock | 457 +++++++- .../client/src/jsonMain.ts | 135 +-- .../server/package.json | 2 +- extensions/vscode-account/src/AADHelper.ts | 28 +- extensions/vscode-account/src/extension.ts | 1 - scripts/code.sh | 2 +- src/main.js | 5 +- .../api/browser/mainThreadQueryEditor.ts | 8 +- .../browser/editData/editDataInput.ts | 2 +- .../browser/editData/editDataResultsInput.ts | 5 + .../browser/editor/profiler/dashboardInput.ts | 6 +- .../browser/editor/profiler/profilerInput.ts | 4 + .../browser/modelComponents/modelViewInput.ts | 4 +- .../modelComponents/queryTextEditor.ts | 2 +- src/sql/workbench/browser/taskUtilities.ts | 2 +- .../common/editor/query/queryEditorInput.ts | 4 +- .../common/editor/query/queryResultsInput.ts | 5 + .../electron-browser/commandLine.ts | 2 +- .../common/editorReplacerContribution.ts | 4 +- .../notebook/browser/models/notebookInput.ts | 11 +- .../notebook/browser/notebook.component.ts | 2 +- .../common/models/nodebookInputFactory.ts | 8 +- .../test/browser/notebookInput.test.ts | 2 +- .../contrib/query/browser/flavorStatus.ts | 11 +- .../contrib/query/browser/queryEditor.ts | 6 +- .../contrib/query/common/queryInputFactory.ts | 8 +- .../queryPlan/common/queryPlanInput.ts | 6 +- src/tsconfig.base.json | 1 + src/vs/base/common/glob.ts | 18 +- src/vs/base/{browser => common}/linkedText.ts | 15 +- .../{browser => common}/linkedText.test.ts | 26 +- .../sharedProcess/sharedProcessMain.ts | 2 +- src/vs/code/electron-main/main.ts | 2 +- src/vs/code/node/cliProcessMain.ts | 2 +- .../editor/browser/controller/coreCommands.ts | 4 +- .../browser/view/domLineBreaksComputer.ts | 36 +- .../browser/viewParts/minimap/minimap.ts | 975 ++++++++++++++---- .../viewParts/minimap/minimapCharRenderer.ts | 16 +- .../overviewRuler/decorationsOverviewRuler.ts | 5 +- .../common/config/commonEditorConfig.ts | 9 +- src/vs/editor/common/config/editorOptions.ts | 114 +- src/vs/editor/common/model/textModel.ts | 68 +- src/vs/editor/common/model/textModelEvents.ts | 2 + src/vs/editor/common/model/tokensStore.ts | 27 +- .../modes/languageConfigurationRegistry.ts | 10 + src/vs/editor/common/view/viewEvents.ts | 14 +- .../editor/common/viewModel/viewModelImpl.ts | 12 +- src/vs/editor/contrib/find/findWidget.ts | 5 +- .../smartSelect/test/smartSelect.test.ts | 127 ++- .../contrib/smartSelect/wordSelections.ts | 2 +- .../test/wordOperations.test.ts | 28 + .../contrib/wordOperations/wordOperations.ts | 1 + .../browser/inspectTokens/inspectTokens.ts | 14 +- .../browser/standaloneCodeEditor.ts | 19 +- .../browser/view/minimapCharRenderer.test.ts | 4 +- .../viewLayout/editorLayoutProvider.test.ts | 380 +++++++ src/vs/monaco.d.ts | 22 + .../common/configurationModels.ts | 2 +- .../{node => common}/configurationService.ts | 0 .../test/node/configurationService.test.ts | 2 +- src/vs/platform/files/common/fileService.ts | 24 +- src/vs/platform/files/common/files.ts | 20 +- .../common/inMemoryFilesystemProvider.ts} | 4 +- .../node/watcher/nodejs/watcherService.ts | 4 +- .../files/node/watcher/nsfw/watcherService.ts | 4 +- .../files/node/watcher/unix/watcherService.ts | 4 +- .../node/watcher/win32/watcherService.ts | 6 +- .../files/test/node/diskFileService.test.ts | 48 +- .../files/test/node/normalizer.test.ts | 22 +- .../test/common/instantiationServiceMock.ts | 24 +- src/vs/platform/markers/common/markers.ts | 8 +- .../notification/common/notification.ts | 10 +- src/vs/platform/progress/common/progress.ts | 36 +- .../storage/browser/storageService.ts | 2 +- src/vs/platform/theme/common/colorRegistry.ts | 4 + src/vs/platform/undoRedo/common/undoRedo.ts | 73 ++ .../undoRedo/common/undoRedoService.ts | 241 +++++ .../common/abstractSynchronizer.ts | 2 +- .../userDataSync/common/globalStateSync.ts | 2 +- .../common/userDataAuthTokenService.ts | 7 + .../common/userDataAutoSyncService.ts | 7 + .../userDataSync/common/userDataSync.ts | 23 +- .../userDataSync/common/userDataSyncIpc.ts | 1 + .../common/userDataSyncService.ts | 65 +- .../common/userDataSyncStoreService.ts | 10 +- .../test/common/keybindingsMerge.test.ts | 27 +- .../test/common/userDataSyncClient.ts | 246 +++++ .../test/common/userDataSyncService.test.ts | 555 ++++++++++ .../mainThreadFileSystemEventService.ts | 24 +- .../api/browser/mainThreadLanguageFeatures.ts | 30 +- .../api/browser/mainThreadSaveParticipant.ts | 376 +------ .../workbench/api/browser/mainThreadSearch.ts | 10 +- .../api/browser/mainThreadWebview.ts | 4 +- .../api/browser/viewsExtensionPoint.ts | 11 +- .../workbench/api/common/extHost.protocol.ts | 7 + .../api/common/extHostExtensionService.ts | 2 +- .../api/common/extHostLanguageFeatures.ts | 6 + .../api/common/extHostTerminalService.ts | 14 +- .../api/node/extHostTerminalService.ts | 4 +- src/vs/workbench/browser/dnd.ts | 4 +- src/vs/workbench/browser/labels.ts | 2 +- src/vs/workbench/browser/layout.ts | 69 +- src/vs/workbench/browser/part.ts | 2 +- .../workbench/browser/parts/compositeBar.ts | 4 +- .../browser/parts/editor/baseEditor.ts | 4 +- .../parts/editor/breadcrumbsControl.ts | 4 +- .../parts/editor/editor.contribution.ts | 2 +- .../browser/parts/editor/editorGroupView.ts | 4 +- .../browser/parts/editor/rangeDecorations.ts | 2 +- .../browser/parts/editor/textDiffEditor.ts | 4 +- .../browser/parts/editor/textEditor.ts | 12 +- .../parts/editor/textResourceEditor.ts | 6 +- .../notifications/notificationsAlerts.ts | 16 +- .../notifications/notificationsCenter.ts | 4 +- .../parts/notifications/notificationsList.ts | 2 +- .../notifications/notificationsStatus.ts | 8 +- .../notifications/notificationsToasts.ts | 10 +- .../notifications/notificationsViewer.ts | 41 +- .../browser/parts/panel/panelPart.ts | 2 +- .../parts/quickopen/quickOpenController.ts | 2 +- .../browser/parts/views/media/views.css | 8 +- .../browser/parts/views/viewPaneContainer.ts | 144 ++- src/vs/workbench/browser/viewlet.ts | 2 +- src/vs/workbench/browser/web.main.ts | 2 +- src/vs/workbench/browser/workbench.ts | 8 +- src/vs/workbench/common/editor.ts | 37 +- .../common/editor/textEditorModel.ts | 2 +- src/vs/workbench/common/notifications.ts | 141 ++- src/vs/workbench/common/resources.ts | 6 +- src/vs/workbench/common/views.ts | 36 +- .../contrib/backup/common/backupRestorer.ts | 2 +- .../electron-browser/backupRestorer.test.ts | 2 +- .../electron-browser/backupTracker.test.ts | 4 +- .../bulkEdit/browser/bulkEdit.contribution.ts | 4 +- .../test/browser/bulkEditPreview.test.ts | 2 +- .../browser/codeEditor.contribution.ts | 1 + .../codeEditor/browser/saveParticipants.ts | 342 ++++++ .../test/browser/saveParticipant.test.ts} | 4 +- .../contrib/comments/browser/commentsView.ts | 2 +- .../contrib/customEditor/browser/commands.ts | 4 +- .../customEditor/browser/customEditorInput.ts | 17 +- .../browser/customEditorInputFactory.ts | 2 +- .../customEditor/browser/customEditors.ts | 18 +- .../contrib/debug/browser/debugService.ts | 7 +- .../contrib/debug/browser/debugSession.ts | 38 +- .../contrib/debug/browser/rawDebugSession.ts | 2 +- .../workbench/contrib/debug/common/debug.ts | 3 - .../contrib/debug/common/debugModel.ts | 9 - .../contrib/debug/common/debugProtocol.d.ts | 12 +- .../debug/test/browser/breakpoints.test.ts | 2 +- .../debug/test/browser/callStack.test.ts | 4 +- .../contrib/debug/test/common/mockDebug.ts | 2 - .../debugANSIHandling.test.ts | 2 +- .../experiments/common/experimentService.ts | 122 ++- .../experimentService.test.ts | 253 ++++- .../browser/extensions.contribution.ts | 27 +- .../extensions/common/extensionsInput.ts | 12 +- .../runtimeExtensionsInput.ts | 12 +- .../browser/editors/fileEditorTracker.ts | 12 +- .../files/browser/editors/textFileEditor.ts | 18 +- .../editors/textFileSaveErrorHandler.ts | 6 +- .../contrib/files/browser/fileActions.ts | 29 +- .../contrib/files/browser/fileCommands.ts | 2 +- .../files/browser/files.contribution.ts | 2 +- .../files/browser/media/explorerviewlet.css | 1 - .../files/browser/media/fileactions.css | 2 +- .../files/browser/views/explorerViewer.ts | 12 +- .../files/browser/views/openEditorsView.ts | 2 +- .../files/common/editors/fileEditorInput.ts | 131 ++- .../contrib/files/common/explorerService.ts | 10 +- .../workbench/contrib/files/common/files.ts | 2 +- .../fileActions.contribution.ts | 2 +- .../test/browser/fileEditorInput.test.ts | 24 +- .../contrib/logs/common/logs.contribution.ts | 2 +- .../contrib/markers/browser/markersView.ts | 2 +- .../contrib/output/browser/logViewer.ts | 6 +- .../browser/preferences.contribution.ts | 2 +- .../preferences/browser/preferencesEditor.ts | 6 +- .../common/preferencesContribution.ts | 2 +- .../workbench/contrib/scm/browser/activity.ts | 2 +- .../contrib/scm/browser/media/scmViewlet.css | 11 - .../contrib/scm/browser/repositoryPane.ts | 2 +- .../contrib/scm/browser/scmViewlet.ts | 86 +- .../contrib/search/browser/searchActions.ts | 2 +- .../contrib/search/browser/searchView.ts | 2 +- .../browser/searchEditor.contribution.ts | 2 +- .../searchEditor/browser/searchEditor.ts | 2 +- .../searchEditor/browser/searchEditorInput.ts | 6 +- .../snippets/browser/snippetsService.ts | 4 +- .../userDataSync/browser/userDataSync.ts | 20 +- .../browser/userDataSyncTrigger.ts | 2 +- .../webview/browser/webviewEditorInput.ts | 12 +- .../common/viewsWelcome.contribution.ts | 25 + .../common/viewsWelcomeContribution.ts | 58 ++ .../common/viewsWelcomeExtensionPoint.ts | 53 + .../walkThrough/browser/walkThroughInput.ts | 8 +- .../walkThrough/browser/walkThroughPart.ts | 2 +- .../node/accessibilityService.ts | 8 +- .../bulkEdit/browser/bulkEditService.ts | 6 +- .../services/bulkEdit/browser/conflicts.ts | 2 +- .../configuration/browser/configuration.ts | 6 +- .../services/editor/browser/editorService.ts | 4 +- .../test/browser/editorGroupsService.test.ts | 5 +- .../editor/test/browser/editorService.test.ts | 3 +- .../test/browser/editorsObserver.test.ts | 1 - .../extensions/browser/extensionUrlHandler.ts | 42 +- .../services/history/browser/history.ts | 12 +- .../history/test/browser/history.test.ts | 3 +- .../keybinding/browser/keybindingService.ts | 2 +- .../keybinding/browser/keymapService.ts | 2 +- .../keybindingEditing.test.ts | 3 + .../outputChannelModelService.ts | 2 +- .../preferences/browser/preferencesService.ts | 4 +- .../common/preferencesEditorInput.ts | 9 +- .../browser/media/progressService.css | 8 - .../progress/browser/progressService.ts | 157 ++- .../textfile/browser/textFileService.ts | 212 +--- .../textfile/common/textFileEditorModel.ts | 28 +- .../common/textFileEditorModelManager.ts | 187 +++- .../common/textFileSaveParticipant.ts | 69 ++ .../services/textfile/common/textfiles.ts | 99 +- .../electron-browser/nativeTextFileService.ts | 4 +- .../test/browser/textFileEditorModel.test.ts | 86 +- .../textFileEditorModelManager.test.ts | 12 + .../test/browser/textFileService.test.ts | 84 +- .../browser/textModelResolverService.test.ts | 2 +- .../themes/browser/workbenchThemeService.ts | 2 +- .../test/browser/untitledTextEditor.test.ts | 38 +- .../fileUserDataProvider.test.ts | 20 +- .../userDataAuthTokenService.ts | 8 + .../common/workingCopyFileService.ts | 237 +++++ .../browser/workingCopyFileService.test.ts | 168 +++ .../api/mainThreadDocumentsAndEditors.test.ts | 2 +- .../browser/api/mainThreadEditors.test.ts | 25 +- .../browser/parts/editor/baseEditor.test.ts | 12 +- .../test/browser/parts/editor/editor.test.ts | 28 +- .../browser/parts/editor/editorGroups.test.ts | 9 +- .../browser/parts/editor/editorInput.test.ts | 2 + .../parts/editor/rangeDecorations.test.ts | 2 +- .../test/browser/workbenchTestServices.ts | 38 +- .../test/common/notifications.test.ts | 51 +- .../electron-browser/workbenchTestServices.ts | 3 - src/vs/workbench/workbench.common.main.ts | 4 + 253 files changed, 6414 insertions(+), 2296 deletions(-) rename src/vs/base/{browser => common}/linkedText.ts (79%) rename src/vs/base/test/{browser => common}/linkedText.test.ts (77%) rename src/vs/platform/configuration/{node => common}/configurationService.ts (100%) rename src/vs/{workbench/services/userData/common/inMemoryUserDataProvider.ts => platform/files/common/inMemoryFilesystemProvider.ts} (98%) create mode 100644 src/vs/platform/undoRedo/common/undoRedo.ts create mode 100644 src/vs/platform/undoRedo/common/undoRedoService.ts create mode 100644 src/vs/platform/userDataSync/test/common/userDataSyncClient.ts create mode 100644 src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts create mode 100644 src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts rename src/vs/workbench/{test/electron-browser/api/mainThreadSaveParticipant.test.ts => contrib/codeEditor/test/browser/saveParticipant.test.ts} (98%) create mode 100644 src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts create mode 100644 src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts create mode 100644 src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts create mode 100644 src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts create mode 100644 src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts create mode 100644 src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts diff --git a/.eslintrc.json b/.eslintrc.json index 7cd1dedda8..65822e432a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -193,7 +193,7 @@ "typemoq", "sinon", "vs/nls", - "azdata", + "azdata", "**/{vs,sql}/base/common/**", "**/{vs,sql}/base/test/common/**", "**/{vs,sql}/base/parts/*/common/**", diff --git a/.vscode/launch.json b/.vscode/launch.json index e3ad9c874e..e22fd1be1b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,8 +16,6 @@ "request": "attach", "name": "Attach to Extension Host", "port": 5870, - "timeout": 30000, - "restart": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" ] diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 880b2e95f4..eafc311235 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -31,20 +31,6 @@ } } }, - { - "type": "npm", - "script": "strict-function-types-watch", - "label": "TS - Strict Function Types", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "problemMatcher": { - "base": "$tsc-watch", - "owner": "typescript-function-types", - "applyTo": "allDocuments" - } - }, { "type": "npm", "script": "strict-vscode-watch", diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 6be05972f7..795bc78556 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -207,7 +207,7 @@ steps: "toolVersion": "1.0" } ] - SessionTimeout: 120 + SessionTimeout: 60 displayName: Notarization - script: | diff --git a/extensions/git/package.json b/extensions/git/package.json index 9d6d4f43f3..5e8bf47d71 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -467,57 +467,61 @@ ], "menus": { "commandPalette": [ + { + "command": "git.setLogLevel", + "when": "config.git.enabled && !git.missing" + }, { "command": "git.clone", - "when": "config.git.enabled" + "when": "config.git.enabled && !git.missing" }, { "command": "git.init", - "when": "config.git.enabled" + "when": "config.git.enabled && !git.missing" }, { "command": "git.openRepository", - "when": "config.git.enabled" + "when": "config.git.enabled && !git.missing" }, { "command": "git.close", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.refresh", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.openFile", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.openHEADFile", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.openChange", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stage", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stageAll", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stageAllTracked", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stageAllUntracked", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stageSelectedRanges", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stageChange", @@ -525,7 +529,7 @@ }, { "command": "git.revertSelectedRanges", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.revertChange", @@ -537,51 +541,63 @@ }, { "command": "git.unstage", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.unstageAll", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.unstageSelectedRanges", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.clean", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.cleanAll", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, + { + "command": "git.cleanAllTracked", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, + { + "command": "git.cleanAllUntracked", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.commit", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.commitStaged", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, + { + "command": "git.commitEmpty", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.commitStagedSigned", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.commitStagedAmend", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.commitAll", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.commitAllSigned", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.commitAllAmend", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.restoreCommitTemplate", @@ -593,111 +609,111 @@ }, { "command": "git.undoCommit", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.checkout", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.branch", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.branchFrom", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.deleteBranch", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.renameBranch", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.pull", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.pullFrom", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.pullRebase", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.pullFrom", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.merge", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.createTag", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.deleteTag", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.fetch", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.fetchPrune", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.fetchAll", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.push", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.pushForce", - "when": "config.git.enabled && config.git.allowForcePush && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && config.git.allowForcePush && gitOpenRepositoryCount != 0" }, { "command": "git.pushTo", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.pushToForce", - "when": "config.git.enabled && config.git.allowForcePush && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && config.git.allowForcePush && gitOpenRepositoryCount != 0" }, { "command": "git.pushWithTags", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.pushWithTagsForce", - "when": "config.git.enabled && config.git.allowForcePush && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && config.git.allowForcePush && gitOpenRepositoryCount != 0" }, { "command": "git.addRemote", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.removeRemote", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.sync", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.syncRebase", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.publish", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.showOutput", @@ -705,35 +721,35 @@ }, { "command": "git.ignore", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stashIncludeUntracked", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stash", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stashPop", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stashPopLatest", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stashApply", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stashApplyLatest", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.stashDrop", - "when": "config.git.enabled && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, { "command": "git.timeline.openDiff", @@ -749,11 +765,6 @@ } ], "scm/title": [ - { - "command": "git.init", - "group": "navigation", - "when": "config.git.enabled && !scmProvider && gitOpenRepositoryCount == 0 && workspaceFolderCount != 0" - }, { "command": "git.commit", "group": "navigation", @@ -1227,76 +1238,76 @@ { "command": "git.openFile", "group": "navigation", - "when": "config.git.enabled && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" }, { "command": "git.openChange", "group": "navigation", - "when": "config.git.enabled && gitOpenRepositoryCount != 0 && !isInDiffEditor && resourceScheme == file" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && resourceScheme == file" }, { "command": "git.stageSelectedRanges", "group": "2_git@1", - "when": "config.git.enabled && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" }, { "command": "git.unstageSelectedRanges", "group": "2_git@2", - "when": "config.git.enabled && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" }, { "command": "git.revertSelectedRanges", "group": "2_git@3", - "when": "config.git.enabled && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" } ], "editor/context": [ { "command": "git.stageSelectedRanges", "group": "2_git@1", - "when": "isInDiffRightEditor && config.git.enabled && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "isInDiffRightEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" }, { "command": "git.unstageSelectedRanges", "group": "2_git@2", - "when": "isInDiffRightEditor && config.git.enabled && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "isInDiffRightEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" }, { "command": "git.revertSelectedRanges", "group": "2_git@3", - "when": "isInDiffRightEditor && config.git.enabled && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" + "when": "isInDiffRightEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" } ], "scm/change/title": [ { "command": "git.stageChange", - "when": "originalResourceScheme == git" + "when": "config.git.enabled && !git.missing && originalResourceScheme == git" }, { "command": "git.revertChange", - "when": "originalResourceScheme == git" + "when": "config.git.enabled && !git.missing && originalResourceScheme == git" } ], "timeline/item/context": [ { "command": "git.timeline.openDiff", "group": "inline", - "when": "timelineItem =~ /git:file\\b/" + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file\\b/" }, { "command": "git.timeline.openDiff", "group": "1_timeline", - "when": "timelineItem =~ /git:file\\b/" + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file\\b/" }, { "command": "git.timeline.copyCommitId", "group": "2_timeline@1", - "when": "timelineItem =~ /git:file:commit\\b/" + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/" }, { "command": "git.timeline.copyCommitMessage", "group": "2_timeline@2", - "when": "timelineItem =~ /git:file:commit\\b/" + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/" } ] }, @@ -1811,7 +1822,34 @@ 72 ] } - } + }, + "viewsWelcome": [ + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.disabled%", + "when": "!config.git.enabled" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.missing%", + "when": "config.git.enabled && git.missing" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.empty%", + "when": "config.git.enabled && !git.missing && workbenchState == empty" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.folder%", + "when": "config.git.enabled && !git.missing && workbenchState == folder" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.workspace%", + "when": "config.git.enabled && !git.missing && workbenchState == workspace" + } + ] }, "dependencies": { "byline": "^5.0.0", @@ -1829,11 +1867,10 @@ "@types/file-type": "^5.2.1", "@types/mocha": "2.2.43", "@types/node": "^12.11.7", - "@types/vscode": "^1.42", "@types/which": "^1.0.28", "mocha": "^3.2.0", "mocha-junit-reporter": "^1.23.3", "mocha-multi-reporters": "^1.1.7", - "vscode-test": "^1.3.0" + "vscode": "^1.1.36" } } diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 534ec69429..53325c0f5f 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -149,5 +149,10 @@ "colors.untracked": "Color for untracked resources.", "colors.ignored": "Color for ignored resources.", "colors.conflict": "Color for resources with conflicts.", - "colors.submodule": "Color for submodule resources." + "colors.submodule": "Color for submodule resources.", + "view.workbench.scm.missing": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use Git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.", + "view.workbench.scm.disabled": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone from URL](command:git.clone)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.folder": "The folder currently open doesn't have a git repository.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.workspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm)." } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 72b986ee7b..fc0a057925 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -44,6 +44,7 @@ export interface Commit { readonly authorDate?: Date; readonly authorName?: string; readonly authorEmail?: string; + readonly commitDate?: Date; } export interface Submodule { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index d04684d7d7..e25bd42374 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -333,7 +333,7 @@ function sanitizePath(path: string): string { return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); } -const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%P%n%B'; +const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%B'; export class Git { @@ -525,6 +525,7 @@ export interface Commit { authorDate?: Date; authorName?: string; authorEmail?: string; + commitDate?: Date; } export class GitStatusParser { @@ -655,15 +656,16 @@ export function parseGitmodules(raw: string): Submodule[] { return result; } -const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm; +const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm; export function parseGitCommits(data: string): Commit[] { let commits: Commit[] = []; let ref; - let name; - let email; - let date; + let authorName; + let authorEmail; + let authorDate; + let commitDate; let parents; let message; let match; @@ -674,7 +676,7 @@ export function parseGitCommits(data: string): Commit[] { break; } - [, ref, name, email, date, parents, message] = match; + [, ref, authorName, authorEmail, authorDate, commitDate, parents, message] = match; if (message[message.length - 1] === '\n') { message = message.substr(0, message.length - 1); @@ -685,9 +687,10 @@ export function parseGitCommits(data: string): Commit[] { hash: ` ${ref}`.substr(1), message: ` ${message}`.substr(1), parents: parents ? parents.split(' ') : [], - authorDate: new Date(Number(date) * 1000), - authorName: ` ${name}`.substr(1), - authorEmail: ` ${email}`.substr(1) + authorDate: new Date(Number(authorDate) * 1000), + authorName: ` ${authorName}`.substr(1), + authorEmail: ` ${authorEmail}`.substr(1), + commitDate: new Date(Number(commitDate) * 1000), }); } while (true); diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index b3f3ae7fdc..2bb2e81904 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -178,7 +178,8 @@ export async function activate(context: ExtensionContext): Promise // console.warn(err.message); // outputChannel.appendLine(err.message); - // warnAboutMissingGit(); + commands.executeCommand('setContext', 'git.missing', true); + // warnAboutMissingGit(); {{SQL CARBON EDIT}} turn-off Git missing prompt return new GitExtensionImpl(); } diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index c2090574ab..8c18a6ddfc 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -192,6 +192,7 @@ suite('git', () => { John Doe john.doe@mail.com 1580811030 +1580811031 8e5a374372b8393906c7e380dbb09349c5385554 This is a commit message.\x00`; @@ -202,6 +203,7 @@ This is a commit message.\x00`; authorDate: new Date(1580811030000), authorName: 'John Doe', authorEmail: 'john.doe@mail.com', + commitDate: new Date(1580811031000), }]); }); @@ -210,6 +212,7 @@ This is a commit message.\x00`; John Doe john.doe@mail.com 1580811030 +1580811031 8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 This is a commit message.\x00`; @@ -220,6 +223,7 @@ This is a commit message.\x00`; authorDate: new Date(1580811030000), authorName: 'John Doe', authorEmail: 'john.doe@mail.com', + commitDate: new Date(1580811031000), }]); }); @@ -228,6 +232,7 @@ This is a commit message.\x00`; John Doe john.doe@mail.com 1580811030 +1580811031 This is a commit message.\x00`; @@ -238,6 +243,7 @@ This is a commit message.\x00`; authorDate: new Date(1580811030000), authorName: 'John Doe', authorEmail: 'john.doe@mail.com', + commitDate: new Date(1580811031000), }]); }); }); diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 0ae8237610..ac135e43a8 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -31,11 +31,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== -"@types/vscode@^1.42": - version "1.42.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.42.0.tgz#0ad891a9487e91e34be7c56985058a179031eb76" - integrity sha512-ds6TceMsh77Fs0Mq0Vap6Y72JbGWB8Bay4DrnJlf5d9ui2RSe1wis13oQm+XhguOeH1HUfLGzaDAoupTUtgabw== - "@types/which@^1.0.28": version "1.0.28" resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6" @@ -48,6 +43,16 @@ agent-base@4, agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +ajv@^6.5.5: + version "6.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" + integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" @@ -62,11 +67,45 @@ applicationinsights@1.0.8: diagnostic-channel-publishers "0.2.1" zone.js "0.7.6" +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -80,16 +119,43 @@ browser-stdout@1.3.0: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8= +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== + commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -102,11 +168,23 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + dayjs@1.8.19: version "1.8.19" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.19.tgz#5117dc390d8f8e586d53891dbff3fa308f51abfe" @@ -140,6 +218,11 @@ debug@^3.1.0: dependencies: ms "^2.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + diagnostic-channel-publishers@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" @@ -157,6 +240,19 @@ diff@3.2.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" integrity sha1-yc45Okt8vQsFinJck98pkCeGj/k= +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -174,16 +270,62 @@ escape-string-regexp@1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + file-type@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-7.2.0.tgz#113cfed52e1d6959ab80248906e2f25a8cdccb74" integrity sha1-ETz+1S4daVmrgCSJBuLyWozcy3Q= +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + glob@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" @@ -196,7 +338,19 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3: +glob@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.2: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -213,16 +367,39 @@ glob@^7.1.3: resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8= +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -236,7 +413,16 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -https-proxy-agent@^2.2.4: +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-proxy-agent@^2.2.1: version "2.2.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== @@ -269,21 +455,61 @@ is-buffer@~1.1.1: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + jschardet@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.1.tgz#af6f8fd0b3b0f5d46a8fd9614a4fce490575c184" integrity sha512-pA5qG9Zwm8CBpGlK/lo2GE9jPxwqRgMV7Lzc/1iaPccw6v4Rhj8Zg2BTyrdmHmxlJojnbLupLeRnaPLsq03x6Q== +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + json3@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + lodash._baseassign@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" @@ -354,7 +580,19 @@ md5@^2.1.0: crypt "~0.0.1" is-buffer "~1.1.1" -minimatch@^3.0.2, minimatch@^3.0.4: +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + +minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -410,6 +648,23 @@ mocha@^3.2.0: mkdirp "0.5.1" supports-color "3.1.2" +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -420,6 +675,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -432,14 +692,68 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -"safer-buffer@>= 2.1.2 < 3": +psl@^1.1.28: + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + +request@^2.88.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -449,6 +763,39 @@ semver@^5.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== +semver@^5.4.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +source-map-support@^0.5.0: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" @@ -463,6 +810,62 @@ supports-color@3.1.2: dependencies: has-flag "^1.0.0" +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== + dependencies: + has-flag "^3.0.0" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +url-parse@^1.4.4: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + vscode-extension-telemetry@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b" @@ -475,20 +878,32 @@ vscode-nls@^4.0.0: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== -vscode-test@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-1.3.0.tgz#3310ab385d9b887b4c82e8f52be1030e7cf9493d" - integrity sha512-LddukcBiSU2FVTDr3c1D8lwkiOvwlJdDL2hqVbn6gIz+rpTqUCkMZSKYm94Y1v0WXlHSDQBsXyY+tchWQgGVsw== +vscode-test@^0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-0.4.3.tgz#461ebf25fc4bc93d77d982aed556658a2e2b90b8" + integrity sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w== dependencies: http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.4" - rimraf "^2.6.3" + https-proxy-agent "^2.2.1" vscode-uri@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.0.0.tgz#2df704222f72b8a71ff266ba0830ed6c51ac1542" integrity sha512-lWXWofDSYD8r/TIyu64MdwB4FaSirQ608PP/TzUyslyOeHGwQ0eTHUZeJrK1ILOmwUHaJtV693m2JoUYroUDpw== +vscode@^1.1.36: + version "1.1.36" + resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.36.tgz#5e1a0d1bf4977d0c7bc5159a9a13d5b104d4b1b6" + integrity sha512-cGFh9jmGLcTapCpPCKvn8aG/j9zVQ+0x5hzYJq5h5YyUXVGa1iamOaB2M2PZXoumQPES4qeAP1FwkI0b6tL4bQ== + dependencies: + glob "^7.1.2" + mocha "^5.2.0" + request "^2.88.0" + semver "^5.4.1" + source-map-support "^0.5.0" + url-parse "^1.4.4" + vscode-test "^0.4.1" + which@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index ba4013e1be..94450bb6e1 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -76,29 +76,29 @@ let telemetryReporter: TelemetryReporter | undefined; export function activate(context: ExtensionContext) { - let toDispose = context.subscriptions; + const toDispose = context.subscriptions; let rangeFormatting: Disposable | undefined = undefined; - let packageInfo = getPackageInfo(context); + const packageInfo = getPackageInfo(context); telemetryReporter = packageInfo && new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); - let serverMain = readJSONFile(context.asAbsolutePath('./server/package.json')).main; - let serverModule = context.asAbsolutePath(path.join('server', serverMain)); + const serverMain = readJSONFile(context.asAbsolutePath('./server/package.json')).main; + const serverModule = context.asAbsolutePath(path.join('server', serverMain)); // The debug options for the server - let debugOptions = { execArgv: ['--nolazy', '--inspect=' + (9000 + Math.round(Math.random() * 10000))] }; + const debugOptions = { execArgv: ['--nolazy', '--inspect=' + (9000 + Math.round(Math.random() * 10000))] }; // If the extension is launch in debug mode the debug server options are use // Otherwise the run options are used - let serverOptions: ServerOptions = { + const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc }, debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; - let documentSelector = ['json', 'jsonc']; + const documentSelector = ['json', 'jsonc']; - let schemaResolutionErrorStatusBarItem = window.createStatusBarItem({ + const schemaResolutionErrorStatusBarItem = window.createStatusBarItem({ id: 'status.json.resolveError', name: localize('json.resolveError', "JSON: Schema Resolution Error"), alignment: StatusBarAlignment.Right, @@ -109,10 +109,10 @@ export function activate(context: ExtensionContext) { schemaResolutionErrorStatusBarItem.text = '$(alert)'; toDispose.push(schemaResolutionErrorStatusBarItem); - let fileSchemaErrors = new Map(); + const fileSchemaErrors = new Map(); // Options to control the language client - let clientOptions: LanguageClientOptions = { + const clientOptions: LanguageClientOptions = { // Register the server for json documents documentSelector, initializationOptions: { @@ -172,17 +172,17 @@ export function activate(context: ExtensionContext) { }; // Create the language client and start the client. - let client = new LanguageClient('json', localize('jsonserver.name', 'JSON Language Server'), serverOptions, clientOptions); + const client = new LanguageClient('json', localize('jsonserver.name', 'JSON Language Server'), serverOptions, clientOptions); client.registerProposedFeatures(); - let disposable = client.start(); + const disposable = client.start(); toDispose.push(disposable); client.onReady().then(() => { const schemaDocuments: { [uri: string]: boolean } = {}; // handle content request client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { - let uri = Uri.parse(uriPath); + const uri = Uri.parse(uriPath); if (uri.scheme !== 'http' && uri.scheme !== 'https') { return workspace.openTextDocument(uri).then(doc => { schemaDocuments[uri.toString()] = true; @@ -212,7 +212,7 @@ export function activate(context: ExtensionContext) { } }); - let handleContentChange = (uriString: string) => { + const handleContentChange = (uriString: string) => { if (schemaDocuments[uriString]) { client.sendNotification(SchemaContentChangeNotification.type, uriString); return true; @@ -220,7 +220,7 @@ export function activate(context: ExtensionContext) { return false; }; - let handleActiveEditorChange = (activeEditor?: TextEditor) => { + const handleActiveEditorChange = (activeEditor?: TextEditor) => { if (!activeEditor) { return; } @@ -244,7 +244,7 @@ export function activate(context: ExtensionContext) { })); toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); - let handleRetryResolveSchemaCommand = () => { + const handleRetryResolveSchemaCommand = () => { if (window.activeTextEditor) { schemaResolutionErrorStatusBarItem.text = '$(watch)'; const activeDocUri = window.activeTextEditor.document.uri.toString(); @@ -282,7 +282,7 @@ export function activate(context: ExtensionContext) { }); - let languageConfiguration: LanguageConfiguration = { + const languageConfiguration: LanguageConfiguration = { wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/, indentationRules: { increaseIndentPattern: /({+(?=([^"]*"[^"]*")*[^"}]*$))|(\[+(?=([^"]*"[^"]*")*[^"\]]*$))/, @@ -300,7 +300,7 @@ export function activate(context: ExtensionContext) { } else if (formatEnabled && !rangeFormatting) { rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, { provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult { - let params: DocumentRangeFormattingParams = { + const params: DocumentRangeFormattingParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range: client.code2ProtocolConverter.asRange(range), options: client.code2ProtocolConverter.asFormattingOptions(options) @@ -325,11 +325,11 @@ export function deactivate(): Promise { } function getSchemaAssociation(_context: ExtensionContext): ISchemaAssociations { - let associations: ISchemaAssociations = {}; + const associations: ISchemaAssociations = {}; extensions.all.forEach(extension => { - let packageJSON = extension.packageJSON; + const packageJSON = extension.packageJSON; if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation) { - let jsonValidation = packageJSON.contributes.jsonValidation; + const jsonValidation = packageJSON.contributes.jsonValidation; if (Array.isArray(jsonValidation)) { jsonValidation.forEach(jv => { let { fileMatch, url } = jv; @@ -359,11 +359,11 @@ function getSchemaAssociation(_context: ExtensionContext): ISchemaAssociations { } function getSettings(): Settings { - let httpSettings = workspace.getConfiguration('http'); + const httpSettings = workspace.getConfiguration('http'); - let resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get('json.maxItemsComputed')))) || 5000; + const resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get('json.maxItemsComputed')))) || 5000; - let settings: Settings = { + const settings: Settings = { http: { proxy: httpSettings.get('proxy'), proxyStrictSSL: httpSettings.get('proxyStrictSSL') @@ -373,10 +373,18 @@ function getSettings(): Settings { resultLimit } }; - let schemaSettingsById: { [schemaId: string]: JSONSchemaSettings } = Object.create(null); - let collectSchemaSettings = (schemaSettings: JSONSchemaSettings[], rootPath?: string, fileMatchPrefix?: string) => { - for (let setting of schemaSettings) { - let url = getSchemaId(setting, rootPath); + const schemaSettingsById: { [schemaId: string]: JSONSchemaSettings } = Object.create(null); + const collectSchemaSettings = (schemaSettings: JSONSchemaSettings[], folderUri?: Uri, isMultiRoot?: boolean) => { + + let fileMatchPrefix = undefined; + if (folderUri && isMultiRoot) { + fileMatchPrefix = folderUri.toString(); + if (fileMatchPrefix[fileMatchPrefix.length - 1] === '/') { + fileMatchPrefix = fileMatchPrefix.substr(0, fileMatchPrefix.length - 1); + } + } + for (const setting of schemaSettings) { + const url = getSchemaId(setting, folderUri); if (!url) { continue; } @@ -385,69 +393,78 @@ function getSettings(): Settings { schemaSetting = schemaSettingsById[url] = { url, fileMatch: [] }; settings.json!.schemas!.push(schemaSetting); } - let fileMatches = setting.fileMatch; - let resultingFileMatches = schemaSetting.fileMatch!; + const fileMatches = setting.fileMatch; if (Array.isArray(fileMatches)) { - if (fileMatchPrefix) { - for (let fileMatch of fileMatches) { - if (fileMatch[0] === '/') { - resultingFileMatches.push(fileMatchPrefix + fileMatch); - resultingFileMatches.push(fileMatchPrefix + '/*' + fileMatch); - } else { - resultingFileMatches.push(fileMatchPrefix + '/' + fileMatch); - resultingFileMatches.push(fileMatchPrefix + '/*/' + fileMatch); - } + const resultingFileMatches = schemaSetting.fileMatch || []; + schemaSetting.fileMatch = resultingFileMatches; + const addMatch = (pattern: string) => { // filter duplicates + if (resultingFileMatches.indexOf(pattern) === -1) { + resultingFileMatches.push(pattern); + } + }; + for (const fileMatch of fileMatches) { + if (fileMatchPrefix) { + if (fileMatch[0] === '/') { + addMatch(fileMatchPrefix + fileMatch); + addMatch(fileMatchPrefix + '/*' + fileMatch); + } else { + addMatch(fileMatchPrefix + '/' + fileMatch); + addMatch(fileMatchPrefix + '/*/' + fileMatch); + } + } else { + addMatch(fileMatch); } - } else { - resultingFileMatches.push(...fileMatches); } - } - if (setting.schema) { + if (setting.schema && !schemaSetting.schema) { schemaSetting.schema = setting.schema; } } }; + const folders = workspace.workspaceFolders; + // merge global and folder settings. Qualify all file matches with the folder path. - let globalSettings = workspace.getConfiguration('json', null).get('schemas'); + const globalSettings = workspace.getConfiguration('json', null).get('schemas'); if (Array.isArray(globalSettings)) { - collectSchemaSettings(globalSettings, workspace.rootPath); + if (!folders) { + collectSchemaSettings(globalSettings); + } } - let folders = workspace.workspaceFolders; if (folders) { - for (let folder of folders) { - let folderUri = folder.uri; + const isMultiRoot = folders.length > 1; + for (const folder of folders) { + const folderUri = folder.uri; - let schemaConfigInfo = workspace.getConfiguration('json', folderUri).inspect('schemas'); + const schemaConfigInfo = workspace.getConfiguration('json', folderUri).inspect('schemas'); - let folderSchemas = schemaConfigInfo!.workspaceFolderValue; + const folderSchemas = schemaConfigInfo!.workspaceFolderValue; if (Array.isArray(folderSchemas)) { - let folderPath = folderUri.toString(); - if (folderPath[folderPath.length - 1] === '/') { - folderPath = folderPath.substr(0, folderPath.length - 1); - } - collectSchemaSettings(folderSchemas, folderUri.fsPath, folderPath); + collectSchemaSettings(folderSchemas, folderUri, isMultiRoot); } + if (Array.isArray(globalSettings)) { + collectSchemaSettings(globalSettings, folderUri, isMultiRoot); + } + } } return settings; } -function getSchemaId(schema: JSONSchemaSettings, rootPath?: string) { +function getSchemaId(schema: JSONSchemaSettings, folderUri?: Uri) { let url = schema.url; if (!url) { if (schema.schema) { url = schema.schema.id || `vscode://schemas/custom/${encodeURIComponent(hash(schema.schema).toString(16))}`; } - } else if (rootPath && (url[0] === '.' || url[0] === '/')) { - url = Uri.file(path.normalize(path.join(rootPath, url))).toString(); + } else if (folderUri && (url[0] === '.' || url[0] === '/')) { + url = folderUri.with({ path: path.posix.join(folderUri.path, url) }).toString(); } return url; } function getPackageInfo(context: ExtensionContext): IPackageInfo | undefined { - let extensionPackage = readJSONFile(context.asAbsolutePath('./package.json')); + const extensionPackage = readJSONFile(context.asAbsolutePath('./package.json')); if (extensionPackage) { return { name: extensionPackage.name, diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 0c26777f01..a21b05e4ab 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -1,7 +1,7 @@ { "name": "vscode-json-languageserver", "description": "JSON language server", - "version": "1.2.2", + "version": "1.2.3", "author": "Microsoft Corporation", "license": "MIT", "engines": { diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 9146510fc9..f2796a1a93 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -21,6 +21,7 @@ interface IToken { accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined expiresIn?: string; // How long access token is valid, in seconds + expiresAt?: number; // UNIX epoch time at which token will expire refreshToken: string; accountName: string; @@ -183,12 +184,33 @@ export class AzureActiveDirectoryService { private convertToSession(token: IToken): vscode.AuthenticationSession { return { id: token.sessionId, - accessToken: () => !token.accessToken ? Promise.reject('Unavailable due to network problems') : Promise.resolve(token.accessToken), + accessToken: () => this.resolveAccessToken(token), accountName: token.accountName, scopes: token.scope.split(' ') }; } + private async resolveAccessToken(token: IToken): Promise { + if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { + Logger.info('Token available from cache'); + return Promise.resolve(token.accessToken); + } + + try { + Logger.info('Token expired or unavailable, trying refresh'); + const refreshedToken = await this.refreshToken(token.refreshToken, token.scope); + if (refreshedToken.accessToken) { + Promise.resolve(token.accessToken); + } else { + throw new Error(); + } + } catch (e) { + throw new Error('Unavailable due to network problems'); + } + + throw new Error('Unavailable due to network problems'); + } + private getTokenClaims(accessToken: string): ITokenClaims { try { return JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString()); @@ -252,9 +274,9 @@ export class AzureActiveDirectoryService { res.writeHead(302, { Location: '/' }); res.end(); } catch (err) { - Logger.error(err.message); res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); res.end(); + throw new Error(err.message); } } catch (e) { Logger.error(e.message); @@ -263,6 +285,7 @@ export class AzureActiveDirectoryService { if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { await this.loginWithoutLocalServer(scope); } + throw new Error(e.message); } finally { setTimeout(() => { server.close(); @@ -374,6 +397,7 @@ export class AzureActiveDirectoryService { const claims = this.getTokenClaims(json.access_token); return { expiresIn: json.expires_in, + expiresAt: Date.now() + json.expires_in * 1000, accessToken: json.access_token, refreshToken: json.refresh_token, scope, diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts index 1bd358034f..e38a45bd8f 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/vscode-account/src/extension.ts @@ -22,7 +22,6 @@ export async function activate(_: vscode.ExtensionContext) { await loginService.login(scopes.sort().join(' ')); return loginService.sessions[0]!; } catch (e) { - vscode.window.showErrorMessage(`Logging in failed: ${e}`); throw e; } }, diff --git a/scripts/code.sh b/scripts/code.sh index 4ba1a00b9f..0afe96bfcb 100755 --- a/scripts/code.sh +++ b/scripts/code.sh @@ -50,7 +50,7 @@ function code() { export VSCODE_LOGS= # Launch Code - exec "$CODE" . "$@" + exec "$CODE" . --no-sandbox "$@" } function code-wsl() diff --git a/src/main.js b/src/main.js index ddf0c4dae3..e5f8e9f548 100644 --- a/src/main.js +++ b/src/main.js @@ -89,7 +89,7 @@ app.once('ready', function () { traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' }; - contentTracing.startRecording(traceOptions, () => onReady()); + contentTracing.startRecording(traceOptions).finally(() => onReady()); } else { onReady(); } @@ -169,6 +169,9 @@ function configureCommandlineSwitchesSync(cliArgs) { app.commandLine.appendSwitch('js-flags', jsFlags); } + // TODO@Ben TODO@Deepak Electron 7 workaround for https://github.com/microsoft/vscode/issues/88873 + app.commandLine.appendSwitch('disable-features', 'LayoutNG'); + return argvConfig; } diff --git a/src/sql/workbench/api/browser/mainThreadQueryEditor.ts b/src/sql/workbench/api/browser/mainThreadQueryEditor.ts index 1d847cced8..af0340a5be 100644 --- a/src/sql/workbench/api/browser/mainThreadQueryEditor.ts +++ b/src/sql/workbench/api/browser/mainThreadQueryEditor.ts @@ -39,7 +39,7 @@ export class MainThreadQueryEditor extends Disposable implements MainThreadQuery public $connect(fileUri: string, connectionId: string): Thenable { return new Promise((resolve, reject) => { let editors = this._editorService.visibleControls.filter(resource => { - return !!resource && resource.input.getResource().toString() === fileUri; + return !!resource && resource.input.resource.toString() === fileUri; }); let editor = editors && editors.length > 0 ? editors[0] : undefined; let options: IConnectionCompletionOptions = { @@ -76,7 +76,7 @@ export class MainThreadQueryEditor extends Disposable implements MainThreadQuery public $connectWithProfile(fileUri: string, connection: azdata.connection.ConnectionProfile): Thenable { return new Promise(async (resolve, reject) => { let editors = this._editorService.visibleControls.filter(resource => { - return !!resource && resource.input.getResource().toString() === fileUri; + return !!resource && resource.input.resource.toString() === fileUri; }); let editor = editors && editors.length > 0 ? editors[0] : undefined; @@ -97,7 +97,7 @@ export class MainThreadQueryEditor extends Disposable implements MainThreadQuery } public $runQuery(fileUri: string, runCurrentQuery: boolean = true): void { - let filteredEditors = this._editorService.visibleControls.filter(editor => editor.input.getResource().toString() === fileUri); + let filteredEditors = this._editorService.visibleControls.filter(editor => editor.input.resource.toString() === fileUri); if (filteredEditors && filteredEditors.length > 0) { let editor = filteredEditors[0]; if (editor instanceof QueryEditor) { @@ -119,7 +119,7 @@ export class MainThreadQueryEditor extends Disposable implements MainThreadQuery public $createQueryTab(fileUri: string, title: string, componentId: string): void { let editors = this._editorService.visibleControls.filter(resource => { - return !!resource && resource.input.getResource().toString() === fileUri; + return !!resource && resource.input.resource.toString() === fileUri; }); let editor = editors && editors.length > 0 ? editors[0] : undefined; diff --git a/src/sql/workbench/browser/editData/editDataInput.ts b/src/sql/workbench/browser/editData/editDataInput.ts index 0127faf239..43f7023feb 100644 --- a/src/sql/workbench/browser/editData/editDataInput.ts +++ b/src/sql/workbench/browser/editData/editDataInput.ts @@ -114,7 +114,7 @@ export class EditDataInput extends EditorInput implements IConnectableInput { public save(): Promise { return Promise.resolve(undefined); } public getTypeId(): string { return EditDataInput.ID; } public setBootstrappedTrue(): void { this._hasBootstrapped = true; } - public getResource(): URI { return this._uri; } + public get resource(): URI { return this._uri; } public supportsSplitEditor(): boolean { return false; } public setupComplete() { this._setup = true; } public get queryString(): string { diff --git a/src/sql/workbench/browser/editData/editDataResultsInput.ts b/src/sql/workbench/browser/editData/editDataResultsInput.ts index e931ea6ff2..712cf30496 100644 --- a/src/sql/workbench/browser/editData/editDataResultsInput.ts +++ b/src/sql/workbench/browser/editData/editDataResultsInput.ts @@ -5,6 +5,7 @@ import { EditorInput } from 'vs/workbench/common/editor'; import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; /** * Input for the EditDataResultsEditor. This input helps with logic for the viewing and editing of @@ -102,4 +103,8 @@ export class EditDataResultsInput extends EditorInput { get uri(): string { return unescape(this._uri); } + + get resource(): URI | undefined { + return undefined; + } } diff --git a/src/sql/workbench/browser/editor/profiler/dashboardInput.ts b/src/sql/workbench/browser/editor/profiler/dashboardInput.ts index 541c7fdb8f..1f4b3cf123 100644 --- a/src/sql/workbench/browser/editor/profiler/dashboardInput.ts +++ b/src/sql/workbench/browser/editor/profiler/dashboardInput.ts @@ -46,8 +46,8 @@ export class DashboardInput extends EditorInput { // vscode has a comment that Mode's will eventually be removed (not sure the state of this comment) // so this might be able to be undone when that happens - if (!model.getModel(this.getResource())) { - model.createModel('', modeService.create('dashboard'), this.getResource()); + if (!model.getModel(this.resource)) { + model.createModel('', modeService.create('dashboard'), this.resource); } this._initializedPromise = _connectionService.connectIfNotConnected(_connectionProfile, 'dashboard').then( u => { @@ -72,7 +72,7 @@ export class DashboardInput extends EditorInput { return DashboardInput.ID; } - public getResource(): URI { + public get resource(): URI { return URI.from({ scheme: 'dashboard', path: 'dashboard' diff --git a/src/sql/workbench/browser/editor/profiler/profilerInput.ts b/src/sql/workbench/browser/editor/profiler/profilerInput.ts index 4292134dcc..df20a460fe 100644 --- a/src/sql/workbench/browser/editor/profiler/profilerInput.ts +++ b/src/sql/workbench/browser/editor/profiler/profilerInput.ts @@ -287,4 +287,8 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { super.dispose(); this._profilerService.disconnectSession(this.id); } + + get resource(): URI | undefined { + return undefined; + } } diff --git a/src/sql/workbench/browser/modelComponents/modelViewInput.ts b/src/sql/workbench/browser/modelComponents/modelViewInput.ts index 6e67f3b082..38d57ebf97 100644 --- a/src/sql/workbench/browser/modelComponents/modelViewInput.ts +++ b/src/sql/workbench/browser/modelComponents/modelViewInput.ts @@ -89,11 +89,11 @@ export class ModelViewInput extends EditorInput { return this._title; } - public getResource(): URI { + public get resource(): URI | undefined { if (this._options.resourceName) { return URI.from({ scheme: ModelViewInput.Scheme, path: this._options.resourceName }); } - return super.getResource(); + return undefined; } public get container(): HTMLElement { diff --git a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts index 50b57bba6c..d7937ea81b 100644 --- a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts +++ b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts @@ -196,7 +196,7 @@ export class QueryTextEditor extends BaseTextEditor { this.refreshEditorConfiguration(); } - private refreshEditorConfiguration(configuration = this.textResourceConfigurationService.getValue(this.input.getResource())): void { + private refreshEditorConfiguration(configuration = this.textResourceConfigurationService.getValue(this.input.resource)): void { if (!this.getControl()) { return; } diff --git a/src/sql/workbench/browser/taskUtilities.ts b/src/sql/workbench/browser/taskUtilities.ts index 4650f9ec96..e24abe2ccb 100644 --- a/src/sql/workbench/browser/taskUtilities.ts +++ b/src/sql/workbench/browser/taskUtilities.ts @@ -76,7 +76,7 @@ export function getCurrentGlobalConnection(objectExplorerService: IObjectExplore let activeInput = workbenchEditorService.activeEditor; if (activeInput) { - connection = connectionManagementService.getConnectionProfile(activeInput.getResource().toString()); + connection = connectionManagementService.getConnectionProfile(activeInput.resource.toString()); } return connection; diff --git a/src/sql/workbench/common/editor/query/queryEditorInput.ts b/src/sql/workbench/common/editor/query/queryEditorInput.ts index 6d0faf29d3..7dd6bd1d2d 100644 --- a/src/sql/workbench/common/editor/query/queryEditorInput.ts +++ b/src/sql/workbench/common/editor/query/queryEditorInput.ts @@ -166,7 +166,7 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab } // Getters for private properties - public get uri(): string { return this.getResource()!.toString(true); } + public get uri(): string { return this.resource!.toString(true); } public get text(): EditorInput { return this._text; } public get results(): QueryResultsInput { return this._results; } // Description is shown beside the tab name in the combobox of open editors @@ -191,7 +191,7 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab // Forwarding resource functions to the inline sql file editor public isDirty(): boolean { return this._text.isDirty(); } - public getResource(): URI | undefined { return this._text.getResource(); } + public get resource(): URI | undefined { return this._text.resource; } public matchInputInstanceType(inputType: any): boolean { return (this._text instanceof inputType); diff --git a/src/sql/workbench/common/editor/query/queryResultsInput.ts b/src/sql/workbench/common/editor/query/queryResultsInput.ts index ca59e536e9..2f2b2b9afb 100644 --- a/src/sql/workbench/common/editor/query/queryResultsInput.ts +++ b/src/sql/workbench/common/editor/query/queryResultsInput.ts @@ -12,6 +12,7 @@ import { QueryPlanState } from 'sql/workbench/common/editor/query/queryPlanState import { MessagePanelState } from 'sql/workbench/common/editor/query/messagePanelState'; import { GridPanelState } from 'sql/workbench/common/editor/query/gridPanelState'; import { QueryModelViewState } from 'sql/workbench/common/editor/query/modelViewState'; +import { URI } from 'vs/base/common/uri'; export class ResultsViewState { public readonly gridPanelState: GridPanelState = new GridPanelState(); @@ -89,4 +90,8 @@ export class QueryResultsInput extends EditorInput { get uri(): string { return this._uri; } + + get resource(): URI | undefined { + return undefined; + } } diff --git a/src/sql/workbench/contrib/commandLine/electron-browser/commandLine.ts b/src/sql/workbench/contrib/commandLine/electron-browser/commandLine.ts index a71c6c75e1..19032e6ef4 100644 --- a/src/sql/workbench/contrib/commandLine/electron-browser/commandLine.ts +++ b/src/sql/workbench/contrib/commandLine/electron-browser/commandLine.ts @@ -198,7 +198,7 @@ export class CommandLineWorkbenchContribution implements IWorkbenchContribution, // If an open and connectable query editor exists for the given URI, attach it to the connection profile private async processFile(uriString: string, profile: IConnectionProfile, warnOnConnectFailure: boolean): Promise { - let activeEditor = this._editorService.editors.filter(v => v.getResource().toString() === uriString).pop(); + let activeEditor = this._editorService.editors.filter(v => v.resource.toString() === uriString).pop(); if (activeEditor instanceof QueryEditorInput && activeEditor.state.connected) { let options: IConnectionCompletionOptions = { params: { connectionType: ConnectionType.editor, runQueryOnCompletion: RunQueryOnConnectionMode.none, input: activeEditor }, diff --git a/src/sql/workbench/contrib/editorReplacement/common/editorReplacerContribution.ts b/src/sql/workbench/contrib/editorReplacement/common/editorReplacerContribution.ts index e7b56c6d0e..6514d3720c 100644 --- a/src/sql/workbench/contrib/editorReplacement/common/editorReplacerContribution.ts +++ b/src/sql/workbench/contrib/editorReplacement/common/editorReplacerContribution.ts @@ -49,12 +49,12 @@ export class EditorReplacementContribution implements IWorkbenchContribution { } if (!language) { // in the case the input doesn't have a preferred mode set we will attempt to guess the mode from the file path - language = this.modeService.getModeIdByFilepathOrFirstLine(editor.getResource()); + language = this.modeService.getModeIdByFilepathOrFirstLine(editor.resource); } if (!language) { // just use the extension - language = path.extname(editor.getResource().toString()).slice(1); // remove the . + language = path.extname(editor.resource.toString()).slice(1); // remove the . } if (!language) { diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index 0cc5b1e452..40309dfa87 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -211,7 +211,7 @@ export abstract class NotebookInput extends EditorInput { private _notebookFindModel: NotebookFindModel; constructor(private _title: string, - private resource: URI, + private _resource: URI, private _textInput: TextInput, @ITextModelService private textModelService: ITextModelService, @IInstantiationService private instantiationService: IInstantiationService, @@ -219,7 +219,6 @@ export abstract class NotebookInput extends EditorInput { @IExtensionService private extensionService: IExtensionService ) { super(); - this.resource = resource; this._standardKernels = []; this._providersLoaded = this.assignProviders(); this._notebookEditorOpenedTimestamp = Date.now(); @@ -299,8 +298,8 @@ export abstract class NotebookInput extends EditorInput { private async setTrustForNewEditor(newInput: IEditorInput | undefined): Promise { let isTrusted = this._model.getNotebookModel().trustedMode; - if (isTrusted && newInput && newInput.getResource() !== this.getResource()) { - await this.notebookService.serializeNotebookStateChange(newInput.getResource(), NotebookChangeType.Saved, undefined, true); + if (isTrusted && newInput && newInput.resource !== this.resource) { + await this.notebookService.serializeNotebookStateChange(newInput.resource, NotebookChangeType.Saved, undefined, true); } } @@ -337,8 +336,8 @@ export abstract class NotebookInput extends EditorInput { public abstract getTypeId(): string; - getResource(): URI { - return this.resource; + get resource(): URI { + return this._resource; } public get untitledEditorModel(): IUntitledTextEditorModel { diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index 763f1f9e86..afe8920d04 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -260,7 +260,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe let editors = this.editorService.visibleControls; for (let editor of editors) { - if (editor && editor.input.getResource() === this._notebookParams.input.notebookUri) { + if (editor && editor.input.resource === this._notebookParams.input.notebookUri) { await editor.group.closeEditor(this._notebookParams.input as NotebookInput, { preserveFocus: true }); // sketchy break; } diff --git a/src/sql/workbench/contrib/notebook/common/models/nodebookInputFactory.ts b/src/sql/workbench/contrib/notebook/common/models/nodebookInputFactory.ts index f7ec938118..d6abd1ec74 100644 --- a/src/sql/workbench/contrib/notebook/common/models/nodebookInputFactory.ts +++ b/src/sql/workbench/contrib/notebook/common/models/nodebookInputFactory.ts @@ -23,9 +23,9 @@ export class NotebookEditorInputAssociation implements ILanguageAssociation { convertInput(activeEditor: IEditorInput): NotebookInput { if (activeEditor instanceof FileEditorInput) { - return this.instantiationService.createInstance(FileNotebookInput, activeEditor.getName(), activeEditor.getResource(), activeEditor); + return this.instantiationService.createInstance(FileNotebookInput, activeEditor.getName(), activeEditor.resource, activeEditor); } else if (activeEditor instanceof UntitledTextEditorInput) { - return this.instantiationService.createInstance(UntitledNotebookInput, activeEditor.getName(), activeEditor.getResource(), activeEditor); + return this.instantiationService.createInstance(UntitledNotebookInput, activeEditor.getName(), activeEditor.resource, activeEditor); } else { return undefined; } @@ -48,7 +48,7 @@ export class FileNoteBookEditorInputFactory implements IEditorInputFactory { deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileNotebookInput | undefined { const factory = editorInputFactoryRegistry.getEditorInputFactory(FILE_EDITOR_INPUT_ID); const fileEditorInput = factory.deserialize(instantiationService, serializedEditorInput) as FileEditorInput; - return instantiationService.createInstance(FileNotebookInput, fileEditorInput.getName(), fileEditorInput.getResource(), fileEditorInput); + return instantiationService.createInstance(FileNotebookInput, fileEditorInput.getName(), fileEditorInput.resource, fileEditorInput); } canSerialize(): boolean { // we can always serialize notebooks @@ -68,7 +68,7 @@ export class UntitledNoteBookEditorInputFactory implements IEditorInputFactory { deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledNotebookInput | undefined { const factory = editorInputFactoryRegistry.getEditorInputFactory(UntitledTextEditorInput.ID); const untitledEditorInput = factory.deserialize(instantiationService, serializedEditorInput) as UntitledTextEditorInput; - return instantiationService.createInstance(UntitledNotebookInput, untitledEditorInput.getName(), untitledEditorInput.getResource(), untitledEditorInput); + return instantiationService.createInstance(UntitledNotebookInput, untitledEditorInput.getName(), untitledEditorInput.resource, untitledEditorInput); } canSerialize(): boolean { // we can always serialize notebooks diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts index 61f993d194..65d4a9d370 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts @@ -115,7 +115,7 @@ suite('Notebook Input', function (): void { assert.strictEqual(untitledNotebookInput.untitledEditorModel, testModel); // getResource - assert.strictEqual(untitledNotebookInput.getResource(), untitledUri); + assert.strictEqual(untitledNotebookInput.resource, untitledUri); // Standard kernels let testKernels = [{ diff --git a/src/sql/workbench/contrib/query/browser/flavorStatus.ts b/src/sql/workbench/contrib/query/browser/flavorStatus.ts index e901514553..41cf434009 100644 --- a/src/sql/workbench/contrib/query/browser/flavorStatus.ts +++ b/src/sql/workbench/contrib/query/browser/flavorStatus.ts @@ -96,12 +96,12 @@ export class SqlFlavorStatusbarItem extends Disposable implements IWorkbenchCont } private _onEditorClosed(event: IEditorCloseEvent): void { - let uri = event.editor.getResource().toString(); + let uri = event.editor.resource.toString(); if (uri && uri in this._sqlStatusEditors) { // If active editor is being closed, hide the query status. let activeEditor = this.editorService.activeControl; if (activeEditor) { - let currentUri = activeEditor.input.getResource().toString(); + let currentUri = activeEditor.input.resource.toString(); if (uri === currentUri) { this.hide(); } @@ -114,7 +114,7 @@ export class SqlFlavorStatusbarItem extends Disposable implements IWorkbenchCont private _onEditorsChanged(): void { let activeEditor = this.editorService.activeControl; if (activeEditor) { - let uri = activeEditor.input.getResource().toString(); + let uri = activeEditor.input.resource.toString(); // Show active editor's language flavor status if (uri) { @@ -145,7 +145,7 @@ export class SqlFlavorStatusbarItem extends Disposable implements IWorkbenchCont private _showStatus(uri: string): void { let activeEditor = this.editorService.activeControl; if (activeEditor) { - let currentUri = activeEditor.input.getResource().toString(); + let currentUri = activeEditor.input.resource.toString(); if (uri === currentUri) { let flavor: SqlProviderEntry = this._sqlStatusEditors[uri]; if (flavor) { @@ -186,7 +186,7 @@ export class ChangeFlavorAction extends Action { public run(): Promise { let activeEditor = this._editorService.activeControl; - let currentUri = activeEditor?.input.getResource().toString(); + let currentUri = activeEditor?.input.resource.toString(); if (this._connectionManagementService.isConnected(currentUri)) { let currentProvider = this._connectionManagementService.getProviderIdFromUri(currentUri); return this._showMessage(Severity.Info, nls.localize('alreadyConnected', @@ -226,4 +226,3 @@ export class ChangeFlavorAction extends Action { return Promise.resolve(undefined); } } - diff --git a/src/sql/workbench/contrib/query/browser/queryEditor.ts b/src/sql/workbench/contrib/query/browser/queryEditor.ts index c24f5a19ac..a95eb41750 100644 --- a/src/sql/workbench/contrib/query/browser/queryEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryEditor.ts @@ -105,7 +105,7 @@ export class QueryEditor extends BaseEditor { this.queryEditorVisible = queryContext.QueryEditorVisibleContext.bindTo(contextKeyService); // Clear view state for deleted files - this._register(fileService.onFileChanges(e => this.onFilesChanged(e))); + this._register(fileService.onDidFilesChange(e => this.onFilesChanged(e))); } private onFilesChanged(e: FileChangesEvent): void { @@ -317,7 +317,7 @@ export class QueryEditor extends BaseEditor { this.inputDisposables.add(this.input.state.onChange(c => this.updateState(c))); this.updateState({ connectingChange: true, connectedChange: true, executingChange: true, resultsVisibleChange: true, sqlCmdModeChanged: true }); - const editorViewState = this.loadTextEditorViewState(this.input.getResource()); + const editorViewState = this.loadTextEditorViewState(this.input.resource); if (editorViewState && editorViewState.resultsHeight && this.splitview.length > 1) { this.splitview.resizeView(1, editorViewState.resultsHeight); @@ -331,7 +331,7 @@ export class QueryEditor extends BaseEditor { // Otherwise we save the view state to restore it later else if (!input.isDisposed()) { - this.saveTextEditorViewState(input.getResource()); + this.saveTextEditorViewState(input.resource); } } diff --git a/src/sql/workbench/contrib/query/common/queryInputFactory.ts b/src/sql/workbench/contrib/query/common/queryInputFactory.ts index b72234d2ac..41c932f4ab 100644 --- a/src/sql/workbench/contrib/query/common/queryInputFactory.ts +++ b/src/sql/workbench/contrib/query/common/queryInputFactory.ts @@ -34,7 +34,7 @@ export class QueryEditorLanguageAssociation implements ILanguageAssociation { @IEditorService private readonly editorService: IEditorService) { } convertInput(activeEditor: IEditorInput): QueryEditorInput | undefined { - const queryResultsInput = this.instantiationService.createInstance(QueryResultsInput, activeEditor.getResource().toString(true)); + const queryResultsInput = this.instantiationService.createInstance(QueryResultsInput, activeEditor.resource.toString(true)); let queryEditorInput: QueryEditorInput; if (activeEditor instanceof FileEditorInput) { queryEditorInput = this.instantiationService.createInstance(FileQueryEditorInput, '', activeEditor, queryResultsInput); @@ -81,8 +81,8 @@ export class FileQueryEditorInputFactory implements IEditorInputFactory { const factory = editorInputFactoryRegistry.getEditorInputFactory(FILE_EDITOR_INPUT_ID); const fileEditorInput = factory.deserialize(instantiationService, serializedEditorInput) as FileEditorInput; // only successfully deserilize the file if the resource actually exists - if (this.fileService.exists(fileEditorInput.getResource())) { - const queryResultsInput = instantiationService.createInstance(QueryResultsInput, fileEditorInput.getResource().toString()); + if (this.fileService.exists(fileEditorInput.resource)) { + const queryResultsInput = instantiationService.createInstance(QueryResultsInput, fileEditorInput.resource.toString()); return instantiationService.createInstance(FileQueryEditorInput, '', fileEditorInput, queryResultsInput); } else { fileEditorInput.dispose(); @@ -110,7 +110,7 @@ export class UntitledQueryEditorInputFactory implements IEditorInputFactory { deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): UntitledQueryEditorInput | undefined { const factory = editorInputFactoryRegistry.getEditorInputFactory(UntitledTextEditorInput.ID); const untitledEditorInput = factory.deserialize(instantiationService, serializedEditorInput) as UntitledTextEditorInput; - const queryResultsInput = instantiationService.createInstance(QueryResultsInput, untitledEditorInput.getResource().toString()); + const queryResultsInput = instantiationService.createInstance(QueryResultsInput, untitledEditorInput.resource.toString()); return instantiationService.createInstance(UntitledQueryEditorInput, '', untitledEditorInput, queryResultsInput); } diff --git a/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts b/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts index 643f7225ad..de12d64993 100644 --- a/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts +++ b/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts @@ -17,7 +17,7 @@ export class QueryPlanConverter implements ILanguageAssociation { constructor(@IInstantiationService private instantiationService: IInstantiationService) { } convertInput(activeEditor: IEditorInput): QueryPlanInput { - return this.instantiationService.createInstance(QueryPlanInput, activeEditor.getResource()); + return this.instantiationService.createInstance(QueryPlanInput, activeEditor.resource); } createBase(activeEditor: QueryPlanInput): IEditorInput { @@ -83,4 +83,8 @@ export class QueryPlanInput extends EditorInput { public get uniqueSelector(): string { return this._uniqueSelector; } + + get resource(): URI | undefined { + return undefined; + } } diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 6d87bff67d..07c36f8d13 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -11,6 +11,7 @@ "strictBindCallApply": true, "strictNullChecks": false, "strictPropertyInitialization": false, + "strict": false, "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index e05fde64d6..b4a4b8ed7b 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -251,14 +251,14 @@ export interface IGlobOptions { } interface ParsedStringPattern { - (path: string, basename: string): string | null | Promise /* the matching pattern */; + (path: string, basename?: string): string | null | Promise /* the matching pattern */; basenames?: string[]; patterns?: string[]; allBasenames?: string[]; allPaths?: string[]; } interface ParsedExpressionPattern { - (path: string, basename: string, name?: string, hasSibling?: (name: string) => boolean | Promise): string | null | Promise /* the matching pattern */; + (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise): string | null | Promise /* the matching pattern */; requiresSiblings?: boolean; allBasenames?: string[]; allPaths?: string[]; @@ -374,7 +374,7 @@ function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern { if (n === 1) { return parsedPatterns[0]; } - const parsedPattern: ParsedStringPattern = function (path: string, basename: string) { + const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) { for (let i = 0, n = parsedPatterns.length; i < n; i++) { if ((parsedPatterns[i])(path, basename)) { return pattern; @@ -409,7 +409,7 @@ function trivia4and5(path: string, pattern: string, matchPathEnds: boolean): Par function toRegExp(pattern: string): ParsedStringPattern { try { const regExp = new RegExp(`^${parseRegExp(pattern)}$`); - return function (path: string, basename: string) { + return function (path: string) { regExp.lastIndex = 0; // reset RegExp to its initial state to reuse it! return typeof path === 'string' && regExp.test(path) ? pattern : null; }; @@ -457,7 +457,7 @@ export function parse(arg1: string | IExpression | IRelativePattern, options: IG if (parsedPattern === NULL) { return FALSE; } - const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[]; } = function (path: string, basename: string) { + const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[]; } = function (path: string, basename?: string) { return !!parsedPattern(path, basename); }; if (parsedPattern.allBasenames) { @@ -540,7 +540,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return parsedPatterns[0]; } - const resultExpression: ParsedStringPattern = function (path: string, basename: string) { + const resultExpression: ParsedStringPattern = function (path: string, basename?: string) { for (let i = 0, n = parsedPatterns.length; i < n; i++) { // Pattern matches path const result = (parsedPatterns[i])(path, basename); @@ -565,7 +565,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return resultExpression; } - const resultExpression: ParsedStringPattern = function (path: string, basename: string, hasSibling?: (name: string) => boolean | Promise) { + const resultExpression: ParsedStringPattern = function (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise) { let name: string | undefined = undefined; for (let i = 0, n = parsedPatterns.length; i < n; i++) { @@ -620,12 +620,12 @@ function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, if (value) { const when = (value).when; if (typeof when === 'string') { - const result: ParsedExpressionPattern = (path: string, basename: string, name: string, hasSibling: (name: string) => boolean | Promise) => { + const result: ParsedExpressionPattern = (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise) => { if (!hasSibling || !parsedPattern(path, basename)) { return null; } - const clausePattern = when.replace('$(basename)', name); + const clausePattern = when.replace('$(basename)', name!); const matched = hasSibling(clausePattern); return isThenable(matched) ? matched.then(m => m ? pattern : null) : diff --git a/src/vs/base/browser/linkedText.ts b/src/vs/base/common/linkedText.ts similarity index 79% rename from src/vs/base/browser/linkedText.ts rename to src/vs/base/common/linkedText.ts index 764b078eca..83d9d199d9 100644 --- a/src/vs/base/browser/linkedText.ts +++ b/src/vs/base/common/linkedText.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { memoize } from 'vs/base/common/decorators'; + export interface ILink { readonly label: string; readonly href: string; @@ -10,7 +12,16 @@ export interface ILink { } export type LinkedTextNode = string | ILink; -export type LinkedText = LinkedTextNode[]; + +export class LinkedText { + + constructor(readonly nodes: LinkedTextNode[]) { } + + @memoize + toString(): string { + return this.nodes.map(node => typeof node === 'string' ? node : node.label).join(''); + } +} const LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: "([^"]+)")?\)/gi; @@ -40,5 +51,5 @@ export function parseLinkedText(text: string): LinkedText { result.push(text.substring(index)); } - return result; + return new LinkedText(result); } diff --git a/src/vs/base/test/browser/linkedText.test.ts b/src/vs/base/test/common/linkedText.test.ts similarity index 77% rename from src/vs/base/test/browser/linkedText.test.ts rename to src/vs/base/test/common/linkedText.test.ts index d1e3bbef14..f7fe6ff5bf 100644 --- a/src/vs/base/test/browser/linkedText.test.ts +++ b/src/vs/base/test/common/linkedText.test.ts @@ -4,50 +4,50 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { parseLinkedText } from 'vs/base/browser/linkedText'; +import { parseLinkedText } from 'vs/base/common/linkedText'; suite('LinkedText', () => { test('parses correctly', () => { - assert.deepEqual(parseLinkedText(''), []); - assert.deepEqual(parseLinkedText('hello'), ['hello']); - assert.deepEqual(parseLinkedText('hello there'), ['hello there']); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href).'), [ + assert.deepEqual(parseLinkedText('').nodes, []); + assert.deepEqual(parseLinkedText('hello').nodes, ['hello']); + assert.deepEqual(parseLinkedText('hello there').nodes, ['hello there']); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href).').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a title").'), [ + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a title").').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href', title: 'and a title' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](random stuff).'), [ + assert.deepEqual(parseLinkedText('Some message with [link text](random stuff).').nodes, [ 'Some message with [link text](random stuff).' ]); - assert.deepEqual(parseLinkedText('Some message with [https link](https://link.href).'), [ + assert.deepEqual(parseLinkedText('Some message with [https link](https://link.href).').nodes, [ 'Some message with ', { label: 'https link', href: 'https://link.href' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [https link](https:).'), [ + assert.deepEqual(parseLinkedText('Some message with [https link](https:).').nodes, [ 'Some message with [https link](https:).' ]); - assert.deepEqual(parseLinkedText('Some message with [a command](command:foobar).'), [ + assert.deepEqual(parseLinkedText('Some message with [a command](command:foobar).').nodes, [ 'Some message with ', { label: 'a command', href: 'command:foobar' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [a command](command:).'), [ + assert.deepEqual(parseLinkedText('Some message with [a command](command:).').nodes, [ 'Some message with [a command](command:).' ]); - assert.deepEqual(parseLinkedText('link [one](command:foo "nice") and link [two](http://foo)...'), [ + assert.deepEqual(parseLinkedText('link [one](command:foo "nice") and link [two](http://foo)...').nodes, [ 'link ', { label: 'one', href: 'command:foo', title: 'nice' }, ' and link ', { label: 'two', href: 'http://foo' }, '...' ]); - assert.deepEqual(parseLinkedText('link\n[one](command:foo "nice")\nand link [two](http://foo)...'), [ + assert.deepEqual(parseLinkedText('link\n[one](command:foo "nice")\nand link [two](http://foo)...').nodes, [ 'link\n', { label: 'one', href: 'command:foo', title: 'nice' }, '\nand link ', diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 00ac8234cb..1734afe8bf 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -17,7 +17,7 @@ import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtension import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/browser/requestService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 5681e2c83a..195db53e8c 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -26,7 +26,7 @@ import { IStateService } from 'vs/platform/state/node/state'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { EnvironmentService, xdgRuntimeDir } from 'vs/platform/environment/node/environmentService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService'; import * as fs from 'fs'; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index ec69823e65..dafc4affde 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -24,7 +24,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProper import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/node/requestService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { mkdirp, writeFile } from 'vs/base/node/pfs'; import { getBaseLabel } from 'vs/base/common/labels'; diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 6eddab8779..467b65283b 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -1614,7 +1614,7 @@ export namespace CoreEditingCommands { constructor() { super({ id: 'deleteLeft', - precondition: EditorContextKeys.writable, + precondition: undefined, kbOpts: { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, @@ -1639,7 +1639,7 @@ export namespace CoreEditingCommands { constructor() { super({ id: 'deleteRight', - precondition: EditorContextKeys.writable, + precondition: undefined, kbOpts: { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 2fb294eb06..6b890c6323 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -149,6 +149,10 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe return result; } +const enum Constants { + SPAN_MODULO_LIMIT = 16384 +} + function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: IStringBuilder): [number[], number[]] { sb.appendASCIIString('
MinimapCharRenderer; - /** * container dom node left position (in CSS px) */ @@ -107,6 +91,11 @@ class MinimapOptions { */ public readonly canvasOuterHeight: number; + public readonly fontScale: number; + public readonly minimapLineHeight: number; + public readonly minimapCharWidth: number; + + public readonly charRenderer: () => MinimapCharRenderer; public readonly backgroundColor: RGBA8; constructor(configuration: IConfiguration, theme: EditorTheme, tokensColorTracker: MinimapTokensColorTracker) { @@ -114,13 +103,13 @@ class MinimapOptions { const pixelRatio = options.get(EditorOption.pixelRatio); const layoutInfo = options.get(EditorOption.layoutInfo); const fontInfo = options.get(EditorOption.fontInfo); + const minimapOpts = options.get(EditorOption.minimap); this.renderMinimap = layoutInfo.renderMinimap | 0; + this.mode = minimapOpts.mode; + this.minimapHeightIsEditorHeight = layoutInfo.minimapHeightIsEditorHeight; this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine); - const minimapOpts = options.get(EditorOption.minimap); this.showSlider = minimapOpts.showSlider; - this.fontScale = Math.round(minimapOpts.scale * pixelRatio); - this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); this.pixelRatio = pixelRatio; this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this.lineHeight = options.get(EditorOption.lineHeight); @@ -128,12 +117,16 @@ class MinimapOptions { this.minimapWidth = layoutInfo.minimapWidth; this.minimapHeight = layoutInfo.height; - this.canvasInnerWidth = Math.floor(pixelRatio * this.minimapWidth); - this.canvasInnerHeight = Math.floor(pixelRatio * this.minimapHeight); + this.canvasInnerWidth = layoutInfo.minimapCanvasInnerWidth; + this.canvasInnerHeight = layoutInfo.minimapCanvasInnerHeight; + this.canvasOuterWidth = layoutInfo.minimapCanvasOuterWidth; + this.canvasOuterHeight = layoutInfo.minimapCanvasOuterHeight; - this.canvasOuterWidth = this.canvasInnerWidth / pixelRatio; - this.canvasOuterHeight = this.canvasInnerHeight / pixelRatio; + this.fontScale = layoutInfo.minimapScale; + this.minimapLineHeight = layoutInfo.minimapLineHeight; + this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale; + this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); this.backgroundColor = MinimapOptions._getMinimapBackground(theme, tokensColorTracker); } @@ -147,12 +140,13 @@ class MinimapOptions { public equals(other: MinimapOptions): boolean { return (this.renderMinimap === other.renderMinimap + && this.mode === other.mode + && this.minimapHeightIsEditorHeight === other.minimapHeightIsEditorHeight && this.scrollBeyondLastLine === other.scrollBeyondLastLine && this.showSlider === other.showSlider && this.pixelRatio === other.pixelRatio && this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth && this.lineHeight === other.lineHeight - && this.fontScale === other.fontScale && this.minimapLeft === other.minimapLeft && this.minimapWidth === other.minimapWidth && this.minimapHeight === other.minimapHeight @@ -160,6 +154,9 @@ class MinimapOptions { && this.canvasInnerHeight === other.canvasInnerHeight && this.canvasOuterWidth === other.canvasOuterWidth && this.canvasOuterHeight === other.canvasOuterHeight + && this.fontScale === other.fontScale + && this.minimapLineHeight === other.minimapLineHeight + && this.minimapCharWidth === other.minimapCharWidth && this.backgroundColor.equals(other.backgroundColor) ); } @@ -177,6 +174,7 @@ class MinimapLayout { */ public readonly scrollHeight: number; + public readonly sliderNeeded: boolean; private readonly _computedSliderRatio: number; /** @@ -200,6 +198,7 @@ class MinimapLayout { constructor( scrollTop: number, scrollHeight: number, + sliderNeeded: boolean, computedSliderRatio: number, sliderTop: number, sliderHeight: number, @@ -208,6 +207,7 @@ class MinimapLayout { ) { this.scrollTop = scrollTop; this.scrollHeight = scrollHeight; + this.sliderNeeded = sliderNeeded; this._computedSliderRatio = computedSliderRatio; this.sliderTop = sliderTop; this.sliderHeight = sliderHeight; @@ -234,15 +234,31 @@ class MinimapLayout { viewportHeight: number, viewportContainsWhitespaceGaps: boolean, lineCount: number, + realLineCount: number, scrollTop: number, scrollHeight: number, previousLayout: MinimapLayout | null ): MinimapLayout { const pixelRatio = options.pixelRatio; - const minimapLineHeight = getMinimapLineHeight(options.renderMinimap, options.fontScale); + const minimapLineHeight = options.minimapLineHeight; const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight); const lineHeight = options.lineHeight; + if (options.minimapHeightIsEditorHeight) { + const logicalScrollHeight = ( + realLineCount * options.lineHeight + + (options.scrollBeyondLastLine ? viewportHeight - options.lineHeight : 0) + ); + const sliderHeight = Math.max(1, Math.floor(viewportHeight * viewportHeight / logicalScrollHeight)); + const maxMinimapSliderTop = Math.max(0, options.minimapHeight - sliderHeight); + // The slider can move from 0 to `maxMinimapSliderTop` + // in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`. + const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight); + const sliderTop = (scrollTop * computedSliderRatio); + const sliderNeeded = (maxMinimapSliderTop > 0); + return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, 1, lineCount); + } + // The visible line count in a viewport can change due to a number of reasons: // a) with the same viewport width, different scroll positions can result in partial lines being visible: // e.g. for a line height of 20, and a viewport height of 600 @@ -283,14 +299,14 @@ class MinimapLayout { let extraLinesAtTheBottom = 0; if (options.scrollBeyondLastLine) { const expectedViewportLineCount = viewportHeight / lineHeight; - extraLinesAtTheBottom = expectedViewportLineCount; + extraLinesAtTheBottom = expectedViewportLineCount - 1; } if (minimapLinesFitting >= lineCount + extraLinesAtTheBottom) { // All lines fit in the minimap const startLineNumber = 1; const endLineNumber = lineCount; - - return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); + const sliderNeeded = (maxMinimapSliderTop > 0); + return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); } else { let startLineNumber = Math.max(1, Math.floor(viewportStartLineNumber - sliderTop * pixelRatio / minimapLineHeight)); @@ -309,7 +325,7 @@ class MinimapLayout { const endLineNumber = Math.min(lineCount, startLineNumber + minimapLinesFitting - 1); - return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); + return new MinimapLayout(scrollTop, scrollHeight, true, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber); } } } @@ -391,17 +407,17 @@ class RenderData { }; } - public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { - return this._renderedLines.onLinesChanged(e.fromLineNumber, e.toLineNumber); + public onLinesChanged(changeFromLineNumber: number, changeToLineNumber: number): boolean { + return this._renderedLines.onLinesChanged(changeFromLineNumber, changeToLineNumber); } - public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): void { - this._renderedLines.onLinesDeleted(e.fromLineNumber, e.toLineNumber); + public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): void { + this._renderedLines.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber); } - public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void { - this._renderedLines.onLinesInserted(e.fromLineNumber, e.toLineNumber); + public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): void { + this._renderedLines.onLinesInserted(insertFromLineNumber, insertToLineNumber); } - public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean { - return this._renderedLines.onTokensChanged(e.ranges); + public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number; }[]): boolean { + return this._renderedLines.onTokensChanged(ranges); } } @@ -458,9 +474,557 @@ class MinimapBuffers { } } -export class Minimap extends ViewPart { +export interface IMinimapModel { + readonly tokensColorTracker: MinimapTokensColorTracker; + readonly options: MinimapOptions; + + getLineCount(): number; + getRealLineCount(): number; + getLineContent(lineNumber: number): string; + getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[]; + getSelections(): Selection[]; + getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[]; + getOptions(): TextModelResolvedOptions; + revealLineNumber(lineNumber: number): void; + setScrollTop(scrollTop: number): void; +} + +interface IMinimapRenderingContext { + readonly viewportContainsWhitespaceGaps: boolean; + + readonly scrollWidth: number; + readonly scrollHeight: number; + + readonly viewportStartLineNumber: number; + readonly viewportEndLineNumber: number; + + readonly scrollTop: number; + readonly scrollLeft: number; + + readonly viewportWidth: number; + readonly viewportHeight: number; +} + +interface SamplingStateLinesDeletedEvent { + type: 'deleted'; + _oldIndex: number; + deleteFromLineNumber: number; + deleteToLineNumber: number; +} + +interface SamplingStateLinesInsertedEvent { + type: 'inserted'; + _i: number; + insertFromLineNumber: number; + insertToLineNumber: number; +} + +interface SamplingStateFlushEvent { + type: 'flush'; +} + +type SamplingStateEvent = SamplingStateLinesInsertedEvent | SamplingStateLinesDeletedEvent | SamplingStateFlushEvent; + +class MinimapSamplingState { + + public static compute(options: IComputedEditorOptions, modelLineCount: number, oldSamplingState: MinimapSamplingState | null): [MinimapSamplingState | null, SamplingStateEvent[]] { + const minimapOpts = options.get(EditorOption.minimap); + const layoutInfo = options.get(EditorOption.layoutInfo); + if (!minimapOpts.enabled || !layoutInfo.minimapIsSampling) { + return [null, []]; + } + + // ratio is intentionally not part of the layout to avoid the layout changing all the time + // so we need to recompute it again... + const pixelRatio = options.get(EditorOption.pixelRatio); + const lineHeight = options.get(EditorOption.lineHeight); + const scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine); + const { minimapLineCount } = EditorLayoutInfoComputer.computeContainedMinimapLineCount({ + modelLineCount: modelLineCount, + scrollBeyondLastLine: scrollBeyondLastLine, + height: layoutInfo.height, + lineHeight: lineHeight, + pixelRatio: pixelRatio + }); + const ratio = modelLineCount / minimapLineCount; + const halfRatio = ratio / 2; + + if (!oldSamplingState || oldSamplingState.minimapLines.length === 0) { + let result: number[] = []; + result[0] = 1; + if (minimapLineCount > 1) { + for (let i = 0, lastIndex = minimapLineCount - 1; i < lastIndex; i++) { + result[i] = Math.round(i * ratio + halfRatio); + } + result[minimapLineCount - 1] = modelLineCount; + } + return [new MinimapSamplingState(ratio, result), []]; + } + + const oldMinimapLines = oldSamplingState.minimapLines; + const oldLength = oldMinimapLines.length; + let result: number[] = []; + let oldIndex = 0; + let oldDeltaLineCount = 0; + let minModelLineNumber = 1; + const MAX_EVENT_COUNT = 10; // generate at most 10 events, if there are more than 10 changes, just flush all previous data + let events: SamplingStateEvent[] = []; + let lastEvent: SamplingStateEvent | null = null; + for (let i = 0; i < minimapLineCount; i++) { + const fromModelLineNumber = Math.max(minModelLineNumber, Math.round(i * ratio)); + const toModelLineNumber = Math.max(fromModelLineNumber, Math.round((i + 1) * ratio)); + + while (oldIndex < oldLength && oldMinimapLines[oldIndex] < fromModelLineNumber) { + if (events.length < MAX_EVENT_COUNT) { + const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount; + if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) { + lastEvent.deleteToLineNumber++; + } else { + lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber }; + events.push(lastEvent); + } + oldDeltaLineCount--; + } + oldIndex++; + } + + let selectedModelLineNumber: number; + if (oldIndex < oldLength && oldMinimapLines[oldIndex] <= toModelLineNumber) { + // reuse the old sampled line + selectedModelLineNumber = oldMinimapLines[oldIndex]; + oldIndex++; + } else { + if (i === 0) { + selectedModelLineNumber = 1; + } else if (i + 1 === minimapLineCount) { + selectedModelLineNumber = modelLineCount; + } else { + selectedModelLineNumber = Math.round(i * ratio + halfRatio); + } + if (events.length < MAX_EVENT_COUNT) { + const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount; + if (lastEvent && lastEvent.type === 'inserted' && lastEvent._i === i - 1) { + lastEvent.insertToLineNumber++; + } else { + lastEvent = { type: 'inserted', _i: i, insertFromLineNumber: oldMinimapLineNumber, insertToLineNumber: oldMinimapLineNumber }; + events.push(lastEvent); + } + oldDeltaLineCount++; + } + } + + result[i] = selectedModelLineNumber; + minModelLineNumber = selectedModelLineNumber; + } + + if (events.length < MAX_EVENT_COUNT) { + while (oldIndex < oldLength) { + const oldMinimapLineNumber = oldIndex + 1 + oldDeltaLineCount; + if (lastEvent && lastEvent.type === 'deleted' && lastEvent._oldIndex === oldIndex - 1) { + lastEvent.deleteToLineNumber++; + } else { + lastEvent = { type: 'deleted', _oldIndex: oldIndex, deleteFromLineNumber: oldMinimapLineNumber, deleteToLineNumber: oldMinimapLineNumber }; + events.push(lastEvent); + } + oldDeltaLineCount--; + oldIndex++; + } + } else { + // too many events, just give up + events = [{ type: 'flush' }]; + } + + return [new MinimapSamplingState(ratio, result), events]; + } + + constructor( + public readonly samplingRatio: number, + public readonly minimapLines: number[] + ) { + } + + public modelLineToMinimapLine(lineNumber: number): number { + return Math.min(this.minimapLines.length, Math.max(1, Math.round(lineNumber / this.samplingRatio))); + } + + /** + * Will return null if the model line ranges are not intersecting with a sampled model line. + */ + public modelLineRangeToMinimapLineRange(fromLineNumber: number, toLineNumber: number): [number, number] | null { + let fromLineIndex = this.modelLineToMinimapLine(fromLineNumber) - 1; + while (fromLineIndex > 0 && this.minimapLines[fromLineIndex - 1] >= fromLineNumber) { + fromLineIndex--; + } + let toLineIndex = this.modelLineToMinimapLine(toLineNumber) - 1; + while (toLineIndex + 1 < this.minimapLines.length && this.minimapLines[toLineIndex + 1] <= toLineNumber) { + toLineIndex++; + } + if (fromLineIndex === toLineIndex) { + const sampledLineNumber = this.minimapLines[fromLineIndex]; + if (sampledLineNumber < fromLineNumber || sampledLineNumber > toLineNumber) { + // This line is not part of the sampled lines ==> nothing to do + return null; + } + } + return [fromLineIndex + 1, toLineIndex + 1]; + } + + /** + * Will always return a range, even if it is not intersecting with a sampled model line. + */ + public decorationLineRangeToMinimapLineRange(startLineNumber: number, endLineNumber: number): [number, number] { + let minimapLineStart = this.modelLineToMinimapLine(startLineNumber); + let minimapLineEnd = this.modelLineToMinimapLine(endLineNumber); + if (startLineNumber !== endLineNumber && minimapLineEnd === minimapLineStart) { + if (minimapLineEnd === this.minimapLines.length) { + if (minimapLineStart > 1) { + minimapLineStart--; + } + } else { + minimapLineEnd++; + } + } + return [minimapLineStart, minimapLineEnd]; + } + + public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): [number, number] { + // have the mapping be sticky + const deletedLineCount = e.toLineNumber - e.fromLineNumber + 1; + let changeStartIndex = this.minimapLines.length; + let changeEndIndex = 0; + for (let i = this.minimapLines.length - 1; i >= 0; i--) { + if (this.minimapLines[i] < e.fromLineNumber) { + break; + } + if (this.minimapLines[i] <= e.toLineNumber) { + // this line got deleted => move to previous available + this.minimapLines[i] = Math.max(1, e.fromLineNumber - 1); + changeStartIndex = Math.min(changeStartIndex, i); + changeEndIndex = Math.max(changeEndIndex, i); + } else { + this.minimapLines[i] -= deletedLineCount; + } + } + return [changeStartIndex, changeEndIndex]; + } + + public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void { + // have the mapping be sticky + const insertedLineCount = e.toLineNumber - e.fromLineNumber + 1; + for (let i = this.minimapLines.length - 1; i >= 0; i--) { + if (this.minimapLines[i] < e.fromLineNumber) { + break; + } + this.minimapLines[i] += insertedLineCount; + } + } +} + +export class Minimap extends ViewPart implements IMinimapModel { + + public readonly tokensColorTracker: MinimapTokensColorTracker; + + private _selections: Selection[]; + private _minimapSelections: Selection[] | null; + + public options: MinimapOptions; + + private _samplingState: MinimapSamplingState | null; + private _shouldCheckSampling: boolean; + + private _actual: InnerMinimap; + + constructor(context: ViewContext) { + super(context); + + this.tokensColorTracker = MinimapTokensColorTracker.getInstance(); + + this._selections = []; + this._minimapSelections = null; + + this.options = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker); + const [samplingState,] = MinimapSamplingState.compute(this._context.configuration.options, this._context.model.getLineCount(), null); + this._samplingState = samplingState; + this._shouldCheckSampling = false; + + this._actual = new InnerMinimap(context.theme, this); + } + + public dispose(): void { + this._actual.dispose(); + super.dispose(); + } + + public getDomNode(): FastDomNode { + return this._actual.getDomNode(); + } + + private _onOptionsMaybeChanged(): boolean { + const opts = new MinimapOptions(this._context.configuration, this._context.theme, this.tokensColorTracker); + if (this.options.equals(opts)) { + return false; + } + this.options = opts; + this._recreateLineSampling(); + this._actual.onDidChangeOptions(); + return true; + } + + // ---- begin view event handlers + + public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + return this._onOptionsMaybeChanged(); + } + public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { + this._selections = e.selections; + this._minimapSelections = null; + return this._actual.onSelectionChanged(); + } + public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { + if (e.affectsMinimap) { + return this._actual.onDecorationsChanged(); + } + return false; + } + public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { + return this._actual.onFlushed(); + } + public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { + if (this._samplingState) { + const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(e.fromLineNumber, e.toLineNumber); + if (minimapLineRange) { + return this._actual.onLinesChanged(minimapLineRange[0], minimapLineRange[1]); + } else { + return false; + } + } else { + return this._actual.onLinesChanged(e.fromLineNumber, e.toLineNumber); + } + } + public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { + if (this._samplingState) { + const [changeStartIndex, changeEndIndex] = this._samplingState.onLinesDeleted(e); + if (changeStartIndex <= changeEndIndex) { + this._actual.onLinesChanged(changeStartIndex + 1, changeEndIndex + 1); + } + this._shouldCheckSampling = true; + return true; + } else { + return this._actual.onLinesDeleted(e.fromLineNumber, e.toLineNumber); + } + } + public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { + if (this._samplingState) { + this._samplingState.onLinesInserted(e); + this._shouldCheckSampling = true; + return true; + } else { + return this._actual.onLinesInserted(e.fromLineNumber, e.toLineNumber); + } + } + public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { + return this._actual.onScrollChanged(); + } + public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { + this._context.model.invalidateMinimapColorCache(); + this._actual.onThemeChanged(); + this._onOptionsMaybeChanged(); + return true; + } + public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean { + if (this._samplingState) { + let ranges: { fromLineNumber: number; toLineNumber: number; }[] = []; + for (const range of e.ranges) { + const minimapLineRange = this._samplingState.modelLineRangeToMinimapLineRange(range.fromLineNumber, range.toLineNumber); + if (minimapLineRange) { + ranges.push({ fromLineNumber: minimapLineRange[0], toLineNumber: minimapLineRange[1] }); + } + } + if (ranges.length) { + return this._actual.onTokensChanged(ranges); + } else { + return false; + } + } else { + return this._actual.onTokensChanged(e.ranges); + } + } + public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean { + return this._actual.onTokensColorsChanged(); + } + public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { + return this._actual.onZonesChanged(); + } + + // --- end event handlers + + public prepareRender(ctx: RenderingContext): void { + if (this._shouldCheckSampling) { + this._shouldCheckSampling = false; + this._recreateLineSampling(); + } + } + + public render(ctx: RestrictedRenderingContext): void { + let viewportStartLineNumber = ctx.visibleRange.startLineNumber; + let viewportEndLineNumber = ctx.visibleRange.endLineNumber; + + if (this._samplingState) { + viewportStartLineNumber = this._samplingState.modelLineToMinimapLine(viewportStartLineNumber); + viewportEndLineNumber = this._samplingState.modelLineToMinimapLine(viewportEndLineNumber); + } + + const minimapCtx: IMinimapRenderingContext = { + viewportContainsWhitespaceGaps: (ctx.viewportData.whitespaceViewportData.length > 0), + + scrollWidth: ctx.scrollWidth, + scrollHeight: ctx.scrollHeight, + + viewportStartLineNumber: viewportStartLineNumber, + viewportEndLineNumber: viewportEndLineNumber, + + scrollTop: ctx.scrollTop, + scrollLeft: ctx.scrollLeft, + + viewportWidth: ctx.viewportWidth, + viewportHeight: ctx.viewportHeight, + }; + this._actual.render(minimapCtx); + } + + //#region IMinimapModel + + private _recreateLineSampling(): void { + this._minimapSelections = null; + + const wasSampling = Boolean(this._samplingState); + const [samplingState, events] = MinimapSamplingState.compute(this._context.configuration.options, this._context.model.getLineCount(), this._samplingState); + this._samplingState = samplingState; + + if (wasSampling && this._samplingState) { + // was sampling, is sampling + for (const event of events) { + switch (event.type) { + case 'deleted': + this._actual.onLinesDeleted(event.deleteFromLineNumber, event.deleteToLineNumber); + break; + case 'inserted': + this._actual.onLinesInserted(event.insertFromLineNumber, event.insertToLineNumber); + break; + case 'flush': + this._actual.onFlushed(); + break; + } + } + } + } + + public getLineCount(): number { + if (this._samplingState) { + return this._samplingState.minimapLines.length; + } + return this._context.model.getLineCount(); + } + + public getRealLineCount(): number { + return this._context.model.getLineCount(); + } + + public getLineContent(lineNumber: number): string { + if (this._samplingState) { + return this._context.model.getLineContent(this._samplingState.minimapLines[lineNumber - 1]); + } + return this._context.model.getLineContent(lineNumber); + } + + public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[] { + if (this._samplingState) { + let result: (ViewLineData | null)[] = []; + for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) { + if (needed[lineIndex]) { + result[lineIndex] = this._context.model.getViewLineData(this._samplingState.minimapLines[startLineNumber + lineIndex - 1]); + } else { + result[lineIndex] = null; + } + } + return result; + } + return this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed).data; + } + + public getSelections(): Selection[] { + if (this._minimapSelections === null) { + if (this._samplingState) { + this._minimapSelections = []; + for (const selection of this._selections) { + const [minimapLineStart, minimapLineEnd] = this._samplingState.decorationLineRangeToMinimapLineRange(selection.startLineNumber, selection.endLineNumber); + this._minimapSelections.push(new Selection(minimapLineStart, selection.startColumn, minimapLineEnd, selection.endColumn)); + } + } else { + this._minimapSelections = this._selections; + } + } + return this._minimapSelections; + } + + public getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] { + let visibleRange: Range; + if (this._samplingState) { + const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1]; + const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1]; + visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.model.getLineMaxColumn(modelEndLineNumber)); + } else { + visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.model.getLineMaxColumn(endLineNumber)); + } + const decorations = this._context.model.getDecorationsInViewport(visibleRange); + + if (this._samplingState) { + let result: ViewModelDecoration[] = []; + for (const decoration of decorations) { + if (!decoration.options.minimap) { + continue; + } + const range = decoration.range; + const minimapStartLineNumber = this._samplingState.modelLineToMinimapLine(range.startLineNumber); + const minimapEndLineNumber = this._samplingState.modelLineToMinimapLine(range.endLineNumber); + result.push(new ViewModelDecoration(new Range(minimapStartLineNumber, range.startColumn, minimapEndLineNumber, range.endColumn), decoration.options)); + } + return result; + } + return decorations; + } + + public getOptions(): TextModelResolvedOptions { + return this._context.model.getOptions(); + } + + public revealLineNumber(lineNumber: number): void { + if (this._samplingState) { + lineNumber = this._samplingState.minimapLines[lineNumber - 1]; + } + this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( + 'mouse', + new Range(lineNumber, 1, lineNumber, 1), + viewEvents.VerticalRevealType.Center, + false, + ScrollType.Smooth + )); + } + + public setScrollTop(scrollTop: number): void { + this._context.viewLayout.setScrollPositionNow({ + scrollTop: scrollTop + }); + } + + //#endregion +} + +class InnerMinimap extends Disposable { + + private readonly _theme: EditorTheme; + private readonly _model: IMinimapModel; - private readonly _tokensColorTracker: MinimapTokensColorTracker; private readonly _domNode: FastDomNode; private readonly _shadow: FastDomNode; private readonly _canvas: FastDomNode; @@ -475,22 +1039,24 @@ export class Minimap extends ViewPart { private readonly _sliderTouchMoveListener: IDisposable; private readonly _sliderTouchEndListener: IDisposable; - private _options: MinimapOptions; private _lastRenderData: RenderData | null; - private _selections: Selection[] = []; private _selectionColor: Color | undefined; private _renderDecorations: boolean = false; private _gestureInProgress: boolean = false; private _buffers: MinimapBuffers | null; - constructor(context: ViewContext) { - super(context); + constructor( + theme: EditorTheme, + model: IMinimapModel + ) { + super(); + + this._theme = theme; + this._model = model; - this._tokensColorTracker = MinimapTokensColorTracker.getInstance(); - this._options = new MinimapOptions(this._context.configuration, this._context.theme, this._tokensColorTracker); this._lastRenderData = null; this._buffers = null; - this._selectionColor = this._context.theme.getColor(minimapSelection); + this._selectionColor = this._theme.getColor(minimapSelection); this._domNode = createFastDomNode(document.createElement('div')); PartFingerprints.write(this._domNode, PartFingerprint.Minimap); @@ -531,27 +1097,30 @@ export class Minimap extends ViewPart { this._mouseDownListener = dom.addStandardDisposableListener(this._domNode.domNode, 'mousedown', (e) => { e.preventDefault(); - const renderMinimap = this._options.renderMinimap; + const renderMinimap = this._model.options.renderMinimap; if (renderMinimap === RenderMinimap.None) { return; } if (!this._lastRenderData) { return; } - const minimapLineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale); - const internalOffsetY = this._options.pixelRatio * e.browserEvent.offsetY; + if (this._model.options.minimapHeightIsEditorHeight) { + if (e.leftButton && this._lastRenderData) { + // pretend the click occured in the center of the slider + const position = dom.getDomNodePagePosition(this._slider.domNode); + const initialPosY = position.top + position.height / 2; + this._startSliderDragging(e.buttons, e.posx, initialPosY, e.posy, this._lastRenderData.renderedLayout); + } + return; + } + const minimapLineHeight = this._model.options.minimapLineHeight; + const internalOffsetY = (this._model.options.canvasInnerHeight / this._model.options.canvasOuterHeight) * e.browserEvent.offsetY; const lineIndex = Math.floor(internalOffsetY / minimapLineHeight); let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber; - lineNumber = Math.min(lineNumber, this._context.model.getLineCount()); + lineNumber = Math.min(lineNumber, this._model.getLineCount()); - this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( - 'mouse', - new Range(lineNumber, 1, lineNumber, 1), - viewEvents.VerticalRevealType.Center, - false, - ScrollType.Smooth - )); + this._model.revealLineNumber(lineNumber); }); this._sliderMouseMoveMonitor = new GlobalMouseMoveMonitor(); @@ -560,36 +1129,7 @@ export class Minimap extends ViewPart { e.preventDefault(); e.stopPropagation(); if (e.leftButton && this._lastRenderData) { - - const initialMousePosition = e.posy; - const initialMouseOrthogonalPosition = e.posx; - const initialSliderState = this._lastRenderData.renderedLayout; - this._slider.toggleClassName('active', true); - - this._sliderMouseMoveMonitor.startMonitoring( - e.target, - e.buttons, - standardMouseMoveMerger, - (mouseMoveData: IStandardMouseMoveEventData) => { - const mouseOrthogonalDelta = Math.abs(mouseMoveData.posx - initialMouseOrthogonalPosition); - - if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) { - // The mouse has wondered away from the scrollbar => reset dragging - this._context.viewLayout.setScrollPositionNow({ - scrollTop: initialSliderState.scrollTop - }); - return; - } - - const mouseDelta = mouseMoveData.posy - initialMousePosition; - this._context.viewLayout.setScrollPositionNow({ - scrollTop: initialSliderState.getDesiredScrollTopFromDelta(mouseDelta) - }); - }, - () => { - this._slider.toggleClassName('active', false); - } - ); + this._startSliderDragging(e.buttons, e.posx, e.posy, e.posy, this._lastRenderData.renderedLayout); } }); @@ -620,12 +1160,41 @@ export class Minimap extends ViewPart { }); } + private _startSliderDragging(initialButtons: number, initialPosX: number, initialPosY: number, posy: number, initialSliderState: MinimapLayout): void { + this._slider.toggleClassName('active', true); + + const handleMouseMove = (posy: number, posx: number) => { + const mouseOrthogonalDelta = Math.abs(posx - initialPosX); + + if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) { + // The mouse has wondered away from the scrollbar => reset dragging + this._model.setScrollTop(initialSliderState.scrollTop); + return; + } + + const mouseDelta = posy - initialPosY; + this._model.setScrollTop(initialSliderState.getDesiredScrollTopFromDelta(mouseDelta)); + }; + + if (posy !== initialPosY) { + handleMouseMove(posy, initialPosX); + } + + this._sliderMouseMoveMonitor.startMonitoring( + this._slider.domNode, + initialButtons, + standardMouseMoveMerger, + (mouseMoveData: IStandardMouseMoveEventData) => handleMouseMove(mouseMoveData.posy, mouseMoveData.posx), + () => { + this._slider.toggleClassName('active', false); + } + ); + } + private scrollDueToTouchEvent(touch: GestureEvent) { const startY = this._domNode.domNode.getBoundingClientRect().top; const scrollTop = this._lastRenderData!.renderedLayout.getDesiredScrollTopFromTouchLocation(touch.pageY - startY); - this._context.viewLayout.setScrollPositionNow({ - scrollTop: scrollTop - }); + this._model.setScrollTop(scrollTop); } public dispose(): void { @@ -640,7 +1209,7 @@ export class Minimap extends ViewPart { } private _getMinimapDomNodeClassName(): string { - if (this._options.showSlider === 'always') { + if (this._model.options.showSlider === 'always') { return 'minimap slider-always'; } return 'minimap slider-mouseover'; @@ -651,124 +1220,105 @@ export class Minimap extends ViewPart { } private _applyLayout(): void { - this._domNode.setLeft(this._options.minimapLeft); - this._domNode.setWidth(this._options.minimapWidth); - this._domNode.setHeight(this._options.minimapHeight); - this._shadow.setHeight(this._options.minimapHeight); + this._domNode.setLeft(this._model.options.minimapLeft); + this._domNode.setWidth(this._model.options.minimapWidth); + this._domNode.setHeight(this._model.options.minimapHeight); + this._shadow.setHeight(this._model.options.minimapHeight); - this._canvas.setWidth(this._options.canvasOuterWidth); - this._canvas.setHeight(this._options.canvasOuterHeight); - this._canvas.domNode.width = this._options.canvasInnerWidth; - this._canvas.domNode.height = this._options.canvasInnerHeight; + this._canvas.setWidth(this._model.options.canvasOuterWidth); + this._canvas.setHeight(this._model.options.canvasOuterHeight); + this._canvas.domNode.width = this._model.options.canvasInnerWidth; + this._canvas.domNode.height = this._model.options.canvasInnerHeight; - this._decorationsCanvas.setWidth(this._options.canvasOuterWidth); - this._decorationsCanvas.setHeight(this._options.canvasOuterHeight); - this._decorationsCanvas.domNode.width = this._options.canvasInnerWidth; - this._decorationsCanvas.domNode.height = this._options.canvasInnerHeight; + this._decorationsCanvas.setWidth(this._model.options.canvasOuterWidth); + this._decorationsCanvas.setHeight(this._model.options.canvasOuterHeight); + this._decorationsCanvas.domNode.width = this._model.options.canvasInnerWidth; + this._decorationsCanvas.domNode.height = this._model.options.canvasInnerHeight; - this._slider.setWidth(this._options.minimapWidth); + this._slider.setWidth(this._model.options.minimapWidth); } private _getBuffer(): ImageData | null { if (!this._buffers) { - if (this._options.canvasInnerWidth > 0 && this._options.canvasInnerHeight > 0) { + if (this._model.options.canvasInnerWidth > 0 && this._model.options.canvasInnerHeight > 0) { this._buffers = new MinimapBuffers( this._canvas.domNode.getContext('2d')!, - this._options.canvasInnerWidth, - this._options.canvasInnerHeight, - this._options.backgroundColor + this._model.options.canvasInnerWidth, + this._model.options.canvasInnerHeight, + this._model.options.backgroundColor ); } } return this._buffers ? this._buffers.getBuffer() : null; } - private _onOptionsMaybeChanged(): boolean { - const opts = new MinimapOptions(this._context.configuration, this._context.theme, this._tokensColorTracker); - if (this._options.equals(opts)) { - return false; - } - this._options = opts; + // ---- begin view event handlers + + public onDidChangeOptions(): void { this._lastRenderData = null; this._buffers = null; this._applyLayout(); this._domNode.setClassName(this._getMinimapDomNodeClassName()); - return true; } - - // ---- begin view event handlers - - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - return this._onOptionsMaybeChanged(); - } - public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { - this._selections = e.selections; + public onSelectionChanged(): boolean { this._renderDecorations = true; return true; } - public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { + public onDecorationsChanged(): boolean { + this._renderDecorations = true; + return true; + } + public onFlushed(): boolean { this._lastRenderData = null; return true; } - public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { + public onLinesChanged(changeFromLineNumber: number, changeToLineNumber: number): boolean { if (this._lastRenderData) { - return this._lastRenderData.onLinesChanged(e); + return this._lastRenderData.onLinesChanged(changeFromLineNumber, changeToLineNumber); } return false; } - public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { + public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): boolean { if (this._lastRenderData) { - this._lastRenderData.onLinesDeleted(e); + this._lastRenderData.onLinesDeleted(deleteFromLineNumber, deleteToLineNumber); } return true; } - public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { + public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): boolean { if (this._lastRenderData) { - this._lastRenderData.onLinesInserted(e); + this._lastRenderData.onLinesInserted(insertFromLineNumber, insertToLineNumber); } return true; } - public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { + public onScrollChanged(): boolean { this._renderDecorations = true; return true; } - public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean { + public onThemeChanged(): boolean { + this._selectionColor = this._theme.getColor(minimapSelection); + this._renderDecorations = true; + return true; + } + public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number; }[]): boolean { if (this._lastRenderData) { - return this._lastRenderData.onTokensChanged(e); + return this._lastRenderData.onTokensChanged(ranges); } return false; } - public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean { + public onTokensColorsChanged(): boolean { this._lastRenderData = null; this._buffers = null; return true; } - public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { + public onZonesChanged(): boolean { this._lastRenderData = null; return true; } - public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { - this._renderDecorations = true; - return true; - } - - public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { - this._context.model.invalidateMinimapColorCache(); - this._selectionColor = this._context.theme.getColor(minimapSelection); - this._renderDecorations = true; - this._onOptionsMaybeChanged(); - return true; - } - // --- end event handlers - public prepareRender(ctx: RenderingContext): void { - // Nothing to read - } - - public render(renderingCtx: RestrictedRenderingContext): void { - const renderMinimap = this._options.renderMinimap; + public render(renderingCtx: IMinimapRenderingContext): void { + const renderMinimap = this._model.options.renderMinimap; if (renderMinimap === RenderMinimap.None) { this._shadow.setClassName('minimap-shadow-hidden'); this._sliderHorizontal.setWidth(0); @@ -782,24 +1332,26 @@ export class Minimap extends ViewPart { } const layout = MinimapLayout.create( - this._options, - renderingCtx.visibleRange.startLineNumber, - renderingCtx.visibleRange.endLineNumber, + this._model.options, + renderingCtx.viewportStartLineNumber, + renderingCtx.viewportEndLineNumber, renderingCtx.viewportHeight, - (renderingCtx.viewportData.whitespaceViewportData.length > 0), - this._context.model.getLineCount(), + renderingCtx.viewportContainsWhitespaceGaps, + this._model.getLineCount(), + this._model.getRealLineCount(), renderingCtx.scrollTop, renderingCtx.scrollHeight, this._lastRenderData ? this._lastRenderData.renderedLayout : null ); + this._slider.setDisplay(layout.sliderNeeded ? 'block' : 'none'); this._slider.setTop(layout.sliderTop); this._slider.setHeight(layout.sliderHeight); // Compute horizontal slider coordinates - const scrollLeftChars = renderingCtx.scrollLeft / this._options.typicalHalfwidthCharacterWidth; - const horizontalSliderLeft = Math.min(this._options.minimapWidth, Math.round(scrollLeftChars * getMinimapCharWidth(this._options.renderMinimap, this._options.fontScale) / this._options.pixelRatio)); + const scrollLeftChars = renderingCtx.scrollLeft / this._model.options.typicalHalfwidthCharacterWidth; + const horizontalSliderLeft = Math.min(this._model.options.minimapWidth, Math.round(scrollLeftChars * this._model.options.minimapCharWidth / this._model.options.pixelRatio)); this._sliderHorizontal.setLeft(horizontalSliderLeft); - this._sliderHorizontal.setWidth(this._options.minimapWidth - horizontalSliderLeft); + this._sliderHorizontal.setWidth(this._model.options.minimapWidth - horizontalSliderLeft); this._sliderHorizontal.setTop(0); this._sliderHorizontal.setHeight(layout.sliderHeight); @@ -810,19 +1362,20 @@ export class Minimap extends ViewPart { private renderDecorations(layout: MinimapLayout) { if (this._renderDecorations) { this._renderDecorations = false; - const decorations = this._context.model.getDecorationsInViewport(new Range(layout.startLineNumber, 1, layout.endLineNumber, this._context.model.getLineMaxColumn(layout.endLineNumber))); + const selections = this._model.getSelections(); + const decorations = this._model.getMinimapDecorationsInViewport(layout.startLineNumber, layout.endLineNumber); - const { renderMinimap, canvasInnerWidth, canvasInnerHeight } = this._options; - const lineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale); - const characterWidth = getMinimapCharWidth(renderMinimap, this._options.fontScale); - const tabSize = this._context.model.getOptions().tabSize; + const { canvasInnerWidth, canvasInnerHeight } = this._model.options; + const lineHeight = this._model.options.minimapLineHeight; + const characterWidth = this._model.options.minimapCharWidth; + const tabSize = this._model.getOptions().tabSize; const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!; canvasContext.clearRect(0, 0, canvasInnerWidth, canvasInnerHeight); const lineOffsetMap = new Map(); - for (let i = 0; i < this._selections.length; i++) { - const selection = this._selections[i]; + for (let i = 0; i < selections.length; i++) { + const selection = selections[i]; for (let line = selection.startLineNumber; line <= selection.endLineNumber; line++) { this.renderDecorationOnLine(canvasContext, lineOffsetMap, selection, this._selectionColor, layout, line, lineHeight, lineHeight, tabSize, characterWidth); @@ -837,7 +1390,7 @@ export class Minimap extends ViewPart { continue; } - const decorationColor = (decoration.options.minimap).getColor(this._context.theme); + const decorationColor = (decoration.options.minimap).getColor(this._theme); for (let line = decoration.range.startLineNumber; line <= decoration.range.endLineNumber; line++) { switch (decoration.options.minimap.position) { @@ -869,7 +1422,7 @@ export class Minimap extends ViewPart { const y = (lineNumber - layout.startLineNumber) * lineHeight; // Skip rendering the line if it's vertically outside our viewport - if (y + height < 0 || y > this._options.canvasInnerHeight) { + if (y + height < 0 || y > this._model.options.canvasInnerHeight) { return; } @@ -877,7 +1430,7 @@ export class Minimap extends ViewPart { let lineIndexToXOffset = lineOffsetMap.get(lineNumber); const isFirstDecorationForLine = !lineIndexToXOffset; if (!lineIndexToXOffset) { - const lineData = this._context.model.getLineContent(lineNumber); + const lineData = this._model.getLineContent(lineNumber); lineIndexToXOffset = [MINIMAP_GUTTER_WIDTH]; for (let i = 1; i < lineData.length + 1; i++) { const charCode = lineData.charCodeAt(i - 1); @@ -922,11 +1475,9 @@ export class Minimap extends ViewPart { } private renderLines(layout: MinimapLayout): RenderData | null { - const renderMinimap = this._options.renderMinimap; - const charRenderer = this._options.charRenderer(); const startLineNumber = layout.startLineNumber; const endLineNumber = layout.endLineNumber; - const minimapLineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale); + const minimapLineHeight = this._model.options.minimapLineHeight; // Check if nothing changed w.r.t. lines from last frame if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) { @@ -944,7 +1495,7 @@ export class Minimap extends ViewPart { } // Render untouched lines by using last rendered data. - let [_dirtyY1, _dirtyY2, needed] = Minimap._renderUntouchedLines( + let [_dirtyY1, _dirtyY2, needed] = InnerMinimap._renderUntouchedLines( imageData, startLineNumber, endLineNumber, @@ -953,27 +1504,39 @@ export class Minimap extends ViewPart { ); // Fetch rendering info from view model for rest of lines that need rendering. - const lineInfo = this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed); - const tabSize = lineInfo.tabSize; - const background = this._options.backgroundColor; - const useLighterFont = this._tokensColorTracker.backgroundIsLight(); + const lineInfo = this._model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed); + const tabSize = this._model.getOptions().tabSize; + const background = this._model.options.backgroundColor; + const tokensColorTracker = this._model.tokensColorTracker; + const useLighterFont = tokensColorTracker.backgroundIsLight(); + const renderMinimap = this._model.options.renderMinimap; + const charRenderer = this._model.options.charRenderer(); + const fontScale = this._model.options.fontScale; + const minimapCharWidth = this._model.options.minimapCharWidth; + + const baseCharHeight = (renderMinimap === RenderMinimap.Text ? Constants.BASE_CHAR_HEIGHT : Constants.BASE_CHAR_HEIGHT + 1); + const renderMinimapLineHeight = baseCharHeight * fontScale; + const innerLinePadding = (minimapLineHeight > renderMinimapLineHeight ? Math.floor((minimapLineHeight - renderMinimapLineHeight) / 2) : 0); // Render the rest of lines let dy = 0; const renderedLines: MinimapLine[] = []; for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) { if (needed[lineIndex]) { - Minimap._renderLine( + InnerMinimap._renderLine( imageData, background, useLighterFont, renderMinimap, - this._tokensColorTracker, + minimapCharWidth, + tokensColorTracker, charRenderer, dy, + innerLinePadding, tabSize, - lineInfo.data[lineIndex]!, - this._options.fontScale + lineInfo[lineIndex]!, + fontScale, + minimapLineHeight ); } renderedLines[lineIndex] = new MinimapLine(dy); @@ -1093,17 +1656,20 @@ export class Minimap extends ViewPart { backgroundColor: RGBA8, useLighterFont: boolean, renderMinimap: RenderMinimap, + charWidth: number, colorTracker: MinimapTokensColorTracker, minimapCharRenderer: MinimapCharRenderer, dy: number, + innerLinePadding: number, tabSize: number, lineData: ViewLineData, - fontScale: number + fontScale: number, + minimapLineHeight: number ): void { const content = lineData.content; const tokens = lineData.tokens; - const charWidth = getMinimapCharWidth(renderMinimap, fontScale); const maxDx = target.width - charWidth; + const force1pxHeight = (minimapLineHeight === 1); let dx = MINIMAP_GUTTER_WIDTH; let charIndex = 0; @@ -1135,9 +1701,9 @@ export class Minimap extends ViewPart { for (let i = 0; i < count; i++) { if (renderMinimap === RenderMinimap.Blocks) { - minimapCharRenderer.blockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont); + minimapCharRenderer.blockRenderChar(target, dx, dy + innerLinePadding, tokenColor, backgroundColor, useLighterFont, force1pxHeight); } else { // RenderMinimap.Text - minimapCharRenderer.renderChar(target, dx, dy, charCode, tokenColor, backgroundColor, fontScale, useLighterFont); + minimapCharRenderer.renderChar(target, dx, dy + innerLinePadding, charCode, tokenColor, backgroundColor, fontScale, useLighterFont, force1pxHeight); } dx += charWidth; @@ -1158,20 +1724,17 @@ registerThemingParticipant((theme, collector) => { if (minimapBackgroundValue) { collector.addRule(`.monaco-editor .minimap > canvas { opacity: ${minimapBackgroundValue.rgba.a}; will-change: opacity; }`); } - const sliderBackground = theme.getColor(scrollbarSliderBackground); + const sliderBackground = theme.getColor(minimapSliderBackground); if (sliderBackground) { - const halfSliderBackground = sliderBackground.transparent(0.5); - collector.addRule(`.monaco-editor .minimap-slider, .monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${halfSliderBackground}; }`); + collector.addRule(`.monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${sliderBackground}; }`); } - const sliderHoverBackground = theme.getColor(scrollbarSliderHoverBackground); + const sliderHoverBackground = theme.getColor(minimapSliderHoverBackground); if (sliderHoverBackground) { - const halfSliderHoverBackground = sliderHoverBackground.transparent(0.5); - collector.addRule(`.monaco-editor .minimap-slider:hover, .monaco-editor .minimap-slider:hover .minimap-slider-horizontal { background: ${halfSliderHoverBackground}; }`); + collector.addRule(`.monaco-editor .minimap-slider:hover .minimap-slider-horizontal { background: ${sliderHoverBackground}; }`); } - const sliderActiveBackground = theme.getColor(scrollbarSliderActiveBackground); + const sliderActiveBackground = theme.getColor(minimapSliderActiveBackground); if (sliderActiveBackground) { - const halfSliderActiveBackground = sliderActiveBackground.transparent(0.5); - collector.addRule(`.monaco-editor .minimap-slider.active, .monaco-editor .minimap-slider.active .minimap-slider-horizontal { background: ${halfSliderActiveBackground}; }`); + collector.addRule(`.monaco-editor .minimap-slider.active .minimap-slider-horizontal { background: ${sliderActiveBackground}; }`); } const shadow = theme.getColor(scrollbarShadow); if (shadow) { diff --git a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts index 9b44e5a7f0..01252136ca 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts @@ -34,11 +34,13 @@ export class MinimapCharRenderer { color: RGBA8, backgroundColor: RGBA8, fontScale: number, - useLighterFont: boolean + useLighterFont: boolean, + force1pxHeight: boolean ): void { const charWidth = Constants.BASE_CHAR_WIDTH * this.scale; const charHeight = Constants.BASE_CHAR_HEIGHT * this.scale; - if (dx + charWidth > target.width || dy + charHeight > target.height) { + const renderHeight = (force1pxHeight ? 1 : charHeight); + if (dx + charWidth > target.width || dy + renderHeight > target.height) { console.warn('bad render request outside image data'); return; } @@ -60,7 +62,7 @@ export class MinimapCharRenderer { let sourceOffset = charIndex * charWidth * charHeight; let row = dy * destWidth + dx * Constants.RGBA_CHANNELS_CNT; - for (let y = 0; y < charHeight; y++) { + for (let y = 0; y < renderHeight; y++) { let column = row; for (let x = 0; x < charWidth; x++) { const c = charData[sourceOffset++] / 255; @@ -80,11 +82,13 @@ export class MinimapCharRenderer { dy: number, color: RGBA8, backgroundColor: RGBA8, - useLighterFont: boolean + useLighterFont: boolean, + force1pxHeight: boolean ): void { const charWidth = Constants.BASE_CHAR_WIDTH * this.scale; const charHeight = Constants.BASE_CHAR_HEIGHT * this.scale; - if (dx + charWidth > target.width || dy + charHeight > target.height) { + const renderHeight = (force1pxHeight ? 1 : charHeight); + if (dx + charWidth > target.width || dy + renderHeight > target.height) { console.warn('bad render request outside image data'); return; } @@ -108,7 +112,7 @@ export class MinimapCharRenderer { const dest = target.data; let row = dy * destWidth + dx * Constants.RGBA_CHANNELS_CNT; - for (let y = 0; y < charHeight; y++) { + for (let y = 0; y < renderHeight; y++) { let column = row; for (let x = 0; x < charWidth; x++) { dest[column++] = colorR; diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index efedcc7411..73cc1b15c5 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -276,7 +276,10 @@ export class DecorationsOverviewRuler extends ViewPart { return true; } public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { - return true; + if (e.affectsOverviewRuler) { + return true; + } + return false; } public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index c56cacffb4..7e43c5b42b 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -287,6 +287,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC public options!: ComputedEditorOptions; private _isDominatedByLongLines: boolean; + private _maxLineNumber: number; private _lineNumbersDigitCount: number; private _rawOptions: IEditorOptions; @@ -298,6 +299,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC this.isSimpleWidget = isSimpleWidget; this._isDominatedByLongLines = false; + this._maxLineNumber = 1; this._lineNumbersDigitCount = 1; this._rawOptions = deepCloneAndMigrateOptions(_options); @@ -347,6 +349,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC fontInfo: this.readConfiguration(bareFontInfo), extraEditorClassName: partialEnv.extraEditorClassName, isDominatedByLongLines: this._isDominatedByLongLines, + maxLineNumber: this._maxLineNumber, lineNumbersDigitCount: this._lineNumbersDigitCount, emptySelectionClipboard: partialEnv.emptySelectionClipboard, pixelRatio: partialEnv.pixelRatio, @@ -405,11 +408,11 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC } public setMaxLineNumber(maxLineNumber: number): void { - let digitCount = CommonEditorConfiguration._digitCount(maxLineNumber); - if (this._lineNumbersDigitCount === digitCount) { + if (this._maxLineNumber === maxLineNumber) { return; } - this._lineNumbersDigitCount = digitCount; + this._maxLineNumber = maxLineNumber; + this._lineNumbersDigitCount = CommonEditorConfiguration._digitCount(maxLineNumber); this._recomputeOptions(); } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 3807fd74a8..700d614992 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -678,6 +678,7 @@ export interface IEnvironmentalOptions { readonly fontInfo: FontInfo; readonly extraEditorClassName: string; readonly isDominatedByLongLines: boolean; + readonly maxLineNumber: number; readonly lineNumbersDigitCount: number; readonly emptySelectionClipboard: boolean; readonly pixelRatio: number; @@ -1691,6 +1692,14 @@ export interface EditorLayoutInfo { * The width of the minimap */ readonly minimapWidth: number; + readonly minimapHeightIsEditorHeight: boolean; + readonly minimapIsSampling: boolean; + readonly minimapScale: number; + readonly minimapLineHeight: number; + readonly minimapCanvasInnerWidth: number; + readonly minimapCanvasInnerHeight: number; + readonly minimapCanvasOuterWidth: number; + readonly minimapCanvasOuterHeight: number; /** * Minimap render type @@ -1724,6 +1733,7 @@ export interface EditorLayoutInfoComputerEnv { outerWidth: number; outerHeight: number; lineHeight: number; + maxLineNumber: number; lineNumbersDigitCount: number; typicalHalfwidthCharacterWidth: number; maxDigitWidth: number; @@ -1747,6 +1757,7 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption= 2 ? Math.round(minimap.scale * 2) : minimap.scale); + let minimapScale = (pixelRatio >= 2 ? Math.round(minimap.scale * 2) : minimap.scale); const minimapMaxColumn = minimap.maxColumn | 0; + const minimapMode = minimap.mode; const scrollbar = options.get(EditorOption.scrollbar); const verticalScrollbarWidth = scrollbar.verticalScrollbarSize | 0; @@ -1811,19 +1838,65 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption 1) { + minimapHeightIsEditorHeight = true; + minimapIsSampling = true; + minimapScale = 1; + minimapLineHeight = 1; + minimapCharWidth = minimapScale / pixelRatio; + } else { + const effectiveMinimapHeight = Math.ceil((modelLineCount + extraLinesBeyondLastLine) * minimapLineHeight); + if (minimapMode === 'cover' || effectiveMinimapHeight > minimapCanvasInnerHeight) { + minimapHeightIsEditorHeight = true; + const configuredFontScale = minimapScale; + minimapLineHeight = Math.min(lineHeight * pixelRatio, Math.max(1, Math.floor(1 / desiredRatio))); + minimapScale = Math.min(configuredFontScale + 1, Math.max(1, Math.floor(minimapLineHeight / baseCharHeight))); + if (minimapScale > configuredFontScale) { + minimapWidthMultiplier = Math.min(2, minimapScale / configuredFontScale); + } + minimapCharWidth = minimapScale / pixelRatio / minimapWidthMultiplier; + minimapCanvasInnerHeight = Math.ceil((Math.max(typicalViewportLineCount, modelLineCount + extraLinesBeyondLastLine)) * minimapLineHeight); + } + } + } + renderMinimap = minimapRenderCharacters ? RenderMinimap.Text : RenderMinimap.Blocks; // Given: @@ -1855,6 +1928,10 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption(input.mode, this.defaultValue.mode, ['actual', 'cover', 'contain']), side: EditorStringEnumOption.stringSet<'right' | 'left'>(input.side, this.defaultValue.side, ['right', 'left']), showSlider: EditorStringEnumOption.stringSet<'always' | 'mouseover'>(input.showSlider, this.defaultValue.showSlider, ['always', 'mouseover']), renderCharacters: EditorBooleanOption.boolean(input.renderCharacters, this.defaultValue.renderCharacters), diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index edabe24555..91787ebfca 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1423,19 +1423,15 @@ export class TextModel extends Disposable implements model.ITextModel { private _changeDecorations(ownerId: number, callback: (changeAccessor: model.IModelDecorationsChangeAccessor) => T): T | null { let changeAccessor: model.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: model.IModelDecorationOptions): string => { - this._onDidChangeDecorations.fire(); return this._deltaDecorationsImpl(ownerId, [], [{ range: range, options: options }])[0]; }, changeDecoration: (id: string, newRange: IRange): void => { - this._onDidChangeDecorations.fire(); this._changeDecorationImpl(id, newRange); }, changeDecorationOptions: (id: string, options: model.IModelDecorationOptions) => { - this._onDidChangeDecorations.fire(); this._changeDecorationOptionsImpl(id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { - this._onDidChangeDecorations.fire(); this._deltaDecorationsImpl(ownerId, [id], []); }, deltaDecorations: (oldDecorations: string[], newDecorations: model.IModelDeltaDecoration[]): string[] => { @@ -1443,7 +1439,6 @@ export class TextModel extends Disposable implements model.ITextModel { // nothing to do return []; } - this._onDidChangeDecorations.fire(); return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } }; @@ -1474,7 +1469,6 @@ export class TextModel extends Disposable implements model.ITextModel { try { this._onDidChangeDecorations.beginDeferredEmit(); - this._onDidChangeDecorations.fire(); return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } finally { this._onDidChangeDecorations.endDeferredEmit(); @@ -1622,6 +1616,7 @@ export class TextModel extends Disposable implements model.ITextModel { this._decorationsTree.delete(node); node.reset(this.getVersionId(), startOffset, endOffset, range); this._decorationsTree.insert(node); + this._onDidChangeDecorations.checkAffectedAndFire(node.options); } private _changeDecorationOptionsImpl(decorationId: string, options: ModelDecorationOptions): void { @@ -1633,6 +1628,9 @@ export class TextModel extends Disposable implements model.ITextModel { const nodeWasInOverviewRuler = (node.options.overviewRuler && node.options.overviewRuler.color ? true : false); const nodeIsInOverviewRuler = (options.overviewRuler && options.overviewRuler.color ? true : false); + this._onDidChangeDecorations.checkAffectedAndFire(node.options); + this._onDidChangeDecorations.checkAffectedAndFire(options); + if (nodeWasInOverviewRuler !== nodeIsInOverviewRuler) { // Delete + Insert due to an overview ruler status change this._decorationsTree.delete(node); @@ -1666,6 +1664,7 @@ export class TextModel extends Disposable implements model.ITextModel { // (2) remove the node from the tree (if it exists) if (node) { this._decorationsTree.delete(node); + this._onDidChangeDecorations.checkAffectedAndFire(node.options); } } @@ -1688,6 +1687,7 @@ export class TextModel extends Disposable implements model.ITextModel { node.ownerId = ownerId; node.reset(versionId, startOffset, endOffset, range); node.setOptions(options); + this._onDidChangeDecorations.checkAffectedAndFire(options); this._decorationsTree.insert(node); @@ -1713,7 +1713,7 @@ export class TextModel extends Disposable implements model.ITextModel { throw new Error('Illegal value for lineNumber'); } - this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens); + this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens, false); } public setTokens(tokens: MultilineTokens[]): void { @@ -1725,16 +1725,34 @@ export class TextModel extends Disposable implements model.ITextModel { for (let i = 0, len = tokens.length; i < len; i++) { const element = tokens[i]; - ranges.push({ fromLineNumber: element.startLineNumber, toLineNumber: element.startLineNumber + element.tokens.length - 1 }); + let minChangedLineNumber = 0; + let maxChangedLineNumber = 0; + let hasChange = false; for (let j = 0, lenJ = element.tokens.length; j < lenJ; j++) { - this.setLineTokens(element.startLineNumber + j, element.tokens[j]); + const lineNumber = element.startLineNumber + j; + if (hasChange) { + this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], false); + maxChangedLineNumber = lineNumber; + } else { + const lineHasChange = this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], true); + if (lineHasChange) { + hasChange = true; + minChangedLineNumber = lineNumber; + maxChangedLineNumber = lineNumber; + } + } + } + if (hasChange) { + ranges.push({ fromLineNumber: minChangedLineNumber, toLineNumber: maxChangedLineNumber }); } } - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - ranges: ranges - }); + if (ranges.length > 0) { + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + ranges: ranges + }); + } } public setSemanticTokens(tokens: MultilineTokens2[] | null): void { @@ -3083,11 +3101,15 @@ export class DidChangeDecorationsEmitter extends Disposable { private _deferredCnt: number; private _shouldFire: boolean; + private _affectsMinimap: boolean; + private _affectsOverviewRuler: boolean; constructor() { super(); this._deferredCnt = 0; this._shouldFire = false; + this._affectsMinimap = false; + this._affectsOverviewRuler = false; } public beginDeferredEmit(): void { @@ -3098,13 +3120,31 @@ export class DidChangeDecorationsEmitter extends Disposable { this._deferredCnt--; if (this._deferredCnt === 0) { if (this._shouldFire) { + const event: IModelDecorationsChangedEvent = { + affectsMinimap: this._affectsMinimap, + affectsOverviewRuler: this._affectsOverviewRuler, + }; this._shouldFire = false; - this._actual.fire({}); + this._affectsMinimap = false; + this._affectsOverviewRuler = false; + this._actual.fire(event); } } } + public checkAffectedAndFire(options: ModelDecorationOptions): void { + if (!this._affectsMinimap) { + this._affectsMinimap = options.minimap && options.minimap.position ? true : false; + } + if (!this._affectsOverviewRuler) { + this._affectsOverviewRuler = options.overviewRuler && options.overviewRuler.color ? true : false; + } + this._shouldFire = true; + } + public fire(): void { + this._affectsMinimap = true; + this._affectsOverviewRuler = true; this._shouldFire = true; } } diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index dcb0e807ec..feefeaa4cb 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -76,6 +76,8 @@ export interface IModelContentChangedEvent { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { + readonly affectsMinimap: boolean; + readonly affectsOverviewRuler: boolean; } /** diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index d329f0b0ff..7f5a45f96c 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -964,10 +964,35 @@ export class TokensStore { this._len += insertCount; } - public setTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null): void { + public setTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null, checkEquality: boolean): boolean { const tokens = TokensStore._massageTokens(topLevelLanguageId, lineTextLength, _tokens); this._ensureLine(lineIndex); + const oldTokens = this._lineTokens[lineIndex]; this._lineTokens[lineIndex] = tokens; + + if (checkEquality) { + return !TokensStore._equals(oldTokens, tokens); + } + return false; + } + + private static _equals(_a: Uint32Array | ArrayBuffer | null, _b: Uint32Array | ArrayBuffer | null) { + if (!_a || !_b) { + return !_a && !_b; + } + + const a = toUint32Array(_a); + const b = toUint32Array(_b); + + if (a.length !== b.length) { + return false; + } + for (let i = 0, len = a.length; i < len; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; } //#region Editing diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index 151cd64d0a..4e7fa81ede 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -274,6 +274,16 @@ export class LanguageConfigurationRegistryImpl { return ensureValidWordDefinition(value.wordDefinition || null); } + public getWordDefinitions(): [LanguageId, RegExp][] { + let result: [LanguageId, RegExp][] = []; + this._entries.forEach((value, language) => { + if (value) { + result.push([language, value.wordDefinition]); + } + }); + return result; + } + public getFoldingRules(languageId: LanguageId): FoldingRules { let value = this._getRichEditSupport(languageId); if (!value) { diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 4db5ffb766..75156d5e38 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -10,6 +10,7 @@ import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ScrollType, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; +import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; export const enum ViewEventType { ViewConfigurationChanged = 1, @@ -82,8 +83,17 @@ export class ViewDecorationsChangedEvent { public readonly type = ViewEventType.ViewDecorationsChanged; - constructor() { - // Nothing to do + readonly affectsMinimap: boolean; + readonly affectsOverviewRuler: boolean; + + constructor(source: IModelDecorationsChangedEvent | null) { + if (source) { + this.affectsMinimap = source.affectsMinimap; + this.affectsOverviewRuler = source.affectsOverviewRuler; + } else { + this.affectsMinimap = true; + this.affectsOverviewRuler = true; + } } } diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index bca4ad2bd9..d39ebba99c 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -172,7 +172,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (this.lines.setWrappingSettings(fontInfo, wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent)) { eventsCollector.emit(new viewEvents.ViewFlushedEvent()); eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); this.decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); @@ -185,7 +185,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (e.hasChanged(EditorOption.readOnly)) { // Must read again all decorations due to readOnly filtering this.decorations.reset(); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); } eventsCollector.emit(new viewEvents.ViewConfigurationChangedEvent(e)); @@ -291,7 +291,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) { eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); this.decorations.onLineMappingChanged(); } } finally { @@ -354,7 +354,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const eventsCollector = this._beginEmit(); eventsCollector.emit(new viewEvents.ViewFlushedEvent()); eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); } finally { this._endEmit(); } @@ -365,7 +365,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel this.decorations.onModelDecorationsChanged(); try { const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(e)); } finally { this._endEmit(); } @@ -379,7 +379,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (lineMappingChanged) { eventsCollector.emit(new viewEvents.ViewFlushedEvent()); eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); + eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); this.decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); this.viewLayout.onHeightMaybeChanged(); diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 53cdb4b77a..2bdf4ad478 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -419,8 +419,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } if (currentMatch) { const ariaLabel = nls.localize('ariaSearchNoResultWithLineNum', "{0} found for '{1}', at {2}", label, searchString, currentMatch.startLineNumber + ':' + currentMatch.startColumn); - const lineContent = this._codeEditor.getModel()?.getLineContent(currentMatch.startLineNumber); - if (lineContent) { + const model = this._codeEditor.getModel(); + if (model && (currentMatch.startLineNumber <= model.getLineCount()) && (currentMatch.startLineNumber >= 1)) { + const lineContent = model.getLineContent(currentMatch.startLineNumber); return `${lineContent}, ${ariaLabel}`; } diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index 261beeb52a..d9c561b963 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -180,9 +180,11 @@ suite('SmartSelect', () => { // -- bracket selections async function assertRanges(provider: SelectionRangeProvider, value: string, ...expected: IRange[]): Promise { + let index = value.indexOf('|'); + value = value.replace('|', ''); let model = modelService.createModel(value, new StaticLanguageSelector(mode.getLanguageIdentifier()), URI.parse('fake:lang')); - let pos = model.getPositionAt(value.indexOf('|')); + let pos = model.getPositionAt(index); let all = await provider.provideSelectionRanges(model, [pos], CancellationToken.None); let ranges = all![0]; @@ -197,18 +199,18 @@ suite('SmartSelect', () => { test('bracket selection', async () => { await assertRanges(new BracketSelectionRangeProvider(), '(|)', - new Range(1, 2, 1, 3), new Range(1, 1, 1, 4) + new Range(1, 2, 1, 2), new Range(1, 1, 1, 3) ); await assertRanges(new BracketSelectionRangeProvider(), '[[[](|)]]', - new Range(1, 6, 1, 7), new Range(1, 5, 1, 8), // () - new Range(1, 3, 1, 8), new Range(1, 2, 1, 9), // [[]()] - new Range(1, 2, 1, 9), new Range(1, 1, 1, 10), // [[[]()]] + new Range(1, 6, 1, 6), new Range(1, 5, 1, 7), // () + new Range(1, 3, 1, 7), new Range(1, 2, 1, 8), // [[]()] + new Range(1, 2, 1, 8), new Range(1, 1, 1, 9), // [[[]()]] ); await assertRanges(new BracketSelectionRangeProvider(), '[a[](|)a]', - new Range(1, 6, 1, 7), new Range(1, 5, 1, 8), - new Range(1, 2, 1, 9), new Range(1, 1, 1, 10), + new Range(1, 6, 1, 6), new Range(1, 5, 1, 7), + new Range(1, 2, 1, 8), new Range(1, 1, 1, 9), ); // no bracket @@ -219,23 +221,23 @@ suite('SmartSelect', () => { await assertRanges(new BracketSelectionRangeProvider(), '|[[[]()]]'); // edge - await assertRanges(new BracketSelectionRangeProvider(), '[|[[]()]]', new Range(1, 2, 1, 9), new Range(1, 1, 1, 10)); - await assertRanges(new BracketSelectionRangeProvider(), '[[[]()]|]', new Range(1, 2, 1, 9), new Range(1, 1, 1, 10)); + await assertRanges(new BracketSelectionRangeProvider(), '[|[[]()]]', new Range(1, 2, 1, 8), new Range(1, 1, 1, 9)); + await assertRanges(new BracketSelectionRangeProvider(), '[[[]()]|]', new Range(1, 2, 1, 8), new Range(1, 1, 1, 9)); - await assertRanges(new BracketSelectionRangeProvider(), 'aaa(aaa)bbb(b|b)ccc(ccc)', new Range(1, 13, 1, 16), new Range(1, 12, 1, 17)); - await assertRanges(new BracketSelectionRangeProvider(), '(aaa(aaa)bbb(b|b)ccc(ccc))', new Range(1, 14, 1, 17), new Range(1, 13, 1, 18), new Range(1, 2, 1, 26), new Range(1, 1, 1, 27)); + await assertRanges(new BracketSelectionRangeProvider(), 'aaa(aaa)bbb(b|b)ccc(ccc)', new Range(1, 13, 1, 15), new Range(1, 12, 1, 16)); + await assertRanges(new BracketSelectionRangeProvider(), '(aaa(aaa)bbb(b|b)ccc(ccc))', new Range(1, 14, 1, 16), new Range(1, 13, 1, 17), new Range(1, 2, 1, 25), new Range(1, 1, 1, 26)); }); test('bracket with leading/trailing', async () => { await assertRanges(new BracketSelectionRangeProvider(), 'for(a of b){\n foo(|);\n}', - new Range(2, 7, 2, 8), new Range(2, 6, 2, 9), + new Range(2, 7, 2, 7), new Range(2, 6, 2, 8), new Range(1, 13, 3, 1), new Range(1, 12, 3, 2), new Range(1, 1, 3, 2), new Range(1, 1, 3, 2), ); await assertRanges(new BracketSelectionRangeProvider(), 'for(a of b)\n{\n foo(|);\n}', - new Range(3, 7, 3, 8), new Range(3, 6, 3, 9), + new Range(3, 7, 3, 7), new Range(3, 6, 3, 8), new Range(2, 2, 4, 1), new Range(2, 1, 4, 2), new Range(1, 1, 4, 2), new Range(1, 1, 4, 2), ); @@ -244,60 +246,60 @@ suite('SmartSelect', () => { test('in-word ranges', async () => { await assertRanges(new WordSelectionRangeProvider(), 'f|ooBar', - new Range(1, 1, 1, 5), // foo - new Range(1, 1, 1, 8), // fooBar - new Range(1, 1, 1, 8), // doc + new Range(1, 1, 1, 4), // foo + new Range(1, 1, 1, 7), // fooBar + new Range(1, 1, 1, 7), // doc ); await assertRanges(new WordSelectionRangeProvider(), 'f|oo_Ba', - new Range(1, 1, 1, 5), - new Range(1, 1, 1, 8), - new Range(1, 1, 1, 8), + new Range(1, 1, 1, 4), + new Range(1, 1, 1, 7), + new Range(1, 1, 1, 7), ); await assertRanges(new WordSelectionRangeProvider(), 'f|oo-Ba', - new Range(1, 1, 1, 5), - new Range(1, 1, 1, 8), - new Range(1, 1, 1, 8), + new Range(1, 1, 1, 4), + new Range(1, 1, 1, 7), + new Range(1, 1, 1, 7), ); }); test('Default selection should select current word/hump first in camelCase #67493', async function () { await assertRanges(new WordSelectionRangeProvider(), 'Abs|tractSmartSelect', - new Range(1, 1, 1, 10), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 1, 1, 9), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'AbstractSma|rtSelect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'Abstrac-Sma|rt-elect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rt_elect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rt-elect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rtSelect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); }); @@ -321,4 +323,49 @@ suite('SmartSelect', () => { reg.dispose(); }); + + test('Expand selection in words with underscores is inconsistent #90589', async function () { + + await assertRanges(new WordSelectionRangeProvider(), 'Hel|lo_World', + new Range(1, 1, 1, 6), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello_Wo|rld', + new Range(1, 7, 1, 12), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello|_World', + new Range(1, 1, 1, 6), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello_|World', + new Range(1, 7, 1, 12), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello|-World', + new Range(1, 1, 1, 6), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello-|World', + new Range(1, 7, 1, 12), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello|World', + new Range(1, 6, 1, 11), + new Range(1, 1, 1, 11), + new Range(1, 1, 1, 11), + ); + }); }); diff --git a/src/vs/editor/contrib/smartSelect/wordSelections.ts b/src/vs/editor/contrib/smartSelect/wordSelections.ts index 78c7a7252e..938a58e445 100644 --- a/src/vs/editor/contrib/smartSelect/wordSelections.ts +++ b/src/vs/editor/contrib/smartSelect/wordSelections.ts @@ -40,7 +40,7 @@ export class WordSelectionRangeProvider implements SelectionRangeProvider { // LEFT anchor (start) for (; start >= 0; start--) { let ch = word.charCodeAt(start); - if (ch === CharCode.Underline || ch === CharCode.Dash) { + if ((start !== offset) && (ch === CharCode.Underline || ch === CharCode.Dash)) { // foo-bar OR foo_bar break; } else if (isLowerAsciiLetter(ch) && isUpperAsciiLetter(lastCh)) { diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index 80f870c72c..687a5c6c04 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -11,6 +11,8 @@ import { Selection } from 'vs/editor/common/core/selection'; import { deserializePipePositions, serializePipePositions, testRepeatedActionAndExtractPositions } from 'vs/editor/contrib/wordOperations/test/wordTestUtils'; import { CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorWordEndRightSelect, CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect, CursorWordStartLeft, CursorWordStartLeftSelect, CursorWordStartRight, CursorWordStartRightSelect, DeleteWordEndLeft, DeleteWordEndRight, DeleteWordLeft, DeleteWordRight, DeleteWordStartLeft, DeleteWordStartRight, CursorWordAccessibilityLeft, CursorWordAccessibilityLeftSelect, CursorWordAccessibilityRight, CursorWordAccessibilityRightSelect } from 'vs/editor/contrib/wordOperations/wordOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { Handler } from 'vs/editor/common/editorCommon'; +import { Cursor } from 'vs/editor/common/controller/cursor'; suite('WordOperations', () => { @@ -193,6 +195,32 @@ suite('WordOperations', () => { assert.deepEqual(actual, EXPECTED); }); + test('issue #51275 - cursorWordStartLeft does not push undo/redo stack element', () => { + function cursorCommand(cursor: Cursor, command: string, extraData?: any, overwriteSource?: string) { + cursor.trigger(overwriteSource || 'tests', command, extraData); + } + + function type(cursor: Cursor, text: string) { + for (let i = 0; i < text.length; i++) { + cursorCommand(cursor, Handler.Type, { text: text.charAt(i) }, 'keyboard'); + } + } + + withTestCodeEditor('', {}, (editor, cursor) => { + type(cursor, 'foo bar baz'); + assert.equal(editor.getValue(), 'foo bar baz'); + + cursorWordStartLeft(editor); + cursorWordStartLeft(editor); + type(cursor, 'q'); + + assert.equal(editor.getValue(), 'foo qbar baz'); + + cursorCommand(cursor, Handler.Undo, {}); + assert.equal(editor.getValue(), 'foo bar baz'); + }); + }); + test('cursorWordEndLeft', () => { const EXPECTED = ['| /*| Just| some| more| text| a|+=| 3| +|5|-|3| +| 7| */| '].join('\n'); const [text,] = deserializePipePositions(EXPECTED); diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index c7813c7134..d226aba210 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -52,6 +52,7 @@ export abstract class MoveWordCommand extends EditorCommand { return this._moveTo(sel, outPosition, this._inSelectionMode); }); + model.pushStackElement(); editor._getCursors().setStates('moveWordCommand', CursorChangeReason.NotSet, result.map(r => CursorState.fromModelSelection(r))); if (result.length === 1) { const pos = new Position(result[0].positionLineNumber, result[0].positionColumn); diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 71ab88a4c1..608f41961c 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -6,6 +6,7 @@ import 'vs/css!./inspectTokens'; import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { escape } from 'vs/base/common/strings'; import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; @@ -48,6 +49,7 @@ class InspectTokensController extends Disposable implements IEditorContribution this._register(this._editor.onDidChangeModel((e) => this.stop())); this._register(this._editor.onDidChangeModelLanguage((e) => this.stop())); this._register(TokenizationRegistry.onDidChange((e) => this.stop())); + this._register(this._editor.onKeyUp((e) => e.keyCode === KeyCode.Escape && this.stop())); } public dispose(): void { @@ -222,13 +224,13 @@ class InspectTokensWidget extends Disposable implements IContentWidget { result += `
`; - let metadata = this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]); + let metadata = (token2Index << 1) + 1 < data.tokens2.length ? this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]) : null; result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; + result += ``; + result += ``; + result += ``; + result += ``; + result += ``; result += ``; result += `
`; diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 0a2a0fd1d8..dca8eec3b7 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -124,6 +124,13 @@ export interface IGlobalEditorOptions { * Defaults to 20000. */ maxTokenizationLineLength?: number; + /** + * Theme to be used for rendering. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * You can create custom themes via `monaco.editor.defineTheme`. + * To switch a theme, use `monaco.editor.setTheme` + */ + theme?: string; } /** @@ -334,6 +341,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon private readonly _contextViewService: ContextViewService; private readonly _configurationService: IConfigurationService; + private readonly _standaloneThemeService: IStandaloneThemeService; private _ownsModel: boolean; constructor( @@ -363,6 +371,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon this._contextViewService = contextViewService; this._configurationService = configurationService; + this._standaloneThemeService = themeService; this._register(toDispose); this._register(themeDomRegistration); @@ -391,6 +400,9 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon public updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void { applyConfigurationValues(this._configurationService, newOptions, false); + if (typeof newOptions.theme === 'string') { + this._standaloneThemeService.setTheme(newOptions.theme); + } super.updateOptions(newOptions); } @@ -414,6 +426,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon private readonly _contextViewService: ContextViewService; private readonly _configurationService: IConfigurationService; + private readonly _standaloneThemeService: IStandaloneThemeService; constructor( domElement: HTMLElement, @@ -443,6 +456,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon this._contextViewService = contextViewService; this._configurationService = configurationService; + this._standaloneThemeService = themeService; this._register(toDispose); this._register(themeDomRegistration); @@ -454,8 +468,11 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon super.dispose(); } - public updateOptions(newOptions: IDiffEditorOptions): void { + public updateOptions(newOptions: IDiffEditorOptions & IGlobalEditorOptions): void { applyConfigurationValues(this._configurationService, newOptions, true); + if (typeof newOptions.theme === 'string') { + this._standaloneThemeService.setTheme(newOptions.theme); + } super.updateOptions(newOptions); } diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index 5b881a6ad7..7f69067959 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -78,7 +78,7 @@ suite('MinimapCharRenderer', () => { imageData.data[4 * i + 2] = background.b; imageData.data[4 * i + 3] = 255; } - renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 2, false); + renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 2, false, false); let actual: number[] = []; for (let i = 0; i < imageData.data.length; i++) { @@ -108,7 +108,7 @@ suite('MinimapCharRenderer', () => { imageData.data[4 * i + 3] = 255; } - renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 1, false); + renderer.renderChar(imageData, 0, 0, 'd'.charCodeAt(0), color, background, 1, false, false); let actual: number[] = []; for (let i = 0; i < imageData.data.length; i++) { diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index 32fdd4a48f..77bb4b261d 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -17,6 +17,7 @@ interface IEditorLayoutProviderOpts { readonly showLineNumbers: boolean; readonly lineNumbersMinChars: number; readonly lineNumbersDigitCount: number; + maxLineNumber?: number; readonly lineDecorationsWidth: number; @@ -32,6 +33,7 @@ interface IEditorLayoutProviderOpts { readonly minimapSide: 'left' | 'right'; readonly minimapRenderCharacters: boolean; readonly minimapMaxColumn: number; + minimapMode?: 'actual' | 'cover' | 'contain'; readonly pixelRatio: number; } @@ -45,6 +47,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { options._write(EditorOption.folding, false); const minimapOptions: EditorMinimapOptions = { enabled: input.minimap, + mode: input.minimapMode || 'actual', side: input.minimapSide, renderCharacters: input.minimapRenderCharacters, maxColumn: input.minimapMaxColumn, @@ -77,6 +80,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { outerWidth: input.outerWidth, outerHeight: input.outerHeight, lineHeight: input.lineHeight, + maxLineNumber: input.maxLineNumber || Math.pow(10, input.lineNumbersDigitCount) - 1, lineNumbersDigitCount: input.lineNumbersDigitCount, typicalHalfwidthCharacterWidth: input.typicalHalfwidthCharacterWidth, maxDigitWidth: input.maxDigitWidth, @@ -125,6 +129,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, viewportColumn: 98, verticalScrollbarWidth: 0, @@ -179,6 +191,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, viewportColumn: 97, verticalScrollbarWidth: 11, @@ -233,6 +253,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, viewportColumn: 88, verticalScrollbarWidth: 0, @@ -287,6 +315,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 88, verticalScrollbarWidth: 0, @@ -341,6 +377,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 88, verticalScrollbarWidth: 0, @@ -395,6 +439,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 83, verticalScrollbarWidth: 0, @@ -449,6 +501,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 83, verticalScrollbarWidth: 0, @@ -503,6 +563,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 82, verticalScrollbarWidth: 0, @@ -557,6 +625,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 171, verticalScrollbarWidth: 0, @@ -611,6 +687,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.None, minimapLeft: 0, minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, viewportColumn: 169, verticalScrollbarWidth: 0, @@ -665,6 +749,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 903, minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 2, + minimapCanvasInnerWidth: 97, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, viewportColumn: 89, verticalScrollbarWidth: 0, @@ -719,6 +811,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 903, minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 194, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, viewportColumn: 89, verticalScrollbarWidth: 0, @@ -773,6 +873,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 945, minimapWidth: 55, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 220, + minimapCanvasInnerHeight: 3200, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, viewportColumn: 93, verticalScrollbarWidth: 0, @@ -827,6 +935,270 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 0, minimapWidth: 55, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 220, + minimapCanvasInnerHeight: 3200, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, + viewportColumn: 93, + + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + + overviewRuler: { + top: 0, + width: 0, + height: 800, + right: 0 + } + }); + }); + + test('EditorLayoutProvider 11 - minimap mode cover without sampling', () => { + doTest({ + outerWidth: 1000, + outerHeight: 800, + showGlyphMargin: false, + lineHeight: 16, + showLineNumbers: false, + lineNumbersMinChars: 0, + lineNumbersDigitCount: 3, + maxLineNumber: 120, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + scrollbarArrowSize: 0, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + minimapMode: 'cover', + pixelRatio: 2, + }, { + width: 1000, + height: 800, + + glyphMarginLeft: 0, + glyphMarginWidth: 0, + + lineNumbersLeft: 0, + lineNumbersWidth: 0, + + decorationsLeft: 0, + decorationsWidth: 10, + + contentLeft: 10, + contentWidth: 893, + + renderMinimap: RenderMinimap.Text, + minimapLeft: 903, + minimapWidth: 97, + minimapHeightIsEditorHeight: true, + minimapIsSampling: false, + minimapScale: 3, + minimapLineHeight: 13, + minimapCanvasInnerWidth: 291, + minimapCanvasInnerHeight: 1560, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, + viewportColumn: 89, + + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + + overviewRuler: { + top: 0, + width: 0, + height: 800, + right: 0 + } + }); + }); + + test('EditorLayoutProvider 12 - minimap mode cover with sampling', () => { + doTest({ + outerWidth: 1000, + outerHeight: 800, + showGlyphMargin: false, + lineHeight: 16, + showLineNumbers: false, + lineNumbersMinChars: 0, + lineNumbersDigitCount: 4, + maxLineNumber: 2500, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + scrollbarArrowSize: 0, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + minimapMode: 'cover', + pixelRatio: 2, + }, { + width: 1000, + height: 800, + + glyphMarginLeft: 0, + glyphMarginWidth: 0, + + lineNumbersLeft: 0, + lineNumbersWidth: 0, + + decorationsLeft: 0, + decorationsWidth: 10, + + contentLeft: 10, + contentWidth: 935, + + renderMinimap: RenderMinimap.Text, + minimapLeft: 945, + minimapWidth: 55, + minimapHeightIsEditorHeight: true, + minimapIsSampling: true, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 110, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, + viewportColumn: 93, + + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + + overviewRuler: { + top: 0, + width: 0, + height: 800, + right: 0 + } + }); + }); + + test('EditorLayoutProvider 13 - minimap mode contain without sampling', () => { + doTest({ + outerWidth: 1000, + outerHeight: 800, + showGlyphMargin: false, + lineHeight: 16, + showLineNumbers: false, + lineNumbersMinChars: 0, + lineNumbersDigitCount: 3, + maxLineNumber: 120, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + scrollbarArrowSize: 0, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + minimapMode: 'contain', + pixelRatio: 2, + }, { + width: 1000, + height: 800, + + glyphMarginLeft: 0, + glyphMarginWidth: 0, + + lineNumbersLeft: 0, + lineNumbersWidth: 0, + + decorationsLeft: 0, + decorationsWidth: 10, + + contentLeft: 10, + contentWidth: 893, + + renderMinimap: RenderMinimap.Text, + minimapLeft: 903, + minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 194, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, + viewportColumn: 89, + + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + + overviewRuler: { + top: 0, + width: 0, + height: 800, + right: 0 + } + }); + }); + + test('EditorLayoutProvider 14 - minimap mode contain with sampling', () => { + doTest({ + outerWidth: 1000, + outerHeight: 800, + showGlyphMargin: false, + lineHeight: 16, + showLineNumbers: false, + lineNumbersMinChars: 0, + lineNumbersDigitCount: 4, + maxLineNumber: 2500, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + scrollbarArrowSize: 0, + verticalScrollbarHasArrows: false, + minimap: true, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + minimapMode: 'contain', + pixelRatio: 2, + }, { + width: 1000, + height: 800, + + glyphMarginLeft: 0, + glyphMarginWidth: 0, + + lineNumbersLeft: 0, + lineNumbersWidth: 0, + + decorationsLeft: 0, + decorationsWidth: 10, + + contentLeft: 10, + contentWidth: 935, + + renderMinimap: RenderMinimap.Text, + minimapLeft: 945, + minimapWidth: 55, + minimapHeightIsEditorHeight: true, + minimapIsSampling: true, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 110, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, viewportColumn: 93, verticalScrollbarWidth: 0, @@ -881,6 +1253,14 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { renderMinimap: RenderMinimap.Text, minimapLeft: 1096, minimapWidth: 91, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 182, + minimapCanvasInnerHeight: 844, + minimapCanvasOuterWidth: 91, + minimapCanvasOuterHeight: 422, viewportColumn: 83, verticalScrollbarWidth: 14, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f60cbf4137..e982ce6248 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1108,6 +1108,13 @@ declare namespace monaco.editor { * Defaults to 20000. */ maxTokenizationLineLength?: number; + /** + * Theme to be used for rendering. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * You can create custom themes via `monaco.editor.defineTheme`. + * To switch a theme, use `monaco.editor.setTheme` + */ + theme?: string; } /** @@ -2390,6 +2397,8 @@ declare namespace monaco.editor { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { + readonly affectsMinimap: boolean; + readonly affectsOverviewRuler: boolean; } export interface IModelOptionsChangedEvent { @@ -3334,6 +3343,14 @@ declare namespace monaco.editor { * The width of the minimap */ readonly minimapWidth: number; + readonly minimapHeightIsEditorHeight: boolean; + readonly minimapIsSampling: boolean; + readonly minimapScale: number; + readonly minimapLineHeight: number; + readonly minimapCanvasInnerWidth: number; + readonly minimapCanvasInnerHeight: number; + readonly minimapCanvasOuterWidth: number; + readonly minimapCanvasOuterHeight: number; /** * Minimap render type */ @@ -3383,6 +3400,11 @@ declare namespace monaco.editor { * Defaults to 'right'. */ side?: 'right' | 'left'; + /** + * Control the minimap rendering mode. + * Defaults to 'actual'. + */ + mode?: 'actual' | 'cover' | 'contain'; /** * Control the rendering of the minimap slider. * Defaults to 'mouseover'. diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index c41cfb0ceb..80e8588b26 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -353,7 +353,7 @@ export class UserSettings extends Disposable { super(); this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); this._register(this.fileService.watch(dirname(this.userSettingsResource))); - this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire())); + this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire())); } async loadConfiguration(): Promise { diff --git a/src/vs/platform/configuration/node/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts similarity index 100% rename from src/vs/platform/configuration/node/configurationService.ts rename to src/vs/platform/configuration/common/configurationService.ts diff --git a/src/vs/platform/configuration/test/node/configurationService.test.ts b/src/vs/platform/configuration/test/node/configurationService.test.ts index 278138bc8d..c09c091d18 100644 --- a/src/vs/platform/configuration/test/node/configurationService.test.ts +++ b/src/vs/platform/configuration/test/node/configurationService.test.ts @@ -9,7 +9,7 @@ import * as path from 'vs/base/common/path'; import * as fs from 'fs'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { testFile } from 'vs/base/test/node/utils'; diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index f753244476..333b2f93f5 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -55,7 +55,7 @@ export class FileService extends Disposable implements IFileService { // Forward events from provider const providerDisposables = new DisposableStore(); - providerDisposables.add(provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes)))); + providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes)))); providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme }))); if (typeof provider.onDidErrorOccur === 'function') { providerDisposables.add(provider.onDidErrorOccur(error => this._onError.fire(new Error(error)))); @@ -147,11 +147,11 @@ export class FileService extends Disposable implements IFileService { //#endregion - private _onAfterOperation: Emitter = this._register(new Emitter()); - readonly onAfterOperation: Event = this._onAfterOperation.event; + private _onDidRunOperation = this._register(new Emitter()); + readonly onDidRunOperation = this._onDidRunOperation.event; - private _onError: Emitter = this._register(new Emitter()); - readonly onError: Event = this._onError.event; + private _onError = this._register(new Emitter()); + readonly onError = this._onError.event; //#region File Metadata Resolving @@ -299,7 +299,7 @@ export class FileService extends Disposable implements IFileService { const fileStat = await this.writeFile(resource, bufferOrReadableOrStream); // events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); + this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); return fileStat; } @@ -549,7 +549,7 @@ export class FileService extends Disposable implements IFileService { // resolve and send events const fileStat = await this.resolve(target, { resolveMetadata: true }); - this._onAfterOperation.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat)); + this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat)); return fileStat; } @@ -563,7 +563,7 @@ export class FileService extends Disposable implements IFileService { // resolve and send events const fileStat = await this.resolve(target, { resolveMetadata: true }); - this._onAfterOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat)); + this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat)); return fileStat; } @@ -717,7 +717,7 @@ export class FileService extends Disposable implements IFileService { // events const fileStat = await this.resolve(resource, { resolveMetadata: true }); - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); + this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); return fileStat; } @@ -799,15 +799,15 @@ export class FileService extends Disposable implements IFileService { await provider.delete(resource, { recursive, useTrash }); // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); + this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); } //#endregion //#region File Watching - private _onFileChanges: Emitter = this._register(new Emitter()); - readonly onFileChanges: Event = this._onFileChanges.event; + private _onDidFilesChange = this._register(new Emitter()); + readonly onDidFilesChange = this._onDidFilesChange.event; private activeWatchers = new Map(); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 540118760b..5a47f89a70 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -63,12 +63,12 @@ export interface IFileService { * Allows to listen for file changes. The event will fire for every file within the opened workspace * (if any) as well as all files that have been watched explicitly using the #watch() API. */ - readonly onFileChanges: Event; + readonly onDidFilesChange: Event; /** * An event that is fired upon successful completion of a certain file operation. */ - readonly onAfterOperation: Event; + readonly onDidRunOperation: Event; /** * Resolve the properties of a file/folder identified by the resource. @@ -471,15 +471,7 @@ export interface IFileChange { export class FileChangesEvent { - private readonly _changes: readonly IFileChange[]; - - constructor(changes: readonly IFileChange[]) { - this._changes = changes; - } - - get changes() { - return this._changes; - } + constructor(public readonly changes: readonly IFileChange[]) { } /** * Returns true if this change event contains the provided file with the given change type (if provided). In case of @@ -493,7 +485,7 @@ export class FileChangesEvent { const checkForChangeType = !isUndefinedOrNull(type); - return this._changes.some(change => { + return this.changes.some(change => { if (checkForChangeType && change.type !== type) { return false; } @@ -550,11 +542,11 @@ export class FileChangesEvent { } private getOfType(type: FileChangeType): IFileChange[] { - return this._changes.filter(change => change.type === type); + return this.changes.filter(change => change.type === type); } private hasType(type: FileChangeType): boolean { - return this._changes.some(change => { + return this.changes.some(change => { return change.type === type; }); } diff --git a/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts similarity index 98% rename from src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts rename to src/vs/platform/files/common/inMemoryFilesystemProvider.ts index 33a5fdd6c9..f8ed55455f 100644 --- a/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts +++ b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts @@ -66,9 +66,7 @@ export class InMemoryFileSystemProvider extends Disposable implements IFileSyste async readdir(resource: URI): Promise<[string, FileType][]> { const entry = this._lookupAsDirectory(resource, false); let result: [string, FileType][] = []; - for (const [name, child] of entry.entries) { - result.push([name, child.type]); - } + entry.entries.forEach((child, name) => result.push([name, child.type])); return result; } diff --git a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts index f25db23073..34ab3f2ebb 100644 --- a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts @@ -20,7 +20,7 @@ export class FileWatcher extends Disposable { constructor( private path: string, - private onFileChanges: (changes: IDiskFileChange[]) => void, + private onDidFilesChange: (changes: IDiskFileChange[]) => void, private onLogMessage: (msg: ILogMessage) => void, private verboseLogging: boolean ) { @@ -101,7 +101,7 @@ export class FileWatcher extends Disposable { // Fire if (normalizedFileChanges.length > 0) { - this.onFileChanges(normalizedFileChanges); + this.onDidFilesChange(normalizedFileChanges); } return Promise.resolve(); diff --git a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts index 3b39adca39..b615930e5f 100644 --- a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts @@ -21,7 +21,7 @@ export class FileWatcher extends Disposable { constructor( private folders: IWatcherRequest[], - private onFileChanges: (changes: IDiskFileChange[]) => void, + private onDidFilesChange: (changes: IDiskFileChange[]) => void, private onLogMessage: (msg: ILogMessage) => void, private verboseLogging: boolean, ) { @@ -68,7 +68,7 @@ export class FileWatcher extends Disposable { this.service.setVerboseLogging(this.verboseLogging); const options = {}; - this._register(this.service.watch(options)(e => !this.isDisposed && this.onFileChanges(e))); + this._register(this.service.watch(options)(e => !this.isDisposed && this.onDidFilesChange(e))); this._register(this.service.onLogMessage(m => this.onLogMessage(m))); diff --git a/src/vs/platform/files/node/watcher/unix/watcherService.ts b/src/vs/platform/files/node/watcher/unix/watcherService.ts index 92b9e77e94..23155e5c1f 100644 --- a/src/vs/platform/files/node/watcher/unix/watcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/watcherService.ts @@ -20,7 +20,7 @@ export class FileWatcher extends Disposable { constructor( private folders: IWatcherRequest[], - private onFileChanges: (changes: IDiskFileChange[]) => void, + private onDidFilesChange: (changes: IDiskFileChange[]) => void, private onLogMessage: (msg: ILogMessage) => void, private verboseLogging: boolean, private watcherOptions: IWatcherOptions = {} @@ -67,7 +67,7 @@ export class FileWatcher extends Disposable { this.service.setVerboseLogging(this.verboseLogging); - this._register(this.service.watch(this.watcherOptions)(e => !this.isDisposed && this.onFileChanges(e))); + this._register(this.service.watch(this.watcherOptions)(e => !this.isDisposed && this.onDidFilesChange(e))); this._register(this.service.onLogMessage(m => this.onLogMessage(m))); diff --git a/src/vs/platform/files/node/watcher/win32/watcherService.ts b/src/vs/platform/files/node/watcher/win32/watcherService.ts index 522012a790..f5a5fb8b00 100644 --- a/src/vs/platform/files/node/watcher/win32/watcherService.ts +++ b/src/vs/platform/files/node/watcher/win32/watcherService.ts @@ -16,7 +16,7 @@ export class FileWatcher implements IDisposable { constructor( folders: { path: string, excludes: string[] }[], - private onFileChanges: (changes: IDiskFileChange[]) => void, + private onDidFilesChange: (changes: IDiskFileChange[]) => void, private onLogMessage: (msg: ILogMessage) => void, private verboseLogging: boolean ) { @@ -62,7 +62,7 @@ export class FileWatcher implements IDisposable { // Emit through event emitter if (events.length > 0) { - this.onFileChanges(events); + this.onDidFilesChange(events); } } @@ -72,4 +72,4 @@ export class FileWatcher implements IDisposable { this.service = undefined; } } -} \ No newline at end of file +} diff --git a/src/vs/platform/files/test/node/diskFileService.test.ts b/src/vs/platform/files/test/node/diskFileService.test.ts index e9729e2111..e528458c4e 100644 --- a/src/vs/platform/files/test/node/diskFileService.test.ts +++ b/src/vs/platform/files/test/node/diskFileService.test.ts @@ -165,7 +165,7 @@ suite('Disk File Service', function () { test('createFolder', async () => { let event: FileOperationEvent | undefined; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const parent = await service.resolve(URI.file(testDir)); @@ -185,7 +185,7 @@ suite('Disk File Service', function () { test('createFolder: creating multiple folders at once', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const multiFolderPaths = ['a', 'couple', 'of', 'folders']; const parent = await service.resolve(URI.file(testDir)); @@ -460,7 +460,7 @@ suite('Disk File Service', function () { test('deleteFile', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const resource = URI.file(join(testDir, 'deep', 'conway.js')); const source = await service.resolve(resource); @@ -496,7 +496,7 @@ suite('Disk File Service', function () { const source = await service.resolve(link); let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); await service.del(source.resource); @@ -519,7 +519,7 @@ suite('Disk File Service', function () { await symlink(target.fsPath, link.fsPath); let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); await service.del(link); @@ -532,7 +532,7 @@ suite('Disk File Service', function () { test('deleteFolder (recursive)', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const resource = URI.file(join(testDir, 'deep')); const source = await service.resolve(resource); @@ -561,7 +561,7 @@ suite('Disk File Service', function () { test('move', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = URI.file(join(testDir, 'index.html')); const sourceContents = readFileSync(source.fsPath); @@ -641,7 +641,7 @@ suite('Disk File Service', function () { async function testMoveAcrossProviders(sourceFile = 'index.html'): Promise { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = URI.file(join(testDir, sourceFile)); const sourceContents = readFileSync(source.fsPath); @@ -665,7 +665,7 @@ suite('Disk File Service', function () { test('move - multi folder', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const multiFolderPaths = ['a', 'couple', 'of', 'folders']; const renameToPath = join(...multiFolderPaths, 'other.html'); @@ -684,7 +684,7 @@ suite('Disk File Service', function () { test('move - directory', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = URI.file(join(testDir, 'deep')); @@ -728,7 +728,7 @@ suite('Disk File Service', function () { async function testMoveFolderAcrossProviders(): Promise { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = URI.file(join(testDir, 'deep')); const sourceChildren = readdirSync(source.fsPath); @@ -753,7 +753,7 @@ suite('Disk File Service', function () { test('move - MIX CASE', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); @@ -774,7 +774,7 @@ suite('Disk File Service', function () { test('move - same file', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); @@ -794,7 +794,7 @@ suite('Disk File Service', function () { test('move - same file #2', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); @@ -817,7 +817,7 @@ suite('Disk File Service', function () { test('move - source parent of target', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); let source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); const originalSize = source.size; @@ -839,7 +839,7 @@ suite('Disk File Service', function () { test('move - FILE_MOVE_CONFLICT', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); let source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); const originalSize = source.size; @@ -863,7 +863,7 @@ suite('Disk File Service', function () { let createEvent: FileOperationEvent; let moveEvent: FileOperationEvent; let deleteEvent: FileOperationEvent; - disposables.add(service.onAfterOperation(e => { + disposables.add(service.onDidRunOperation(e => { if (e.operation === FileOperation.CREATE) { createEvent = e; } else if (e.operation === FileOperation.DELETE) { @@ -927,7 +927,7 @@ suite('Disk File Service', function () { async function doTestCopy(sourceName: string = 'index.html') { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = await service.resolve(URI.file(join(testDir, sourceName))); const target = URI.file(join(testDir, 'other.html')); @@ -952,7 +952,7 @@ suite('Disk File Service', function () { let createEvent: FileOperationEvent; let copyEvent: FileOperationEvent; let deleteEvent: FileOperationEvent; - disposables.add(service.onAfterOperation(e => { + disposables.add(service.onDidRunOperation(e => { if (e.operation === FileOperation.CREATE) { createEvent = e; } else if (e.operation === FileOperation.DELETE) { @@ -1057,7 +1057,7 @@ suite('Disk File Service', function () { test('copy - same file', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); @@ -1077,7 +1077,7 @@ suite('Disk File Service', function () { test('copy - same file #2', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); @@ -1567,7 +1567,7 @@ suite('Disk File Service', function () { async function assertCreateFile(converter: (content: string) => VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const contents = 'Hello World'; const resource = URI.file(join(testDir, 'test.txt')); @@ -1600,7 +1600,7 @@ suite('Disk File Service', function () { test('createFile (allows to overwrite existing)', async () => { let event: FileOperationEvent; - disposables.add(service.onAfterOperation(e => event = e)); + disposables.add(service.onDidRunOperation(e => event = e)); const contents = 'Hello World'; const resource = URI.file(join(testDir, 'test.txt')); @@ -2152,7 +2152,7 @@ suite('Disk File Service', function () { return event.changes.map(change => `Change: type ${toString(change.type)} path ${change.resource.toString()}`).join('\n'); } - const listenerDisposable = service.onFileChanges(event => { + const listenerDisposable = service.onDidFilesChange(event => { watcherDisposable.dispose(); listenerDisposable.dispose(); diff --git a/src/vs/platform/files/test/node/normalizer.test.ts b/src/vs/platform/files/test/node/normalizer.test.ts index e5f59ad59d..1cab97925e 100644 --- a/src/vs/platform/files/test/node/normalizer.test.ts +++ b/src/vs/platform/files/test/node/normalizer.test.ts @@ -15,14 +15,14 @@ function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent { } class TestFileWatcher { - private readonly _onFileChanges: Emitter; + private readonly _onDidFilesChange: Emitter; constructor() { - this._onFileChanges = new Emitter(); + this._onDidFilesChange = new Emitter(); } - get onFileChanges(): Event { - return this._onFileChanges.event; + get onDidFilesChange(): Event { + return this._onDidFilesChange.event; } report(changes: IDiskFileChange[]): void { @@ -36,7 +36,7 @@ class TestFileWatcher { // Emit through event emitter if (normalizedEvents.length > 0) { - this._onFileChanges.fire(toFileChangesEvent(normalizedEvents)); + this._onDidFilesChange.fire(toFileChangesEvent(normalizedEvents)); } } } @@ -62,7 +62,7 @@ suite('Normalizer', () => { { path: deleted.fsPath, type: FileChangeType.DELETED }, ]; - watch.onFileChanges(e => { + watch.onDidFilesChange(e => { assert.ok(e); assert.equal(e.changes.length, 3); assert.ok(e.contains(added, FileChangeType.ADDED)); @@ -101,7 +101,7 @@ suite('Normalizer', () => { { path: updatedFile.fsPath, type: FileChangeType.UPDATED } ]; - watch.onFileChanges(e => { + watch.onDidFilesChange(e => { assert.ok(e); assert.equal(e.changes.length, 5); @@ -131,7 +131,7 @@ suite('Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onFileChanges(e => { + watch.onDidFilesChange(e => { assert.ok(e); assert.equal(e.changes.length, 1); @@ -156,7 +156,7 @@ suite('Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onFileChanges(e => { + watch.onDidFilesChange(e => { assert.ok(e); assert.equal(e.changes.length, 2); @@ -182,7 +182,7 @@ suite('Normalizer', () => { { path: unrelated.fsPath, type: FileChangeType.UPDATED }, ]; - watch.onFileChanges(e => { + watch.onDidFilesChange(e => { assert.ok(e); assert.equal(e.changes.length, 2); @@ -211,7 +211,7 @@ suite('Normalizer', () => { { path: updated.fsPath, type: FileChangeType.DELETED } ]; - watch.onFileChanges(e => { + watch.onDidFilesChange(e => { assert.ok(e); assert.equal(e.changes.length, 2); diff --git a/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts b/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts index a067712f3b..bdb9c24e60 100644 --- a/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts +++ b/src/vs/platform/instantiation/test/common/instantiationServiceMock.ts @@ -13,6 +13,8 @@ interface IServiceMock { service: any; } +const isSinonSpyLike = (fn: Function): fn is sinon.SinonSpy => fn && 'callCount' in fn; + export class TestInstantiationService extends InstantiationService { private _servciesMap: Map, any>; @@ -37,10 +39,10 @@ export class TestInstantiationService extends InstantiationService { public stub(service: ServiceIdentifier, ctor: Function): T; public stub(service: ServiceIdentifier, obj: Partial): T; - public stub(service: ServiceIdentifier, ctor: Function, property: string, value: any): sinon.SinonStub; - public stub(service: ServiceIdentifier, obj: Partial, property: string, value: any): sinon.SinonStub; - public stub(service: ServiceIdentifier, property: string, value: any): sinon.SinonStub; - public stub(serviceIdentifier: ServiceIdentifier, arg2: any, arg3?: string, arg4?: any): sinon.SinonStub { + public stub(service: ServiceIdentifier, ctor: Function, property: string, value: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub; + public stub(service: ServiceIdentifier, obj: Partial, property: string, value: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub; + public stub(service: ServiceIdentifier, property: string, value: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub; + public stub(serviceIdentifier: ServiceIdentifier, arg2: any, arg3?: string, arg4?: any): sinon.SinonStub | sinon.SinonSpy { let service = typeof arg2 !== 'string' ? arg2 : undefined; let serviceMock: IServiceMock = { id: serviceIdentifier, service: service }; let property = typeof arg2 === 'string' ? arg2 : arg3; @@ -53,9 +55,11 @@ export class TestInstantiationService extends InstantiationService { stubObject[property].restore(); } if (typeof value === 'function') { - stubObject[property] = value; + const spy = isSinonSpyLike(value) ? value : sinon.spy(value); + stubObject[property] = spy; + return spy; } else { - let stub = value ? sinon.stub().returns(value) : sinon.stub(); + const stub = value ? sinon.stub().returns(value) : sinon.stub(); stubObject[property] = stub; return stub; } @@ -67,9 +71,9 @@ export class TestInstantiationService extends InstantiationService { } public stubPromise(service?: ServiceIdentifier, fnProperty?: string, value?: any): T | sinon.SinonStub; - public stubPromise(service?: ServiceIdentifier, ctor?: any, fnProperty?: string, value?: any): sinon.SinonStub; - public stubPromise(service?: ServiceIdentifier, obj?: any, fnProperty?: string, value?: any): sinon.SinonStub; - public stubPromise(arg1?: any, arg2?: any, arg3?: any, arg4?: any): sinon.SinonStub { + public stubPromise(service?: ServiceIdentifier, ctor?: any, fnProperty?: string, value?: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub; + public stubPromise(service?: ServiceIdentifier, obj?: any, fnProperty?: string, value?: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub; + public stubPromise(arg1?: any, arg2?: any, arg3?: any, arg4?: any): sinon.SinonStub | sinon.SinonSpy { arg3 = typeof arg2 === 'string' ? Promise.resolve(arg3) : arg3; arg4 = typeof arg2 !== 'string' && typeof arg3 === 'string' ? Promise.resolve(arg4) : arg4; return this.stub(arg1, arg2, arg3, arg4); @@ -124,4 +128,4 @@ export class TestInstantiationService extends InstantiationService { interface SinonOptions { mock?: boolean; stub?: boolean; -} \ No newline at end of file +} diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index 36835eb840..1ce413141c 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -135,15 +135,15 @@ export namespace IMarkerData { export function makeKeyOptionalMessage(markerData: IMarkerData, useMessage: boolean): string { let result: string[] = [emptyString]; if (markerData.source) { - result.push(markerData.source.replace('¦', '\¦')); + result.push(markerData.source.replace('¦', '\\¦')); } else { result.push(emptyString); } if (markerData.code) { if (typeof markerData.code === 'string') { - result.push(markerData.code.replace('¦', '\¦')); + result.push(markerData.code.replace('¦', '\\¦')); } else { - result.push(markerData.code.value.replace('¦', '\¦')); + result.push(markerData.code.value.replace('¦', '\\¦')); } } else { result.push(emptyString); @@ -157,7 +157,7 @@ export namespace IMarkerData { // Modifed to not include the message as part of the marker key to work around // https://github.com/microsoft/vscode/issues/77475 if (markerData.message && useMessage) { - result.push(markerData.message.replace('¦', '\¦')); + result.push(markerData.message.replace('¦', '\\¦')); } else { result.push(emptyString); } diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index c349a877e0..617fb78ce3 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -6,7 +6,7 @@ import BaseSeverity from 'vs/base/common/severity'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IAction } from 'vs/base/common/actions'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; export import Severity = BaseSeverity; @@ -318,16 +318,14 @@ export class NoOpNotification implements INotificationHandle { readonly progress = new NoOpProgress(); - private readonly _onDidClose: Emitter = new Emitter(); - readonly onDidClose: Event = this._onDidClose.event; + readonly onDidClose = Event.None; + readonly onDidChangeVisibility = Event.None; updateSeverity(severity: Severity): void { } updateMessage(message: NotificationMessage): void { } updateActions(actions?: INotificationActions): void { } - close(): void { - this._onDidClose.dispose(); - } + close(): void { } } export class NoOpProgress implements INotificationProgress { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 6f7a6f6725..15de3a5024 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -17,7 +17,11 @@ export interface IProgressService { _serviceBrand: undefined; - withProgress(options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void): Promise; + withProgress( + options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + task: (progress: IProgress) => Promise, + onDidCancel?: (choice?: number) => void + ): Promise; } export interface IProgressIndicator { @@ -45,19 +49,19 @@ export const enum ProgressLocation { } export interface IProgressOptions { - location: ProgressLocation | string; - title?: string; - source?: string; - total?: number; - cancellable?: boolean; - buttons?: string[]; + readonly location: ProgressLocation | string; + readonly title?: string; + readonly source?: string; + readonly total?: number; + readonly cancellable?: boolean; + readonly buttons?: string[]; } export interface IProgressNotificationOptions extends IProgressOptions { readonly location: ProgressLocation.Notification; readonly primaryActions?: ReadonlyArray; readonly secondaryActions?: ReadonlyArray; - delay?: number; + readonly delay?: number; } export interface IProgressWindowOptions extends IProgressOptions { @@ -66,8 +70,8 @@ export interface IProgressWindowOptions extends IProgressOptions { } export interface IProgressCompositeOptions extends IProgressOptions { - location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string; - delay?: number; + readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string; + readonly delay?: number; } export interface IProgressStep { @@ -96,20 +100,14 @@ export interface IProgress { export class Progress implements IProgress { - private _callback: (data: T) => void; private _value?: T; + get value(): T | undefined { return this._value; } - constructor(callback: (data: T) => void) { - this._callback = callback; - } - - get value(): T | undefined { - return this._value; - } + constructor(private callback: (data: T) => void) { } report(item: T) { this._value = item; - this._callback(this._value); + this.callback(this._value); } } diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 02674e3437..9db829273f 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -224,7 +224,7 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase this.isWatching = true; this._register(this.fileService.watch(this.file)); - this._register(this.fileService.onFileChanges(e => { + this._register(this.fileService.onDidFilesChange(e => { if (document.hasFocus()) { return; // optimization: ignore changes from ourselves by checking for focus } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 0fa0e80abf..2957214362 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -428,6 +428,10 @@ export const minimapError = registerColor('minimap.errorHighlight', { dark: new export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hc: null }, nls.localize('minimapBackground', "Minimap background color.")); +export const minimapSliderBackground = registerColor('minimapSlider.background', { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hc: transparent(scrollbarSliderBackground, 0.5) }, nls.localize('minimapSliderBackground', "Minimap slider background color.")); +export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hc: transparent(scrollbarSliderHoverBackground, 0.5) }, nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); +export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hc: transparent(scrollbarSliderActiveBackground, 0.5) }, nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); + export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoForeground }, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts new file mode 100644 index 0000000000..c5aea94912 --- /dev/null +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; + +export const IUndoRedoService = createDecorator('undoRedoService'); + +export interface IUndoRedoContext { + replaceCurrentElement(others: IUndoRedoElement[]): void; +} + +export interface IUndoRedoElement { + /** + * None, one or multiple resources that this undo/redo element impacts. + */ + readonly resources: URI[]; + + /** + * The label of the undo/redo element. + */ + readonly label: string; + + /** + * Undo. + * Will always be called before `redo`. + * Can be called multiple times. + * e.g. `undo` -> `redo` -> `undo` -> `redo` + */ + undo(ctx: IUndoRedoContext): void; + + /** + * Redo. + * Will always be called after `undo`. + * Can be called multiple times. + * e.g. `undo` -> `redo` -> `undo` -> `redo` + */ + redo(ctx: IUndoRedoContext): void; + + /** + * Invalidate the edits concerning `resource`. + * i.e. the undo/redo stack for that particular resource has been destroyed. + */ + invalidate(resource: URI): boolean; +} + +export interface IUndoRedoService { + _serviceBrand: undefined; + + /** + * Add a new element to the `undo` stack. + * This will destroy the `redo` stack. + */ + pushElement(element: IUndoRedoElement): void; + + /** + * Get the last pushed element. If the last pushed element has been undone, returns null. + */ + getLastElement(resource: URI): IUndoRedoElement | null; + + /** + * Remove elements that target `resource`. + */ + removeElements(resource: URI): void; + + canUndo(resource: URI): boolean; + undo(resource: URI): void; + + redo(resource: URI): void; + canRedo(resource: URI): boolean; +} diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts new file mode 100644 index 0000000000..ff459d6d4b --- /dev/null +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUndoRedoService, IUndoRedoElement } 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'; + +class StackElement { + public readonly actual: IUndoRedoElement; + public readonly label: string; + public readonly resources: URI[]; + public readonly strResources: string[]; + + constructor(actual: IUndoRedoElement) { + this.actual = actual; + this.label = actual.label; + this.resources = actual.resources; + this.strResources = this.resources.map(resource => uriGetComparisonKey(resource)); + } + + public invalidate(resource: URI): void { + if (this.resources.length > 1) { + this.actual.invalidate(resource); + } + } +} + +class ResourceEditStack { + public resource: URI; + public past: StackElement[]; + public future: StackElement[]; + + constructor(resource: URI) { + this.resource = resource; + this.past = []; + this.future = []; + } +} + +export class UndoRedoService implements IUndoRedoService { + _serviceBrand: undefined; + + private readonly _editStacks: Map; + + constructor() { + this._editStacks = new Map(); + } + + public pushElement(_element: IUndoRedoElement): void { + const element = new StackElement(_element); + for (let i = 0, len = element.resources.length; i < len; i++) { + const resource = element.resources[i]; + const strResource = element.strResources[i]; + + let editStack: ResourceEditStack; + if (this._editStacks.has(strResource)) { + editStack = this._editStacks.get(strResource)!; + } else { + editStack = new ResourceEditStack(resource); + this._editStacks.set(strResource, editStack); + } + + // remove the future + for (const futureElement of editStack.future) { + futureElement.invalidate(resource); + } + editStack.future = []; + editStack.past.push(element); + } + } + + public getLastElement(resource: URI): IUndoRedoElement | null { + const strResource = uriGetComparisonKey(resource); + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + if (editStack.future.length > 0) { + return null; + } + if (editStack.past.length === 0) { + return null; + } + return editStack.past[editStack.past.length - 1].actual; + } + return null; + } + + public removeElements(resource: URI): void { + const strResource = uriGetComparisonKey(resource); + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + for (const pastElement of editStack.past) { + pastElement.invalidate(resource); + } + for (const futureElement of editStack.future) { + futureElement.invalidate(resource); + } + this._editStacks.delete(strResource); + } + } + + public canUndo(resource: URI): boolean { + const strResource = uriGetComparisonKey(resource); + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + return (editStack.past.length > 0); + } + return false; + } + + public undo(resource: URI): void { + const strResource = uriGetComparisonKey(resource); + if (!this._editStacks.has(strResource)) { + return; + } + + const editStack = this._editStacks.get(strResource)!; + if (editStack.past.length === 0) { + return; + } + + const element = editStack.past[editStack.past.length - 1]; + + let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null; + try { + element.actual.undo({ + replaceCurrentElement: (others: IUndoRedoElement[]): void => { + replaceCurrentElement = others; + } + }); + } catch (e) { + onUnexpectedError(e); + editStack.past.pop(); + editStack.future.push(element); + return; + } + + if (replaceCurrentElement === null) { + // regular case + editStack.past.pop(); + editStack.future.push(element); + return; + } + + const replaceCurrentElementMap = new Map(); + for (const _replace of replaceCurrentElement) { + const replace = new StackElement(_replace); + for (const strResource of replace.strResources) { + replaceCurrentElementMap.set(strResource, replace); + } + } + + for (let i = 0, len = element.strResources.length; i < len; i++) { + const strResource = element.strResources[i]; + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + for (let j = editStack.past.length - 1; j >= 0; j--) { + if (editStack.past[j] === element) { + if (replaceCurrentElementMap.has(strResource)) { + editStack.past[j] = replaceCurrentElementMap.get(strResource)!; + } else { + editStack.past.splice(j, 1); + } + break; + } + } + } + } + } + + public canRedo(resource: URI): boolean { + const strResource = uriGetComparisonKey(resource); + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + return (editStack.future.length > 0); + } + return false; + } + + redo(resource: URI): void { + const strResource = uriGetComparisonKey(resource); + if (!this._editStacks.has(strResource)) { + return; + } + + const editStack = this._editStacks.get(strResource)!; + if (editStack.future.length === 0) { + return; + } + + const element = editStack.future[editStack.future.length - 1]; + + let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null; + try { + element.actual.redo({ + replaceCurrentElement: (others: IUndoRedoElement[]): void => { + replaceCurrentElement = others; + } + }); + } catch (e) { + onUnexpectedError(e); + editStack.future.pop(); + editStack.past.push(element); + return; + } + + if (replaceCurrentElement === null) { + // regular case + editStack.future.pop(); + editStack.past.push(element); + return; + } + + const replaceCurrentElementMap = new Map(); + for (const _replace of replaceCurrentElement) { + const replace = new StackElement(_replace); + for (const strResource of replace.strResources) { + replaceCurrentElementMap.set(strResource, replace); + } + } + + for (let i = 0, len = element.strResources.length; i < len; i++) { + const strResource = element.strResources[i]; + if (this._editStacks.has(strResource)) { + const editStack = this._editStacks.get(strResource)!; + for (let j = editStack.future.length - 1; j >= 0; j--) { + if (editStack.future[j] === element) { + if (replaceCurrentElementMap.has(strResource)) { + editStack.future[j] = replaceCurrentElementMap.get(strResource)!; + } else { + editStack.future.splice(j, 1); + } + break; + } + } + } + } + } +} diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 501bfad9ee..d0b8560999 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -174,7 +174,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { ) { super(source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService); this._register(this.fileService.watch(dirname(file))); - this._register(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); } async stop(): Promise { diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index cd1941c7e0..1b299bd0af 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -39,7 +39,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs ) { super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService); this._register(this.fileService.watch(dirname(this.environmentService.argvResource))); - this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire())); + this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire())); } async pull(): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts b/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts index c2c5bd0cfa..5d7e09cb3e 100644 --- a/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts +++ b/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts @@ -14,6 +14,9 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut private _onDidChangeToken: Emitter = this._register(new Emitter()); readonly onDidChangeToken: Event = this._onDidChangeToken.event; + private _onTokenFailed: Emitter = this._register(new Emitter()); + readonly onTokenFailed: Event = this._onTokenFailed.event; + private _token: string | undefined; constructor() { @@ -30,4 +33,8 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut this._onDidChangeToken.fire(token); } } + + sendTokenFailed(): void { + this._onTokenFailed.fire(); + } } diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 96a95614b1..6360914cd0 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -72,6 +72,13 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto return this.sync(loop, auto); } } + if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.SessionExpired) { + this.logService.info('Auto Sync: Cloud has new session'); + this.logService.info('Auto Sync: Resetting the local sync state.'); + await this.userDataSyncService.resetLocal(); + this.logService.info('Auto Sync: Completed resetting the local sync state.'); + return this.sync(loop, auto); + } this.logService.error(e); this.successiveFailures++; this._onError.fire(e instanceof UserDataSyncError ? { code: e.code, source: e.source } : { code: UserDataSyncErrorCode.Unknown }); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index aecbb0cb43..26f86b2118 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, joinPath } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; @@ -120,23 +120,27 @@ export interface IUserData { } export interface IUserDataSyncStore { - url: string; + url: URI; authenticationProviderId: string; } export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined { - const value = configurationService.getValue(CONFIGURATION_SYNC_STORE_KEY); - return value && value.url && value.authenticationProviderId ? value : undefined; + const value = configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY); + if (value && value.url && value.authenticationProviderId) { + return { + url: joinPath(URI.parse(value.url), 'v1'), + authenticationProviderId: value.authenticationProviderId + }; + } + return undefined; } export const ALL_RESOURCE_KEYS: ResourceKey[] = ['settings', 'keybindings', 'extensions', 'globalState']; export type ResourceKey = 'settings' | 'keybindings' | 'extensions' | 'globalState'; export interface IUserDataManifest { - settings: string; - keybindings: string; - extensions: string; - globalState: string; + latest?: Record + session: string; } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); @@ -162,6 +166,7 @@ export enum UserDataSyncErrorCode { TooLarge = 'TooLarge', NoRef = 'NoRef', TurnedOff = 'TurnedOff', + SessionExpired = 'SessionExpired', // Local Errors LocalPreconditionFailed = 'LocalPreconditionFailed', @@ -303,9 +308,11 @@ export interface IUserDataAuthTokenService { _serviceBrand: undefined; readonly onDidChangeToken: Event; + readonly onTokenFailed: Event; getToken(): Promise; setToken(accessToken: string | undefined): Promise; + sendTokenFailed(): void; } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 5ca22ea41c..49c687dd48 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -96,6 +96,7 @@ export class UserDataAuthTokenServiceChannel implements IServerChannel { listen(_: unknown, event: string): Event { switch (event) { case 'onDidChangeToken': return this.service.onDidChangeToken; + case 'onTokenFailed': return this.service.onTokenFailed; } throw new Error(`Event not found: ${event}`); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index dc17c2cf27..7616d87ab5 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -14,11 +14,14 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { equals } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; type SyncErrorClassification = { source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; +const SESSION_ID_KEY = 'sync.sessionId'; + export class UserDataSyncService extends Disposable implements IUserDataSyncService { _serviceBrand: any; @@ -48,6 +51,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IStorageService private readonly storageService: IStorageService, ) { super(); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); @@ -96,7 +100,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.setStatus(SyncStatus.Syncing); } - const manifest = await this.userDataSyncStoreService.manifest(); + let manifest = await this.userDataSyncStoreService.manifest(); // Server has no data but this machine was synced before if (manifest === null && await this.hasPreviouslySynced()) { @@ -104,14 +108,30 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); } + const sessionId = this.storageService.get(SESSION_ID_KEY, StorageScope.GLOBAL); + // Server session is different from client session + if (sessionId && manifest && sessionId !== manifest.session) { + throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); + } + for (const synchroniser of this.synchronisers) { try { - await synchroniser.sync(manifest ? manifest[synchroniser.resourceKey] : undefined); + await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resourceKey] : undefined); } catch (e) { this.handleSyncError(e, synchroniser.source); } } + // After syncing, get the manifest if it was not available before + if (manifest === null) { + manifest = await this.userDataSyncStoreService.manifest(); + } + + // Update local session id + if (manifest && manifest.session !== sessionId) { + this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL); + } + this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); } finally { @@ -140,26 +160,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return synchroniser.accept(content); } - private async hasPreviouslySynced(): Promise { - await this.checkEnablement(); - for (const synchroniser of this.synchronisers) { - if (await synchroniser.hasPreviouslySynced()) { - return true; - } - } - return false; - } - - private async hasLocalData(): Promise { - await this.checkEnablement(); - for (const synchroniser of this.synchronisers) { - if (await synchroniser.hasLocalData()) { - return true; - } - } - return false; - } - async getRemoteContent(source: SyncSource, preview: boolean): Promise { await this.checkEnablement(); for (const synchroniser of this.synchronisers) { @@ -189,6 +189,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ async resetLocal(): Promise { await this.checkEnablement(); + this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL); for (const synchroniser of this.synchronisers) { try { synchroniser.resetLocal(); @@ -199,6 +200,26 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } + private async hasPreviouslySynced(): Promise { + await this.checkEnablement(); + for (const synchroniser of this.synchronisers) { + if (await synchroniser.hasPreviouslySynced()) { + return true; + } + } + return false; + } + + private async hasLocalData(): Promise { + await this.checkEnablement(); + for (const synchroniser of this.synchronisers) { + if (await synchroniser.hasLocalData()) { + return true; + } + } + return false; + } + private async resetRemote(): Promise { await this.checkEnablement(); try { diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index f423e76d6b..1dc583d5b3 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -6,7 +6,6 @@ import { Disposable, } from 'vs/base/common/lifecycle'; import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request'; -import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; @@ -33,7 +32,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new Error('No settings sync store url configured.'); } - const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key, 'latest').toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource', key, 'latest').toString(); const headers: IHeaders = {}; // Disable caching as they are cached by synchronisers headers['Cache-Control'] = 'no-cache'; @@ -65,7 +64,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new Error('No settings sync store url configured.'); } - const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key).toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource', key).toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; if (ref) { headers['If-Match'] = ref; @@ -89,7 +88,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new Error('No settings sync store url configured.'); } - const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', 'latest').toString(); + const url = joinPath(this.userDataSyncStore.url, 'manifest').toString(); const headers: IHeaders = { 'Content-Type': 'application/json' }; const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None); @@ -105,7 +104,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new Error('No settings sync store url configured.'); } - const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource').toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource').toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None); @@ -134,6 +133,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } if (context.res.statusCode === 401) { + this.authTokenService.sendTokenFailed(); throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source); } diff --git a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts index 1e7c456bbd..570539acca 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -5,11 +5,7 @@ import * as assert from 'assert'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; -import { FormattingOptions } from 'vs/base/common/jsonFormatter'; -import { URI } from 'vs/base/common/uri'; -import type { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; suite('KeybindingsMerge - No Conflicts', () => { @@ -613,7 +609,7 @@ suite('KeybindingsMerge - No Conflicts', () => { }); async function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) { - const userDataSyncUtilService = new MockUserDataSyncUtilService(); + const userDataSyncUtilService = new TestUserDataSyncUtilService(); const formattingOptions = await userDataSyncUtilService.resolveFormattingOptions(); return merge(localContent, remoteContent, baseContent, formattingOptions, userDataSyncUtilService); } @@ -621,22 +617,3 @@ async function mergeKeybindings(localContent: string, remoteContent: string, bas function stringify(value: any): string { return JSON.stringify(value, null, '\t'); } - -class MockUserDataSyncUtilService implements IUserDataSyncUtilService { - - _serviceBrand: any; - - async resolveUserBindings(userbindings: string[]): Promise> { - const keys: IStringDictionary = {}; - for (const keybinding of userbindings) { - keys[keybinding] = keybinding; - } - return keys; - } - - async resolveFormattingOptions(file?: URI): Promise { - return { eol: '\n', insertSpaces: false, tabSize: 4 }; - } - - async ignoreExtensionsToSync(extensions: IExtensionIdentifier[]): Promise { } -} diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts new file mode 100644 index 0000000000..e179b1f156 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -0,0 +1,246 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRequestService } from 'vs/platform/request/common/request'; +import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataAuthTokenService, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { generateUuid } from 'vs/base/common/uuid'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NullLogService, ILogService } from 'vs/platform/log/common/log'; +import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { Schemas } from 'vs/base/common/network'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; +import { IGlobalExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, DidInstallExtensionEvent, DidUninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; +import { Emitter } from 'vs/base/common/event'; + +export class UserDataSyncClient extends Disposable { + + readonly instantiationService: TestInstantiationService; + + constructor(readonly testServer: UserDataSyncTestServer = new UserDataSyncTestServer()) { + super(); + this.instantiationService = new TestInstantiationService(); + } + + async setUp(empty: boolean = false): Promise { + const userDataDirectory = URI.file('userdata').with({ scheme: Schemas.inMemory }); + const userDataSyncHome = joinPath(userDataDirectory, '.sync'); + const environmentService = this.instantiationService.stub(IEnvironmentService, >{ + userDataSyncHome, + settingsResource: joinPath(userDataDirectory, 'settings.json'), + settingsSyncPreviewResource: joinPath(userDataSyncHome, 'settings.json'), + keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'), + keybindingsSyncPreviewResource: joinPath(userDataSyncHome, 'keybindings.json'), + argvResource: joinPath(userDataDirectory, 'argv.json'), + }); + + const logService = new NullLogService(); + this.instantiationService.stub(ILogService, logService); + + const fileService = this._register(new FileService(logService)); + fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); + this.instantiationService.stub(IFileService, fileService); + + this.instantiationService.stub(IStorageService, new InMemoryStorageService()); + + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ + 'configurationSync.store': { + url: this.testServer.url, + authenticationProviderId: 'test' + } + }))); + + const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + await configurationService.initialize(); + this.instantiationService.stub(IConfigurationService, configurationService); + + this.instantiationService.stub(IRequestService, this.testServer); + this.instantiationService.stub(IUserDataAuthTokenService, >{ + onDidChangeToken: new Emitter().event, + async getToken() { return 'token'; } + }); + + this.instantiationService.stub(IUserDataSyncLogService, logService); + this.instantiationService.stub(ITelemetryService, NullTelemetryService); + this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); + this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService()); + this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService)); + + this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService)); + this.instantiationService.stub(IExtensionManagementService, >{ + async getInstalled() { return []; }, + onDidInstallExtension: new Emitter().event, + onDidUninstallExtension: new Emitter().event, + }); + this.instantiationService.stub(IExtensionGalleryService, >{ + isEnabled() { return true; }, + async getCompatibleExtension() { return null; } + }); + + this.instantiationService.stub(ISettingsSyncService, this.instantiationService.createInstance(SettingsSynchroniser)); + this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService)); + + if (empty) { + await fileService.del(environmentService.settingsResource); + } else { + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' }))); + } + await configurationService.reloadConfiguration(); + } + +} + +export class UserDataSyncTestServer implements IRequestService { + + _serviceBrand: any; + + readonly url: string = 'http://host:3000'; + private session: string | null = null; + private readonly data: Map = new Map(); + + private _requests: { url: string, type: string, headers?: IHeaders }[] = []; + get requests(): { url: string, type: string, headers?: IHeaders }[] { return this._requests; } + + private _responses: { status: number }[] = []; + get responses(): { status: number }[] { return this._responses; } + reset(): void { this._requests = []; this._responses = []; } + + async resolveProxy(url: string): Promise { return url; } + + async request(options: IRequestOptions, token: CancellationToken): Promise { + const headers: IHeaders = {}; + if (options.headers) { + if (options.headers['If-None-Match']) { + headers['If-None-Match'] = options.headers['If-None-Match']; + } + if (options.headers['If-Match']) { + headers['If-Match'] = options.headers['If-Match']; + } + } + this._requests.push({ url: options.url!, type: options.type!, headers }); + const requestContext = await this.doRequest(options); + this._responses.push({ status: requestContext.res.statusCode! }); + return requestContext; + } + + private async doRequest(options: IRequestOptions): Promise { + const versionUrl = `${this.url}/v1/`; + const relativePath = options.url!.indexOf(versionUrl) === 0 ? options.url!.substring(versionUrl.length) : undefined; + const segments = relativePath ? relativePath.split('/') : []; + if (options.type === 'GET' && segments.length === 1 && segments[0] === 'manifest') { + return this.getManifest(options.headers); + } + if (options.type === 'GET' && segments.length === 3 && segments[0] === 'resource' && segments[2] === 'latest') { + return this.getLatestData(segments[1], options.headers); + } + if (options.type === 'POST' && segments.length === 2 && segments[0] === 'resource') { + return this.writeData(segments[1], options.data, options.headers); + } + if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'resource') { + return this.clear(options.headers); + } + return this.toResponse(501); + } + + private async getManifest(headers?: IHeaders): Promise { + if (this.session) { + const latest: Record = Object.create({}); + const manifest: IUserDataManifest = { session: this.session, latest }; + this.data.forEach((value, key) => latest[key] = value.ref); + return this.toResponse(200, { 'Content-Type': 'application/json' }, JSON.stringify(manifest)); + } + return this.toResponse(204); + } + + private async getLatestData(resource: string, headers: IHeaders = {}): Promise { + const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource); + if (resourceKey) { + const data = this.data.get(resourceKey); + if (!data) { + return this.toResponse(204, { etag: '0' }); + } + if (headers['If-None-Match'] === data.ref) { + return this.toResponse(304); + } + return this.toResponse(200, { etag: data.ref }, data.content || ''); + } + return this.toResponse(204); + } + + private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise { + if (!headers['If-Match']) { + return this.toResponse(428); + } + if (!this.session) { + this.session = generateUuid(); + } + const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource); + if (resourceKey) { + const data = this.data.get(resourceKey); + if (headers['If-Match'] !== (data ? data.ref : '0')) { + return this.toResponse(412); + } + const ref = `${parseInt(data?.ref || '0') + 1}`; + this.data.set(resourceKey, { ref, content }); + return this.toResponse(200, { etag: ref }); + } + return this.toResponse(204); + } + + private async clear(headers?: IHeaders): Promise { + this.data.clear(); + this.session = null; + return this.toResponse(204); + } + + private toResponse(statusCode: number, headers?: IHeaders, data?: string): IRequestContext { + return { + res: { + headers: headers || {}, + statusCode + }, + stream: bufferToStream(VSBuffer.fromString(data || '')) + }; + } +} + +export class TestUserDataSyncUtilService implements IUserDataSyncUtilService { + + _serviceBrand: any; + + async resolveUserBindings(userbindings: string[]): Promise> { + const keys: IStringDictionary = {}; + for (const keybinding of userbindings) { + keys[keybinding] = keybinding; + } + return keys; + } + + async resolveFormattingOptions(file?: URI): Promise { + return { eol: '\n', insertSpaces: false, tabSize: 4 }; + } + +} + diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts new file mode 100644 index 0000000000..626bd7955a --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -0,0 +1,555 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IUserDataSyncService, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { VSBuffer } from 'vs/base/common/buffer'; + +suite('UserDataSyncService', () => { + + const disposableStore = new DisposableStore(); + + teardown(() => disposableStore.clear()); + + test('test first time sync ever', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncService); + + // Sync for first time + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { 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': '0' } }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + ]); + + }); + + test('test first time sync ever with no data', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(true); + const testObject = client.instantiationService.get(IUserDataSyncService); + + // Sync for first time + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { 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: {} }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + ]); + + }); + + test('test first time sync from the client with no changes - pull', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + + // Sync (pull) from the test client + target.reset(); + await testObject.isFirstTimeSyncWithMerge(); + await testObject.pull(); + + assert.deepEqual(target.requests, [ + // Manifest + { 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: {} }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + ]); + + }); + + test('test first time sync from the client with changes - pull', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client with changes + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + const fileService = testClient.instantiationService.get(IFileService); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + + // Sync (pull) from the test client + target.reset(); + await testObject.isFirstTimeSyncWithMerge(); + await testObject.pull(); + + assert.deepEqual(target.requests, [ + // Manifest + { 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: {} }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + ]); + + }); + + test('test first time sync from the client with no changes - merge', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + + // Sync (merge) from the test client + target.reset(); + await testObject.isFirstTimeSyncWithMerge(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { 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: {} }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + ]); + + }); + + test('test first time sync from the client with changes - merge', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client with changes + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const fileService = testClient.instantiationService.get(IFileService); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + + // Sync (merge) from the test client + target.reset(); + await testObject.isFirstTimeSyncWithMerge(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { 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: {} }, + { 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' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + ]); + + }); + + test('test sync when there are no changes', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // sync from the client again + target.reset(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + ]); + }); + + test('test sync when there are local changes', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + target.reset(); + + // Do changes in the client + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + + // Sync from the client + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, + // Keybindings + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, + // Global state + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, + ]); + }); + + test('test sync when there are remote changes', async () => { + const target = new UserDataSyncTestServer(); + + // Sync from first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Sync from test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Do changes in first client and sync + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Sync from test client + target.reset(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } }, + ]); + + }); + + test('test delete', async () => { + const target = new UserDataSyncTestServer(); + + // Sync from the client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Reset from the client + target.reset(); + await testObject.reset(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} }, + ]); + + }); + + test('test delete and sync', async () => { + const target = new UserDataSyncTestServer(); + + // Sync from the client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Reset from the client + await testObject.reset(); + + // Sync again + target.reset(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { 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': '0' } }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + ]); + + }); + + test('test delete on one client throws turned off error on other client while syncing', async () => { + const target = new UserDataSyncTestServer(); + + // Set up and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Reset from the first client + await client.instantiationService.get(IUserDataSyncService).reset(); + + // Sync from the test client + target.reset(); + try { + await testObject.sync(); + } catch (e) { + assert.ok(e instanceof UserDataSyncError); + assert.deepEqual((e).code, UserDataSyncErrorCode.TurnedOff); + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + ]); + return; + } + throw assert.fail('Should fail with turned off error'); + }); + + test('test creating new session from one client throws session expired error on another client while syncing', async () => { + const target = new UserDataSyncTestServer(); + + // Set up and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Reset from the first client + await client.instantiationService.get(IUserDataSyncService).reset(); + + // Sync again from the first client to create new session + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Sync from the test client + target.reset(); + try { + await testObject.sync(); + } catch (e) { + assert.ok(e instanceof UserDataSyncError); + assert.deepEqual((e).code, UserDataSyncErrorCode.SessionExpired); + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + ]); + return; + } + throw assert.fail('Should fail with turned off error'); + }); + + test('test sync status', async () => { + const target = new UserDataSyncTestServer(); + + // Setup the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncService); + + // sync from the client + const actualStatuses: SyncStatus[] = []; + const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status)); + await testObject.sync(); + + disposable.dispose(); + assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]); + }); + + test('test sync conflicts status', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + let fileService = client.instantiationService.get(IFileService); + let environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + fileService = testClient.instantiationService.get(IFileService); + environmentService = testClient.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 }))); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + + // sync from the client + await testObject.sync(); + + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + assert.deepEqual(testObject.conflictsSources, [SyncSource.Settings]); + }); + + test('test sync will sync other non conflicted areas', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + let fileService = client.instantiationService.get(IFileService); + let environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client and get conflicts in settings + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + let testFileService = testClient.instantiationService.get(IFileService); + let testEnvironmentService = testClient.instantiationService.get(IEnvironmentService); + await testFileService.writeFile(testEnvironmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 }))); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // sync from the first client with changes in keybindings + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // sync from the test client + target.reset(); + const actualStatuses: SyncStatus[] = []; + const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status)); + await testObject.sync(); + + disposable.dispose(); + assert.deepEqual(actualStatuses, []); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } }, + ]); + }); + + test('test stop sync reset status', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + let fileService = client.instantiationService.get(IFileService); + let environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + fileService = testClient.instantiationService.get(IFileService); + environmentService = testClient.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 }))); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // sync from the client + await testObject.stop(); + + assert.deepEqual(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflictsSources, []); + }); + +}); diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index 44da9c39ee..0ee28eafcf 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -15,6 +15,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { URI } from 'vs/base/common/uri'; +import { IWaitUntil } from 'vs/base/common/event'; @extHostCustomer export class MainThreadFileSystemEventService { @@ -28,6 +31,7 @@ export class MainThreadFileSystemEventService { @IProgressService progressService: IProgressService, @IConfigurationService configService: IConfigurationService, @ILogService logService: ILogService, + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService ) { const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); @@ -38,7 +42,7 @@ export class MainThreadFileSystemEventService { changed: [], deleted: [] }; - this._listener.add(fileService.onFileChanges(event => { + this._listener.add(fileService.onDidFilesChange(event => { for (let change of event.changes) { switch (change.type) { case FileChangeType.ADDED: @@ -66,9 +70,7 @@ export class MainThreadFileSystemEventService { messages.set(FileOperation.DELETE, localize('msg-delete', "Running 'File Delete' participants...")); messages.set(FileOperation.MOVE, localize('msg-rename', "Running 'File Rename' participants...")); - - this._listener.add(textFileService.onWillRunOperation(e => { - + function participateInFileOperation(e: IWaitUntil, operation: FileOperation, target: URI, source?: URI): void { const timeout = configService.getValue('files.participants.timeout'); if (timeout <= 0) { return; // disabled @@ -76,19 +78,19 @@ export class MainThreadFileSystemEventService { const p = progressService.withProgress({ location: ProgressLocation.Window }, progress => { - progress.report({ message: messages.get(e.operation) }); + progress.report({ message: messages.get(operation) }); return new Promise((resolve, reject) => { const cts = new CancellationTokenSource(); const timeoutHandle = setTimeout(() => { - logService.trace('CANCELLED file participants because of timeout', timeout, e.target, e.operation); + logService.trace('CANCELLED file participants because of timeout', timeout, target, operation); cts.cancel(); reject(new Error('timeout')); }, timeout); - proxy.$onWillRunFileOperation(e.operation, e.target, e.source, timeout, cts.token) + proxy.$onWillRunFileOperation(operation, target, source, timeout, cts.token) .then(resolve, reject) .finally(() => clearTimeout(timeoutHandle)); }); @@ -96,10 +98,14 @@ export class MainThreadFileSystemEventService { }); e.waitUntil(p); - })); + } + + this._listener.add(textFileService.onWillCreateTextFile(e => participateInFileOperation(e, FileOperation.CREATE, e.resource))); + this._listener.add(workingCopyFileService.onBeforeWorkingCopyFileOperation(e => participateInFileOperation(e, e.operation, e.target, e.source))); // AFTER file operation - this._listener.add(textFileService.onDidRunOperation(e => proxy.$onDidRunFileOperation(e.operation, e.target, e.source))); + this._listener.add(textFileService.onDidCreateTextFile(e => proxy.$onDidRunFileOperation(FileOperation.CREATE, e.resource, undefined))); + this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.target, e.source))); } dispose(): void { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index c7eb18ae12..aa56ff1725 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -11,7 +11,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto } from '../common/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -36,6 +36,34 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageFeatures); this._modeService = modeService; + + if (this._modeService) { + const updateAllWordDefinitions = () => { + const langWordPairs = LanguageConfigurationRegistry.getWordDefinitions(); + let wordDefinitionDtos: ILanguageWordDefinitionDto[] = []; + for (const [languageId, wordDefinition] of langWordPairs) { + const language = this._modeService.getLanguageIdentifier(languageId); + if (!language) { + continue; + } + wordDefinitionDtos.push({ + languageId: language.language, + regexSource: wordDefinition.source, + regexFlags: wordDefinition.flags + }); + } + this._proxy.$setWordDefinitions(wordDefinitionDtos); + }; + LanguageConfigurationRegistry.onDidChange((e) => { + const wordDefinition = LanguageConfigurationRegistry.getWordDefinition(e.languageIdentifier.id); + this._proxy.$setWordDefinitions([{ + languageId: e.languageIdentifier.language, + regexSource: wordDefinition.source, + regexFlags: wordDefinition.flags + }]); + }); + updateAllWordDefinitions(); + } } dispose(): void { diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 0a6f1ed29a..160e919bd0 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -3,329 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IdleValue, raceCancellation } from 'vs/base/common/async'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; -import * as strings from 'vs/base/common/strings'; -import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; -import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; -import { ITextModel } from 'vs/editor/common/model'; -import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; -import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; -import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; -import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; -import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; +import { IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ISaveParticipant, IResolvedTextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileSaveParticipant, IResolvedTextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { SaveReason } from 'vs/workbench/common/editor'; import { ExtHostContext, ExtHostDocumentSaveParticipantShape, IExtHostContext } from '../common/extHost.protocol'; -import { ILabelService } from 'vs/platform/label/common/label'; import { canceled } from 'vs/base/common/errors'; -import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; +import { IDisposable } from 'vs/base/common/lifecycle'; -export interface ISaveParticipantParticipant { - participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }, progress: IProgress, token: CancellationToken): Promise; -} - -/* - * An update participant that ensures any un-tracked changes are synced to the JSON file contents for a - * Notebook before save occurs. While every effort is made to ensure model changes are notified and a listener - * updates the backing model in-place, this is a backup mechanism to hard-update the file before save in case - * some are missed. - */ -class NotebookUpdateParticipant implements ISaveParticipantParticipant { // {{SQL CARBON EDIT}} add notebook participant - - constructor( - @INotebookService private notebookService: INotebookService - ) { - // Nothing - } - - public participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise { - let uri = model.resource; - let notebookEditor = this.notebookService.findNotebookEditor(uri); - if (notebookEditor) { - notebookEditor.notebookParams.input.updateModel(); - } - return Promise.resolve(); - } -} - -class TrimWhitespaceParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService - ) { - // Nothing - } - - async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { - if (this.configurationService.getValue('files.trimTrailingWhitespace', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { - this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO); - } - } - - private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean): void { - let prevSelection: Selection[] = []; - let cursors: Position[] = []; - - const editor = findEditor(model, this.codeEditorService); - if (editor) { - // Find `prevSelection` in any case do ensure a good undo stack when pushing the edit - // Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump - prevSelection = editor.getSelections(); - if (isAutoSaved) { - cursors = prevSelection.map(s => s.getPosition()); - const snippetsRange = SnippetController2.get(editor).getSessionEnclosingRange(); - if (snippetsRange) { - for (let lineNumber = snippetsRange.startLineNumber; lineNumber <= snippetsRange.endLineNumber; lineNumber++) { - cursors.push(new Position(lineNumber, model.getLineMaxColumn(lineNumber))); - } - } - } - } - - const ops = trimTrailingWhitespace(model, cursors); - if (!ops.length) { - return; // Nothing to do - } - - model.pushEditOperations(prevSelection, ops, (_edits) => prevSelection); - } -} - -function findEditor(model: ITextModel, codeEditorService: ICodeEditorService): IActiveCodeEditor | null { - let candidate: IActiveCodeEditor | null = null; - - if (model.isAttachedToEditor()) { - for (const editor of codeEditorService.listCodeEditors()) { - if (editor.hasModel() && editor.getModel() === model) { - if (editor.hasTextFocus()) { - return editor; // favour focused editor if there are multiple - } - - candidate = editor; - } - } - } - - return candidate; -} - -export class FinalNewLineParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService - ) { - // Nothing - } - - async participate(model: IResolvedTextFileEditorModel, _env: { reason: SaveReason; }): Promise { - if (this.configurationService.getValue('files.insertFinalNewline', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { - this.doInsertFinalNewLine(model.textEditorModel); - } - } - - private doInsertFinalNewLine(model: ITextModel): void { - const lineCount = model.getLineCount(); - const lastLine = model.getLineContent(lineCount); - const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1; - - if (!lineCount || lastLineIsEmptyOrWhitespace) { - return; - } - - const edits = [EditOperation.insert(new Position(lineCount, model.getLineMaxColumn(lineCount)), model.getEOL())]; - const editor = findEditor(model, this.codeEditorService); - if (editor) { - editor.executeEdits('insertFinalNewLine', edits, editor.getSelections()); - } else { - model.pushEditOperations([], edits, () => null); - } - } -} - -export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService - ) { - // Nothing - } - - async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { - if (this.configurationService.getValue('files.trimFinalNewlines', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { - this.doTrimFinalNewLines(model.textEditorModel, env.reason === SaveReason.AUTO); - } - } - - /** - * returns 0 if the entire file is empty or whitespace only - */ - private findLastLineWithContent(model: ITextModel): number { - for (let lineNumber = model.getLineCount(); lineNumber >= 1; lineNumber--) { - const lineContent = model.getLineContent(lineNumber); - if (strings.lastNonWhitespaceIndex(lineContent) !== -1) { - // this line has content - return lineNumber; - } - } - // no line has content - return 0; - } - - private doTrimFinalNewLines(model: ITextModel, isAutoSaved: boolean): void { - const lineCount = model.getLineCount(); - - // Do not insert new line if file does not end with new line - if (lineCount === 1) { - return; - } - - let prevSelection: Selection[] = []; - let cannotTouchLineNumber = 0; - const editor = findEditor(model, this.codeEditorService); - if (editor) { - prevSelection = editor.getSelections(); - if (isAutoSaved) { - for (let i = 0, len = prevSelection.length; i < len; i++) { - const positionLineNumber = prevSelection[i].positionLineNumber; - if (positionLineNumber > cannotTouchLineNumber) { - cannotTouchLineNumber = positionLineNumber; - } - } - } - } - - const lastLineNumberWithContent = this.findLastLineWithContent(model); - const deleteFromLineNumber = Math.max(lastLineNumberWithContent + 1, cannotTouchLineNumber + 1); - const deletionRange = model.validateRange(new Range(deleteFromLineNumber, 1, lineCount, model.getLineMaxColumn(lineCount))); - - if (deletionRange.isEmpty()) { - return; - } - - model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], _edits => prevSelection); - - if (editor) { - editor.setSelections(prevSelection); - } - } -} - -class FormatOnSaveParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - ) { - // Nothing - } - - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { - - const model = editorModel.textEditorModel; - const overrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri }; - - if (env.reason === SaveReason.AUTO || !this._configurationService.getValue('editor.formatOnSave', overrides)) { - return undefined; - } - - progress.report({ message: localize('formatting', "Formatting") }); - const editorOrModel = findEditor(model, this._codeEditorService) || model; - await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, token); - } -} - -class CodeActionOnSaveParticipant implements ISaveParticipantParticipant { - - constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - ) { } - - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { - if (env.reason === SaveReason.AUTO) { - return undefined; - } - const model = editorModel.textEditorModel; - - const settingsOverrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.resource }; - const setting = this._configurationService.getValue<{ [kind: string]: boolean }>('editor.codeActionsOnSave', settingsOverrides); - if (!setting) { - return undefined; - } - - const codeActionsOnSave = Object.keys(setting) - .filter(x => setting[x]).map(x => new CodeActionKind(x)) - .sort((a, b) => { - if (CodeActionKind.SourceFixAll.contains(a)) { - if (CodeActionKind.SourceFixAll.contains(b)) { - return 0; - } - return -1; - } - if (CodeActionKind.SourceFixAll.contains(b)) { - return 1; - } - return 0; - }); - - if (!codeActionsOnSave.length) { - return undefined; - } - - const excludedActions = Object.keys(setting) - .filter(x => setting[x] === false) - .map(x => new CodeActionKind(x)); - - progress.report({ message: localize('codeaction', "Quick Fixes") }); - await this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, token); - } - - private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], token: CancellationToken): Promise { - for (const codeActionKind of codeActionsOnSave) { - const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, token); - try { - await this.applyCodeActions(actionsToRun.validActions); - } catch { - // Failure to apply a code action should not block other on save actions - } finally { - actionsToRun.dispose(); - } - } - } - - private async applyCodeActions(actionsToRun: readonly CodeAction[]) { - for (const action of actionsToRun) { - await this._instantiationService.invokeFunction(applyCodeAction, action); - } - } - - private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], token: CancellationToken) { - return getCodeActions(model, model.getFullModelRange(), { - type: CodeActionTriggerType.Auto, - filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, - }, token); - } -} - -class ExtHostSaveParticipant implements ISaveParticipantParticipant { +class ExtHostSaveParticipant implements ITextFileSaveParticipant { private readonly _proxy: ExtHostDocumentSaveParticipantShape; @@ -361,67 +51,19 @@ class ExtHostSaveParticipant implements ISaveParticipantParticipant { // The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace @extHostCustomer -export class SaveParticipant implements ISaveParticipant { +export class SaveParticipant { - private readonly _saveParticipants: IdleValue; + private _saveParticipantDisposable: IDisposable; constructor( extHostContext: IExtHostContext, @IInstantiationService instantiationService: IInstantiationService, - @IProgressService private readonly _progressService: IProgressService, - @ILogService private readonly _logService: ILogService, - @ILabelService private readonly _labelService: ILabelService, @ITextFileService private readonly _textFileService: ITextFileService ) { - this._saveParticipants = new IdleValue(() => [ - instantiationService.createInstance(TrimWhitespaceParticipant), - instantiationService.createInstance(CodeActionOnSaveParticipant), - instantiationService.createInstance(FormatOnSaveParticipant), - instantiationService.createInstance(FinalNewLineParticipant), - instantiationService.createInstance(TrimFinalNewLinesParticipant), - // {{SQL CARBON EDIT}} - instantiationService.createInstance(NotebookUpdateParticipant), - instantiationService.createInstance(ExtHostSaveParticipant, extHostContext), - ]); - // Set as save participant for all text files - this._textFileService.saveParticipant = this; + this._saveParticipantDisposable = this._textFileService.files.addSaveParticipant(instantiationService.createInstance(ExtHostSaveParticipant, extHostContext)); } dispose(): void { - this._textFileService.saveParticipant = undefined; - this._saveParticipants.dispose(); - } - - async participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise { - - const cts = new CancellationTokenSource(token); - - return this._progressService.withProgress({ - title: localize('saveParticipants', "Running Save Participants for '{0}'", this._labelService.getUriLabel(model.resource, { relative: true })), - location: ProgressLocation.Notification, - cancellable: true, - delay: model.isDirty() ? 3000 : 5000 - }, async progress => { - // undoStop before participation - model.textEditorModel.pushStackElement(); - - for (let p of this._saveParticipants.getValue()) { - if (cts.token.isCancellationRequested) { - break; - } - try { - const promise = p.participate(model, context, progress, cts.token); - await raceCancellation(promise, cts.token); - } catch (err) { - this._logService.warn(err); - } - } - - // undoStop after participation - model.textEditorModel.pushStackElement(); - }, () => { - // user cancel - cts.dispose(true); - }); + this._saveParticipantDisposable.dispose(); } } diff --git a/src/vs/workbench/api/browser/mainThreadSearch.ts b/src/vs/workbench/api/browser/mainThreadSearch.ts index 7b02646608..853ed5c8fa 100644 --- a/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -80,10 +80,14 @@ class SearchOperation { } addMatch(match: IFileMatch): void { - if (this.matches.has(match.resource.toString())) { - // Merge with previous IFileMatches + const existingMatch = this.matches.get(match.resource.toString()); + if (existingMatch) { // TODO@rob clean up text/file result types - this.matches.get(match.resource.toString())!.results!.push(...match.results!); + // If a file search returns the same file twice, we would enter this branch. + // It's possible that could happen, #90813 + if (existingMatch.results && match.results) { + existingMatch.results.push(...match.results); + } } else { this.matches.set(match.resource.toString(), match); } diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 71840a9690..580f2fe2b9 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -274,7 +274,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.webview.options = options; webviewInput.webview.extension = extension; - const resource = webviewInput.getResource(); + const resource = webviewInput.resource; const model = await this.retainCustomEditorModel(webviewInput, resource, viewType, capabilities); webviewInput.onDisposeWebview(() => { @@ -312,7 +312,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]) { - const model = await this._customEditorService.models.resolve(webviewInput.getResource(), webviewInput.viewType); + const model = await this._customEditorService.models.resolve(webviewInput.resource, webviewInput.viewType); const existingEntry = this._customEditorModels.get(model); if (existingEntry) { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 63c6a8954e..2cdb9ecc29 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -82,7 +82,7 @@ const viewDescriptor: IJSONSchema = { type: 'object', properties: { id: { - description: localize('vscode.extension.contributes.view.id', 'Identifier of the view. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.'), + description: localize('vscode.extension.contributes.view.id', 'Identifier of the view. This should be unique across all views. It is recommended to include your extension id as part of the view id. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.'), type: 'string' }, name: { @@ -100,7 +100,7 @@ const remoteViewDescriptor: IJSONSchema = { type: 'object', properties: { id: { - description: localize('vscode.extension.contributes.view.id', 'Identifier of the view. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.'), + description: localize('vscode.extension.contributes.view.id', 'Identifier of the view. This should be unique across all views. It is recommended to include your extension id as part of the view id. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.'), type: 'string' }, name: { @@ -375,16 +375,15 @@ class ViewsExtensionHandler implements IWorkbenchContribution { collector.warn(localize('ViewContainerDoesnotExist', "View container '{0}' does not exist and all views registered to it will be added to 'Explorer'.", entry.key)); } const container = viewContainer || this.getDefaultViewContainer(); - const registeredViews = this.viewsRegistry.getViews(container); const viewIds: string[] = []; const viewDescriptors = coalesce(entry.value.map((item, index) => { // validate if (viewIds.indexOf(item.id) !== -1) { - collector.error(localize('duplicateView1', "Cannot register multiple views with same id `{0}` in the view container `{1}`", item.id, container.id)); + collector.error(localize('duplicateView1', "Cannot register multiple views with same id `{0}`", item.id)); return null; } - if (registeredViews.some(v => v.id === item.id)) { - collector.error(localize('duplicateView2', "A view with id `{0}` is already registered in the view container `{1}`", item.id, container.id)); + if (this.viewsRegistry.getView(item.id) !== null) { + collector.error(localize('duplicateView2', "A view with id `{0}` is already registered.", item.id)); return null; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2617ff482e..38157a142a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1202,6 +1202,12 @@ export interface IOutgoingCallDto { to: ICallHierarchyItemDto; } +export interface ILanguageWordDefinitionDto { + languageId: string; + regexSource: string; + regexFlags: string +} + export interface ExtHostLanguageFeaturesShape { $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise; $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise; @@ -1244,6 +1250,7 @@ export interface ExtHostLanguageFeaturesShape { $provideCallHierarchyIncomingCalls(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $provideCallHierarchyOutgoingCalls(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseCallHierarchy(handle: number, sessionId: string): void; + $setWordDefinitions(wordDefinitions: ILanguageWordDefinitionDto[]): void; } export interface ExtHostQuickOpenShape { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index ca9ba3a99d..47c28aabbe 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -194,7 +194,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio } catch (err) { // TODO: write to log once we have one } - await allPromises; + await Promise.all(allPromises); } public isActivated(extensionId: ExtensionIdentifier): boolean { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index fcfd5813e7..b5acd16a40 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1916,4 +1916,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._proxy.$setLanguageConfiguration(handle, languageId, serializedConfiguration); return this._createDisposable(handle); } + + $setWordDefinitions(wordDefinitions: extHostProtocol.ILanguageWordDefinitionDto[]): void { + for (const wordDefinition of wordDefinitions) { + this._documents.setWordDefinitionFor(wordDefinition.languageId, new RegExp(wordDefinition.regexSource, wordDefinition.regexFlags)); + } + } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index a7cd607142..2a214549e9 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -123,16 +123,16 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi strictEnv?: boolean, hideFromUser?: boolean ): Promise { - const terminal = await this._proxy.$createTerminal({ name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser }); - this._name = terminal.name; - this._runQueuedRequests(terminal.id); + const result = await this._proxy.$createTerminal({ name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser }); + this._name = result.name; + this._runQueuedRequests(result.id); } public async createExtensionTerminal(): Promise { - const terminal = await this._proxy.$createTerminal({ name: this._name, isExtensionTerminal: true }); - this._name = terminal.name; - this._runQueuedRequests(terminal.id); - return terminal.id; + const result = await this._proxy.$createTerminal({ name: this._name, isExtensionTerminal: true }); + this._name = result.name; + this._runQueuedRequests(result.id); + return result.id; } public get name(): string { diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 1f69a9684d..2504cf60bb 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -46,15 +46,15 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, { name, shellPath, shellArgs }, name); - terminal.create(shellPath, shellArgs); this._terminals.push(terminal); + terminal.create(shellPath, shellArgs); return terminal; } public createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, options, options.name); - terminal.create(options.shellPath, options.shellArgs, options.cwd, options.env, /*options.waitOnExit*/ undefined, options.strictEnv, options.hideFromUser); this._terminals.push(terminal); + terminal.create(options.shellPath, options.shellArgs, options.cwd, options.env, /*options.waitOnExit*/ undefined, options.strictEnv, options.hideFromUser); return terminal; } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 51d7a020ee..c0ec047fc4 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -240,7 +240,7 @@ export class ResourcesDropHandler { // Untitled: always ensure that we open a new untitled editor for each file we drop if (droppedDirtyEditor.resource.scheme === Schemas.untitled) { - const untitledEditorResource = this.editorService.createInput({ mode: droppedDirtyEditor.mode, encoding: droppedDirtyEditor.encoding, forceUntitled: true }).getResource(); + const untitledEditorResource = this.editorService.createInput({ mode: droppedDirtyEditor.mode, encoding: droppedDirtyEditor.encoding, forceUntitled: true }).resource; if (untitledEditorResource) { droppedDirtyEditor.resource = untitledEditorResource; } @@ -299,7 +299,7 @@ export class ResourcesDropHandler { // Open in separate windows if we drop workspaces or just one folder if (toOpen.length > folderURIs.length || folderURIs.length === 1) { - await this.hostService.openWindow(toOpen, { forceReuseWindow: true }); + await this.hostService.openWindow(toOpen); } // folders.length > 1: Multiple folders: Create new workspace with folders and open diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index e59fe0f6e1..972315c9cf 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -239,7 +239,7 @@ enum Redraw { class ResourceLabelWidget extends IconLabel { private _onDidRender = this._register(new Emitter()); - readonly onDidRender: Event = this._onDidRender.event; + readonly onDidRender = this._onDidRender.event; private readonly renderDisposables = this._register(new DisposableStore()); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 96edafb304..c79b910756 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { EventType, addDisposableListener, addClass, removeClass, isAncestor, getClientArea, Dimension, toggleClass, position, size } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; @@ -25,6 +25,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { LifecyclePhase, StartupKind, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IEditor } from 'vs/editor/common/editorCommon'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -40,6 +41,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { INotificationService, NotificationsFilter } from 'vs/platform/notification/common/notification'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { WINDOW_ACTIVE_BORDER, WINDOW_INACTIVE_BORDER } from 'vs/workbench/common/theme'; +import { LineNumbersType } from 'vs/editor/common/config/editorOptions'; enum Settings { ACTIVITYBAR_VISIBLE = 'workbench.activityBar.visible', @@ -87,26 +89,26 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi //#region Events - private readonly _onZenModeChange: Emitter = this._register(new Emitter()); - readonly onZenModeChange: Event = this._onZenModeChange.event; + private readonly _onZenModeChange = this._register(new Emitter()); + readonly onZenModeChange = this._onZenModeChange.event; - private readonly _onFullscreenChange: Emitter = this._register(new Emitter()); - readonly onFullscreenChange: Event = this._onFullscreenChange.event; + private readonly _onFullscreenChange = this._register(new Emitter()); + readonly onFullscreenChange = this._onFullscreenChange.event; - private readonly _onCenteredLayoutChange: Emitter = this._register(new Emitter()); - readonly onCenteredLayoutChange: Event = this._onCenteredLayoutChange.event; + private readonly _onCenteredLayoutChange = this._register(new Emitter()); + readonly onCenteredLayoutChange = this._onCenteredLayoutChange.event; - private readonly _onMaximizeChange: Emitter = this._register(new Emitter()); - readonly onMaximizeChange: Event = this._onMaximizeChange.event; + private readonly _onMaximizeChange = this._register(new Emitter()); + readonly onMaximizeChange = this._onMaximizeChange.event; - private readonly _onPanelPositionChange: Emitter = this._register(new Emitter()); - readonly onPanelPositionChange: Event = this._onPanelPositionChange.event; + private readonly _onPanelPositionChange = this._register(new Emitter()); + readonly onPanelPositionChange = this._onPanelPositionChange.event; - private readonly _onPartVisibilityChange: Emitter = this._register(new Emitter()); - readonly onPartVisibilityChange: Event = this._onPartVisibilityChange.event; + private readonly _onPartVisibilityChange = this._register(new Emitter()); + readonly onPartVisibilityChange = this._onPartVisibilityChange.event; private readonly _onLayout = this._register(new Emitter()); - readonly onLayout: Event = this._onLayout.event; + readonly onLayout = this._onLayout.event; //#endregion @@ -120,6 +122,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private workbenchGrid!: SerializableGrid; + private editorWidgetSet = new Set(); + private disposed: boolean | undefined; private titleBarPartView!: ISerializableView; @@ -690,18 +694,33 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.zenMode.active = !this.state.zenMode.active; this.state.zenMode.transitionDisposables.clear(); - const setLineNumbers = (lineNumbers?: any) => this.editorService.visibleTextEditorWidgets.forEach(editor => { - // To properly reset line numbers we need to read the configuration for each editor respecting it's uri. - if (!lineNumbers && isCodeEditor(editor) && editor.hasModel()) { - const model = editor.getModel(); - lineNumbers = this.configurationService.getValue('editor.lineNumbers', { resource: model.uri, overrideIdentifier: model.getModeId() }); - } - if (!lineNumbers) { - lineNumbers = this.configurationService.getValue('editor.lineNumbers'); - } + const setLineNumbers = (lineNumbers?: LineNumbersType) => { + const setEditorLineNumbers = (editor: IEditor) => { + // To properly reset line numbers we need to read the configuration for each editor respecting it's uri. + if (!lineNumbers && isCodeEditor(editor) && editor.hasModel()) { + const model = editor.getModel(); + lineNumbers = this.configurationService.getValue('editor.lineNumbers', { resource: model.uri, overrideIdentifier: model.getModeId() }); + } + if (!lineNumbers) { + lineNumbers = this.configurationService.getValue('editor.lineNumbers'); + } - editor.updateOptions({ lineNumbers }); - }); + editor.updateOptions({ lineNumbers }); + }; + + if (!lineNumbers) { + // Reset line numbers on all editors visible and non-visible + for (const editor of this.editorWidgetSet) { + setEditorLineNumbers(editor); + } + this.editorWidgetSet.clear(); + } else { + this.editorService.visibleTextEditorWidgets.forEach(editor => { + this.editorWidgetSet.add(editor); + setEditorLineNumbers(editor); + }); + } + }; // Check if zen mode transitioned to full screen and if now we are out of zen mode // -> we need to go out of full screen (same goes for the centered editor layout) diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index fd648557bc..6ea2a75eb7 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -34,7 +34,7 @@ export abstract class Part extends Component implements ISerializableView { get dimension(): Dimension | undefined { return this._dimension; } protected _onDidVisibilityChange = this._register(new Emitter()); - readonly onDidVisibilityChange: Event = this._onDidVisibilityChange.event; + readonly onDidVisibilityChange = this._onDidVisibilityChange.event; private parent: HTMLElement | undefined; private titleArea: HTMLElement | undefined; diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 20fd73f157..ff15b48b09 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -19,7 +19,7 @@ import { Widget } from 'vs/base/browser/ui/widget'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { ITheme } from 'vs/platform/theme/common/themeService'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; export interface ICompositeBarItem { id: string; @@ -61,7 +61,7 @@ export class CompositeBar extends Widget implements ICompositeBar { private compositeTransfer: LocalSelectionTransfer; private readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + readonly onDidChange = this._onDidChange.event; constructor( items: ICompositeBarItem[], diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index 74defd79e0..2fbecf1ba4 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -39,7 +39,7 @@ export abstract class BaseEditor extends Panel implements IEditor { readonly minimumHeight = DEFAULT_EDITOR_MIN_DIMENSIONS.height; readonly maximumHeight = DEFAULT_EDITOR_MAX_DIMENSIONS.height; - readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; } | undefined> = Event.None; + readonly onDidSizeConstraintsChange = Event.None; protected _input: EditorInput | undefined; protected _options: EditorOptions | undefined; @@ -251,7 +251,7 @@ export class EditorMemento implements IEditorMemento { private doGetResource(resourceOrEditor: URI | EditorInput): URI | undefined { if (resourceOrEditor instanceof EditorInput) { - return resourceOrEditor.getResource(); + return resourceOrEditor.resource; } return resourceOrEditor; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index f2189a70e6..cfe8466964 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -231,7 +231,7 @@ export class BreadcrumbsControl { input = input.master; } - if (!input || !input.getResource() || !this._fileService.canHandleResource(input.getResource()!)) { + if (!input || !input.resource || !this._fileService.canHandleResource(input.resource!)) { // cleanup and return when there is no input or when // we cannot handle this input this._ckBreadcrumbsPossible.set(false); @@ -247,7 +247,7 @@ export class BreadcrumbsControl { this._ckBreadcrumbsVisible.set(true); this._ckBreadcrumbsPossible.set(true); - const uri = input.getResource()!; + const uri = input.resource; const editor = this._getActiveCodeEditor(); const model = new EditorBreadcrumbsModel( uri, editor, diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 65408800c3..1051e9f838 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -130,7 +130,7 @@ class UntitledTextEditorInputFactory implements IEditorInputFactory { const untitledTextEditorInput = editorInput; - let resource = untitledTextEditorInput.getResource(); + let resource = untitledTextEditorInput.resource; if (untitledTextEditorInput.model.hasAssociatedFilePath) { resource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); // untitled with associated file path use the local schema } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 82a43746b9..2fc28adf62 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -542,7 +542,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private toEditorTelemetryDescriptor(editor: EditorInput): object { const descriptor = editor.getTelemetryDescriptor(); - const resource = editor.getResource(); + const resource = editor.resource; const path = resource ? resource.scheme === Schemas.file ? resource.fsPath : resource.path : undefined; if (resource && path) { descriptor['resource'] = { mimeType: guessMimeTypes(resource).join(', '), scheme: resource.scheme, ext: extname(resource), path: hash(path) }; @@ -1580,7 +1580,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { get maximumHeight(): number { return this.editorControl.maximumHeight; } private _onDidChange = this._register(new Relay<{ width: number; height: number; } | undefined>()); - readonly onDidChange: Event<{ width: number; height: number; } | undefined> = this._onDidChange.event; + readonly onDidChange = this._onDidChange.event; layout(width: number, height: number): void { this.dimension = new Dimension(width, height); diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts index 518e48be6c..2b66f578a1 100644 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts @@ -60,7 +60,7 @@ export class RangeHighlightDecorations extends Disposable { private getEditor(resourceRange: IRangeHighlightDecoration): ICodeEditor | undefined { const activeEditor = this.editorService.activeEditor; - const resource = activeEditor && activeEditor.getResource(); + const resource = activeEditor && activeEditor.resource; if (resource) { if (resource.toString() === resourceRange.resource.toString()) { return this.editorService.activeTextEditorWidget as ICodeEditor; diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index a94085494e..6252f2b6dc 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -331,8 +331,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { let modified: URI | undefined; if (modelOrInput instanceof DiffEditorInput) { - original = modelOrInput.originalInput.getResource(); - modified = modelOrInput.modifiedInput.getResource(); + original = modelOrInput.originalInput.resource; + modified = modelOrInput.modifiedInput.resource; } else { original = modelOrInput.original.uri; modified = modelOrInput.modified.uri; diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index b11568422d..1d387d9245 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -58,7 +58,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { this.editorMemento = this.getEditorMemento(editorGroupService, BaseTextEditor.TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => { - const resource = this.getResource(); + const resource = this.getActiveResource(); const value = resource ? this.textResourceConfigurationService.getValue(resource) : undefined; return this.handleConfigurationChangeEvent(value); @@ -130,7 +130,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { // Editor for Text this.editorContainer = parent; - this.editorControl = this._register(this.createEditorControl(parent, this.computeConfiguration(this.textResourceConfigurationService.getValue(this.getResource())))); + this.editorControl = this._register(this.createEditorControl(parent, this.computeConfiguration(this.textResourceConfigurationService.getValue(this.getActiveResource())))); // Model & Language changes const codeEditor = getCodeEditor(this.editorControl); @@ -217,7 +217,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { } getViewState(): IEditorViewState | undefined { - const resource = this.input?.getResource(); + const resource = this.input?.resource; if (resource) { return withNullAsUndefined(this.retrieveTextEditorViewState(resource)); } @@ -266,7 +266,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { private updateEditorConfiguration(configuration?: IEditorConfiguration): void { if (!configuration) { - const resource = this.getResource(); + const resource = this.getActiveResource(); if (resource) { configuration = this.textResourceConfigurationService.getValue(resource); } @@ -292,7 +292,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { } } - private getResource(): URI | undefined { + private getActiveResource(): URI | undefined { const codeEditor = getCodeEditor(this.editorControl); if (codeEditor) { const model = codeEditor.getModel(); @@ -302,7 +302,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { } if (this.input) { - return this.input.getResource(); + return this.input.resource; } return undefined; diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 6c60adc719..94ca9e42ab 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -101,7 +101,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { private restoreTextResourceEditorViewState(editor: EditorInput, control: IEditor) { if (editor instanceof UntitledTextEditorInput || editor instanceof ResourceEditorInput) { - const viewState = this.loadTextEditorViewState(editor.getResource()); + const viewState = this.loadTextEditorViewState(editor.resource); if (viewState) { control.restoreViewState(viewState); } @@ -111,7 +111,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { protected getAriaLabel(): string { let ariaLabel: string; - const inputName = this.input instanceof UntitledTextEditorInput ? basenameOrAuthority(this.input.getResource()) : this.input?.getName(); + const inputName = this.input instanceof UntitledTextEditorInput ? basenameOrAuthority(this.input.resource) : this.input?.getName(); if (this.input?.isReadonly()) { ariaLabel = inputName ? nls.localize('readonlyEditorWithInputAriaLabel', "{0} readonly editor", inputName) : nls.localize('readonlyEditorAriaLabel', "Readonly editor"); } else { @@ -163,7 +163,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor { return; // only enabled for untitled and resource inputs } - const resource = input.getResource(); + const resource = input.resource; // Clear view state if input is disposed if (input.isDisposed()) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts index c1d34edd4f..ec134651db 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts @@ -23,10 +23,10 @@ export class NotificationsAlerts extends Disposable { } private registerListeners(): void { - this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); } - private onDidNotificationChange(e: INotificationChangeEvent): void { + private onDidChangeNotification(e: INotificationChangeEvent): void { if (e.kind === NotificationChangeType.ADD) { // ARIA alert for screen readers @@ -37,7 +37,7 @@ export class NotificationsAlerts extends Disposable { if (e.item.message.original instanceof Error) { console.error(e.item.message.original); } else { - console.error(toErrorMessage(e.item.message.value, true)); + console.error(toErrorMessage(e.item.message.linkedText.toString(), true)); } } } @@ -46,7 +46,7 @@ export class NotificationsAlerts extends Disposable { private triggerAriaAlert(notifiation: INotificationViewItem): void { // Trigger the alert again whenever the label changes - const listener = notifiation.onDidLabelChange(e => { + const listener = notifiation.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.MESSAGE) { this.doTriggerAriaAlert(notifiation); } @@ -60,13 +60,13 @@ export class NotificationsAlerts extends Disposable { private doTriggerAriaAlert(notifiation: INotificationViewItem): void { let alertText: string; if (notifiation.severity === Severity.Error) { - alertText = localize('alertErrorMessage', "Error: {0}", notifiation.message.value); + alertText = localize('alertErrorMessage', "Error: {0}", notifiation.message.linkedText.toString()); } else if (notifiation.severity === Severity.Warning) { - alertText = localize('alertWarningMessage', "Warning: {0}", notifiation.message.value); + alertText = localize('alertWarningMessage', "Warning: {0}", notifiation.message.linkedText.toString()); } else { - alertText = localize('alertInfoMessage', "Info: {0}", notifiation.message.value); + alertText = localize('alertInfoMessage', "Info: {0}", notifiation.message.linkedText.toString()); } alert(alertText); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index cbfa460d4f..5cd63a457a 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -58,7 +58,7 @@ export class NotificationsCenter extends Themable { } private registerListeners(): void { - this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); this._register(this.layoutService.onLayout(dimension => this.layout(dimension))); } @@ -167,7 +167,7 @@ export class NotificationsCenter extends Themable { return keybinding ? keybinding.getLabel() : null; } - private onDidNotificationChange(e: INotificationChangeEvent): void { + private onDidChangeNotification(e: INotificationChangeEvent): void { if (!this._isVisible) { return; // only if visible } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index b406950c46..e4759038c2 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -181,7 +181,7 @@ export class NotificationsList extends Themable { } // Restore DOM focus if we had focus before - if (listHasDOMFocus) { + if (this.isVisible && listHasDOMFocus) { list.domFocus(); } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index a92e2e6c75..73ecb863b3 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -34,11 +34,11 @@ export class NotificationsStatus extends Disposable { } private registerListeners(): void { - this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); - this._register(this.model.onDidStatusMessageChange(e => this.onDidStatusMessageChange(e))); + this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); + this._register(this.model.onDidChangeStatusMessage(e => this.onDidChangeStatusMessage(e))); } - private onDidNotificationChange(e: INotificationChangeEvent): void { + private onDidChangeNotification(e: INotificationChangeEvent): void { if (this.isNotificationsCenterVisible) { return; // no change if notification center is visible } @@ -101,7 +101,7 @@ export class NotificationsStatus extends Disposable { } } - private onDidStatusMessageChange(e: IStatusMessageChangeEvent): void { + private onDidChangeStatusMessage(e: IStatusMessageChangeEvent): void { const statusItem = e.item; switch (e.kind) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 65433efae0..70021862a1 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -90,11 +90,11 @@ export class NotificationsToasts extends Themable { this.model.notifications.forEach(notification => this.addToast(notification)); // Update toasts on notification changes - this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e))); + this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); }); // Filter - this._register(this.model.onDidFilterChange(filter => { + this._register(this.model.onDidChangeFilter(filter => { if (filter === NotificationsFilter.SILENT || filter === NotificationsFilter.ERROR) { this.hide(); } @@ -114,7 +114,7 @@ export class NotificationsToasts extends Themable { ]); } - private onDidNotificationChange(e: INotificationChangeEvent): void { + private onDidChangeNotification(e: INotificationChangeEvent): void { switch (e.kind) { case NotificationChangeType.ADD: return this.addToast(e.item); @@ -194,12 +194,12 @@ export class NotificationsToasts extends Themable { this.layoutContainer(maxDimensions.height); // Update when item height changes due to expansion - itemDisposables.add(item.onDidExpansionChange(() => { + itemDisposables.add(item.onDidChangeExpansion(() => { notificationList.updateNotificationsList(0, 1, [item]); })); // Update when item height potentially changes due to label changes - itemDisposables.add(item.onDidLabelChange(e => { + itemDisposables.add(item.onDidChangeLabel(e => { if (!item.expanded) { return; // dynamic height only applies to expanded notifications } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 5b5b49baf1..40f582bead 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; -import { clearNode, addClass, removeClass, toggleClass, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; +import { clearNode, addClass, removeClass, toggleClass, addDisposableListener, EventType, EventHelper, $ } from 'vs/base/browser/dom'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -23,6 +23,7 @@ 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 { @@ -136,39 +137,25 @@ class NotificationMessageRenderer { static render(message: INotificationMessage, actionHandler?: IMessageActionHandler): HTMLElement { const messageContainer = document.createElement('span'); - // Message has no links - if (message.links.length === 0) { - messageContainer.textContent = message.value; - } + for (const node of message.linkedText.nodes) { + if (typeof node === 'string') { + messageContainer.appendChild(document.createTextNode(node)); + } else { + let title = node.title; - // Message has links - else { - let index = 0; - for (const link of message.links) { - - const textBefore = message.value.substring(index, link.offset); - if (textBefore) { - messageContainer.appendChild(document.createTextNode(textBefore)); + if (!title && startsWith(node.href, 'command:')) { + title = localize('executeCommand', "Click to execute command '{0}'", node.href.substr('command:'.length)); + } else if (!title) { + title = node.href; } - const anchor = document.createElement('a'); - anchor.textContent = link.name; - anchor.title = link.title; - anchor.href = link.href; + const anchor = $('a', { href: node.href, title: title, }, node.label); if (actionHandler) { - actionHandler.toDispose.add(addDisposableListener(anchor, EventType.CLICK, () => actionHandler.callback(link.href))); + actionHandler.toDispose.add(addDisposableListener(anchor, EventType.CLICK, () => actionHandler.callback(node.href))); } messageContainer.appendChild(anchor); - - index = link.offset + link.length; - } - - // Add text after links if any - const textAfter = message.value.substring(index); - if (textAfter) { - messageContainer.appendChild(document.createTextNode(textAfter)); } } @@ -345,7 +332,7 @@ export class NotificationTemplateRenderer extends Disposable { this.renderProgress(notification); // Label Change Events - this.inputDisposables.add(notification.onDidLabelChange(event => { + this.inputDisposables.add(notification.onDidChangeLabel(event => { switch (event.kind) { case NotificationViewItemLabelKind.SEVERITY: this.renderSeverity(notification); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 90442e646e..58dc8a8cdd 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -76,7 +76,7 @@ export class PanelPart extends CompositePart implements IPanelService { //#endregion get onDidPanelOpen(): Event<{ panel: IPanel, focus: boolean; }> { return Event.map(this.onDidCompositeOpen.event, compositeOpen => ({ panel: compositeOpen.composite, focus: compositeOpen.focus })); } - readonly onDidPanelClose: Event = this.onDidCompositeClose.event; + readonly onDidPanelClose = this.onDidCompositeClose.event; private activePanelContextKey: IContextKey; private panelFocusContextKey: IContextKey; diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 8e903e425a..953e32b96e 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -817,7 +817,7 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry { } function resourceForEditorHistory(input: EditorInput, fileService: IFileService): URI | undefined { - const resource = input ? input.getResource() : undefined; + const resource = input ? input.resource : undefined; // For the editor history we only prefer resources that are either untitled or // can be handled by the file service which indicates they are editable resources. diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 8d731197b6..084e5158ab 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -71,7 +71,7 @@ display: none; } -.monaco-workbench .pane > .pane-body > .empty-view { +.monaco-workbench .pane > .pane-body > .welcome-view { width: 100%; height: 100%; padding: 0 20px 0 20px; @@ -79,12 +79,12 @@ box-sizing: border-box; } -.monaco-workbench .pane > .pane-body:not(.empty) > .empty-view, -.monaco-workbench .pane > .pane-body.empty > :not(.empty-view) { +.monaco-workbench .pane > .pane-body:not(.welcome) > .welcome-view, +.monaco-workbench .pane > .pane-body.welcome > :not(.welcome-view) { display: none; } -.monaco-workbench .pane > .pane-body > .empty-view .monaco-button { +.monaco-workbench .pane > .pane-body > .welcome-view .monaco-button { max-width: 260px; margin-left: auto; margin-right: auto; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index a73f88bf05..a326663bc3 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -25,7 +25,7 @@ import { PaneView, IPaneViewOptions, IPaneOptions, Pane, DefaultPaneDndControlle import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; @@ -38,7 +38,7 @@ import { Component } from 'vs/workbench/common/component'; import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; -import { parseLinkedText } from 'vs/base/browser/linkedText'; +import { parseLinkedText } from 'vs/base/common/linkedText'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Button } from 'vs/base/browser/ui/button/button'; import { Link } from 'vs/platform/opener/browser/link'; @@ -60,6 +60,88 @@ export interface IViewPaneOptions extends IPaneOptions { const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); +interface IItem { + readonly descriptor: IViewContentDescriptor; + visible: boolean; +} + +class ViewWelcomeController { + + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + private defaultItem: IItem | undefined; + private items: IItem[] = []; + get contents(): IViewContentDescriptor[] { + const visibleItems = this.items.filter(v => v.visible); + + if (visibleItems.length === 0 && this.defaultItem) { + return [this.defaultItem.descriptor]; + } + + return visibleItems.map(v => v.descriptor); + } + + private contextKeyService: IContextKeyService; + private disposables = new DisposableStore(); + + constructor( + private id: string, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + this.contextKeyService = contextKeyService.createScoped(); + this.disposables.add(this.contextKeyService); + + contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); + Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); + this.onDidChangeViewWelcomeContent(); + } + + private onDidChangeViewWelcomeContent(): void { + const descriptors = viewsRegistry.getViewWelcomeContent(this.id); + + this.items = []; + + for (const descriptor of descriptors) { + if (descriptor.when === 'default') { + this.defaultItem = { descriptor, visible: true }; + } else { + const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true; + this.items.push({ descriptor, visible }); + } + } + + this._onDidChange.fire(); + } + + private onDidChangeContext(): void { + let didChange = false; + + for (const item of this.items) { + if (!item.descriptor.when || item.descriptor.when === 'default') { + continue; + } + + const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when); + + if (item.visible === visible) { + continue; + } + + item.visible = visible; + didChange = true; + } + + if (didChange) { + this._onDidChange.fire(); + } + } + + dispose(): void { + this.disposables.dispose(); + } +} + export abstract class ViewPane extends Pane implements IView { private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; @@ -76,8 +158,8 @@ export abstract class ViewPane extends Pane implements IView { protected _onDidChangeTitleArea = this._register(new Emitter()); readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; - protected _onDidChangeEmptyState = this._register(new Emitter()); - readonly onDidChangeEmptyState: Event = this._onDidChangeEmptyState.event; + protected _onDidChangeViewWelcomeState = this._register(new Emitter()); + readonly onDidChangeViewWelcomeState: Event = this._onDidChangeViewWelcomeState.event; private focusedViewContextKey: IContextKey; @@ -95,8 +177,9 @@ export abstract class ViewPane extends Pane implements IView { protected twistiesContainer?: HTMLElement; private bodyContainer!: HTMLElement; - private emptyViewContainer!: HTMLElement; - private emptyViewDisposable: IDisposable = Disposable.None; + private viewWelcomeContainer!: HTMLElement; + private viewWelcomeDisposable: IDisposable = Disposable.None; + private viewWelcomeController: ViewWelcomeController; constructor( options: IViewPaneOptions, @@ -119,6 +202,8 @@ export abstract class ViewPane extends Pane implements IView { this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); this._register(this.menuActions.onDidChangeTitle(() => this.updateActions())); + + this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); } setVisible(visible: boolean): void { @@ -206,18 +291,15 @@ export abstract class ViewPane extends Pane implements IView { protected renderBody(container: HTMLElement): void { this.bodyContainer = container; - this.emptyViewContainer = append(container, $('.empty-view', { tabIndex: 0 })); + this.viewWelcomeContainer = append(container, $('.welcome-view', { tabIndex: 0 })); - // we should update our empty state whenever - const onEmptyViewContentChange = Event.any( - // the registry changes - Event.map(Event.filter(viewsRegistry.onDidChangeEmptyViewContent, id => id === this.id), () => this.isEmpty()), - // or the view's empty state changes - Event.latch(Event.map(this.onDidChangeEmptyState, () => this.isEmpty())) - ); + const onViewWelcomeChange = Event.any(this.viewWelcomeController.onDidChange, this.onDidChangeViewWelcomeState); + this._register(onViewWelcomeChange(this.updateViewWelcome, this)); + this.updateViewWelcome(); + } - this._register(onEmptyViewContentChange(this.updateEmptyState, this)); - this.updateEmptyState(this.isEmpty()); + protected layoutBody(height: number, width: number): void { + // noop } protected getProgressLocation(): string { @@ -286,26 +368,26 @@ export abstract class ViewPane extends Pane implements IView { // Subclasses to implement for saving state } - private updateEmptyState(isEmpty: boolean): void { - this.emptyViewDisposable.dispose(); + private updateViewWelcome(): void { + this.viewWelcomeDisposable.dispose(); - if (!isEmpty) { - removeClass(this.bodyContainer, 'empty'); - this.emptyViewContainer.innerHTML = ''; + if (!this.shouldShowWelcome()) { + removeClass(this.bodyContainer, 'welcome'); + this.viewWelcomeContainer.innerHTML = ''; return; } - const contents = viewsRegistry.getEmptyViewContent(this.id); + const contents = this.viewWelcomeController.contents; if (contents.length === 0) { - removeClass(this.bodyContainer, 'empty'); - this.emptyViewContainer.innerHTML = ''; + removeClass(this.bodyContainer, 'welcome'); + this.viewWelcomeContainer.innerHTML = ''; return; } const disposables = new DisposableStore(); - addClass(this.bodyContainer, 'empty'); - this.emptyViewContainer.innerHTML = ''; + addClass(this.bodyContainer, 'welcome'); + this.viewWelcomeContainer.innerHTML = ''; for (const { content } of contents) { const lines = content.split('\n'); @@ -317,13 +399,13 @@ export abstract class ViewPane extends Pane implements IView { continue; } - const p = append(this.emptyViewContainer, $('p')); + const p = append(this.viewWelcomeContainer, $('p')); const linkedText = parseLinkedText(line); - for (const node of linkedText) { + for (const node of linkedText.nodes) { if (typeof node === 'string') { append(p, document.createTextNode(node)); - } else if (linkedText.length === 1) { + } else if (linkedText.nodes.length === 1) { const button = new Button(p, { title: node.title }); button.label = node.label; button.onDidClick(_ => this.openerService.open(node.href), null, disposables); @@ -339,10 +421,10 @@ export abstract class ViewPane extends Pane implements IView { } } - this.emptyViewDisposable = disposables; + this.viewWelcomeDisposable = disposables; } - isEmpty(): boolean { + shouldShowWelcome(): boolean { return false; } } diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 75cd20a8cc..c1033f4163 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -68,7 +68,7 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { */ export class ViewletDescriptor extends CompositeDescriptor { - public static create( + static create( ctor: { new(...services: Services): Viewlet }, id: string, name: string, diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 18079b2cfc..d6d580bf75 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -41,7 +41,6 @@ import { joinPath } from 'vs/base/common/resources'; import { BrowserStorageService } from 'vs/platform/storage/browser/storageService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { getThemeTypeSelector, DARK, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService'; -import { InMemoryFileSystemProvider } from 'vs/workbench/services/userData/common/inMemoryUserDataProvider'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; @@ -51,6 +50,7 @@ import { InMemoryLogProvider } from 'vs/workbench/services/log/common/inMemoryLo import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { coalesce } from 'vs/base/common/arrays'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; class BrowserMain extends Disposable { diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 8dc7d6b7f8..b2b7cd680b 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -6,7 +6,7 @@ import 'vs/workbench/browser/style'; import { localize } from 'vs/nls'; -import { Event, Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; +import { Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; import { addClasses, addClass, removeClasses } from 'vs/base/browser/dom'; import { runWhenIdle } from 'vs/base/common/async'; import { getZoomLevel, isFirefox, isSafari, isChrome } from 'vs/base/browser/browser'; @@ -51,13 +51,13 @@ import { Extensions as PanelExtensions, PanelRegistry } from 'vs/workbench/brows export class Workbench extends Layout { private readonly _onBeforeShutdown = this._register(new Emitter()); - readonly onBeforeShutdown: Event = this._onBeforeShutdown.event; + readonly onBeforeShutdown = this._onBeforeShutdown.event; private readonly _onWillShutdown = this._register(new Emitter()); - readonly onWillShutdown: Event = this._onWillShutdown.event; + readonly onWillShutdown = this._onWillShutdown.event; private readonly _onShutdown = this._register(new Emitter()); - readonly onShutdown: Event = this._onShutdown.event; + readonly onShutdown = this._onShutdown.event; constructor( parent: HTMLElement, diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 1d6eb56183..0b7f657b81 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -363,9 +363,13 @@ export interface IEditorInput extends IDisposable { readonly onDidChangeLabel: Event; /** - * Returns the associated resource of this input. + * Returns the optional associated resource of this input. + * + * This resource should be unique for all editors of the same + * kind and is often used to identify the editor input among + * others. */ - getResource(): URI | undefined; + readonly resource: URI | undefined; /** * Unique type identifier for this inpput. @@ -470,11 +474,9 @@ export abstract class EditorInput extends Disposable implements IEditorInput { private disposed: boolean = false; - abstract getTypeId(): string; + abstract get resource(): URI | undefined; - getResource(): URI | undefined { - return undefined; - } + abstract getTypeId(): string; getName(): string { return `Editor ${this.getTypeId()}`; @@ -574,7 +576,7 @@ export abstract class TextResourceEditorInput extends EditorInput { private static readonly MEMOIZER = createMemoizer(); constructor( - protected readonly resource: URI, + public readonly resource: URI, @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @ITextFileService protected readonly textFileService: ITextFileService, @@ -584,15 +586,16 @@ export abstract class TextResourceEditorInput extends EditorInput { ) { super(); + this.registerListeners(); + } + + protected registerListeners(): void { + // Clear label memoizer on certain events that have impact this._register(this.labelService.onDidChangeFormatters(() => TextResourceEditorInput.MEMOIZER.clear())); this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(() => TextResourceEditorInput.MEMOIZER.clear())); } - getResource(): URI { - return this.resource; - } - getName(): string { return this.basename; } @@ -669,9 +672,7 @@ export abstract class TextResourceEditorInput extends EditorInput { return true; // resources without file support are always readonly } - const model = this.textFileService.files.get(this.resource); - - return model?.isReadonly() || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } isSaving(): boolean { @@ -763,7 +764,7 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeS /** * Gets the resource this editor is about. */ - getResource(): URI; + readonly resource: URI; /** * Sets the preferred encoding to use for this input. @@ -799,6 +800,10 @@ export class SideBySideEditorInput extends EditorInput { this.registerListeners(); } + get resource(): URI | undefined { + return undefined; + } + get master(): EditorInput { return this._master; } @@ -1312,7 +1317,7 @@ export function toResource(editor: IEditorInput | undefined, options?: IResource editor = options.supportSideBySide === SideBySideEditor.MASTER ? editor.master : editor.details; } - const resource = editor.getResource(); + const resource = editor.resource; if (!resource || !options || !options.filterByScheme) { return resource; } diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 60ff1dc25a..ccaa0074ba 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -141,7 +141,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel /** * Updates the text editor model with the provided value. If the value is the same as the model has, this is a no-op. */ - protected updateTextEditorModel(newValue?: ITextBufferFactory, preferredMode?: string): void { + updateTextEditorModel(newValue?: ITextBufferFactory, preferredMode?: string): void { if (!this.isResolved()) { return; } diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index b157a20656..88d64a952d 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -10,34 +10,34 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { isPromiseCanceledError } from 'vs/base/common/errors'; import { Action } from 'vs/base/common/actions'; import { isErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { startsWith } from 'vs/base/common/strings'; -import { localize } from 'vs/nls'; import { find, equals } from 'vs/base/common/arrays'; +import { parseLinkedText, LinkedText } from 'vs/base/common/linkedText'; export interface INotificationsModel { - // - // Notifications as Toasts/Center - // + //#region Notifications as Toasts/Center readonly notifications: INotificationViewItem[]; - readonly onDidNotificationChange: Event; - readonly onDidFilterChange: Event; + readonly onDidChangeNotification: Event; + readonly onDidChangeFilter: Event; addNotification(notification: INotification): INotificationHandle; setFilter(filter: NotificationsFilter): void; - // - // Notifications as Status - // + //#endregion + + + //#region Notifications as Status readonly statusMessage: IStatusMessageViewItem | undefined; - readonly onDidStatusMessageChange: Event; + readonly onDidChangeStatusMessage: Event; showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable; + + //#endregion } export const enum NotificationChangeType { @@ -87,19 +87,22 @@ export interface IStatusMessageChangeEvent { kind: StatusMessageChangeType; } -export class NotificationHandle implements INotificationHandle { +export class NotificationHandle extends Disposable implements INotificationHandle { - private readonly _onDidClose: Emitter = new Emitter(); - readonly onDidClose: Event = this._onDidClose.event; + private readonly _onDidClose = this._register(new Emitter()); + readonly onDidClose = this._onDidClose.event; + + constructor(private readonly item: INotificationViewItem, private readonly onClose: (item: INotificationViewItem) => void) { + super(); - constructor(private readonly item: INotificationViewItem, private readonly closeItem: (item: INotificationViewItem) => void) { this.registerListeners(); } private registerListeners(): void { Event.once(this.item.onDidClose)(() => { this._onDidClose.fire(); - this._onDidClose.dispose(); + + this.dispose(); }); } @@ -120,8 +123,9 @@ export class NotificationHandle implements INotificationHandle { } close(): void { - this.closeItem(this.item); - this._onDidClose.dispose(); + this.onClose(this.item); + + this.dispose(); } } @@ -129,14 +133,14 @@ export class NotificationsModel extends Disposable implements INotificationsMode private static readonly NO_OP_NOTIFICATION = new NoOpNotification(); - private readonly _onDidNotificationChange = this._register(new Emitter()); - readonly onDidNotificationChange: Event = this._onDidNotificationChange.event; + private readonly _onDidChangeNotification = this._register(new Emitter()); + readonly onDidChangeNotification = this._onDidChangeNotification.event; - private readonly _onDidStatusMessageChange = this._register(new Emitter()); - readonly onDidStatusMessageChange: Event = this._onDidStatusMessageChange.event; + private readonly _onDidChangeStatusMessage = this._register(new Emitter()); + readonly onDidChangeStatusMessage = this._onDidChangeStatusMessage.event; - private readonly _onDidFilterChange = this._register(new Emitter()); - readonly onDidFilterChange: Event = this._onDidFilterChange.event; + private readonly _onDidChangeFilter = this._register(new Emitter()); + readonly onDidChangeFilter = this._onDidChangeFilter.event; private readonly _notifications: INotificationViewItem[] = []; get notifications(): INotificationViewItem[] { return this._notifications; } @@ -149,7 +153,7 @@ export class NotificationsModel extends Disposable implements INotificationsMode setFilter(filter: NotificationsFilter): void { this.filter = filter; - this._onDidFilterChange.fire(filter); + this._onDidChangeFilter.fire(filter); } addNotification(notification: INotification): INotificationHandle { @@ -168,13 +172,13 @@ export class NotificationsModel extends Disposable implements INotificationsMode this._notifications.splice(0, 0, item); // Events - this._onDidNotificationChange.fire({ item, index: 0, kind: NotificationChangeType.ADD }); + this._onDidChangeNotification.fire({ item, index: 0, kind: NotificationChangeType.ADD }); // Wrap into handle - return new NotificationHandle(item, item => this.closeItem(item)); + return new NotificationHandle(item, item => this.onClose(item)); } - private closeItem(item: INotificationViewItem): void { + private onClose(item: INotificationViewItem): void { const liveItem = this.findNotification(item); if (liveItem && liveItem !== item) { liveItem.close(); // item could have been replaced with another one, make sure to close the live item @@ -197,13 +201,13 @@ export class NotificationsModel extends Disposable implements INotificationsMode const onItemChangeEvent = () => { const index = this._notifications.indexOf(item); if (index >= 0) { - this._onDidNotificationChange.fire({ item, index, kind: NotificationChangeType.CHANGE }); + this._onDidChangeNotification.fire({ item, index, kind: NotificationChangeType.CHANGE }); } }; - const itemExpansionChangeListener = item.onDidExpansionChange(() => onItemChangeEvent()); + const itemExpansionChangeListener = item.onDidChangeExpansion(() => onItemChangeEvent()); - const itemLabelChangeListener = item.onDidLabelChange(e => { + const itemLabelChangeListener = item.onDidChangeLabel(e => { // a label change in the area of actions or the message is a change that potentially has an impact // on the size of the notification and as such we emit a change event so that viewers can redraw if (e.kind === NotificationViewItemLabelKind.ACTIONS || e.kind === NotificationViewItemLabelKind.MESSAGE) { @@ -218,7 +222,7 @@ export class NotificationsModel extends Disposable implements INotificationsMode const index = this._notifications.indexOf(item); if (index >= 0) { this._notifications.splice(index, 1); - this._onDidNotificationChange.fire({ item, index, kind: NotificationChangeType.REMOVE }); + this._onDidChangeNotification.fire({ item, index, kind: NotificationChangeType.REMOVE }); } }); @@ -233,14 +237,14 @@ export class NotificationsModel extends Disposable implements INotificationsMode // Remember as current status message and fire events this._statusMessage = item; - this._onDidStatusMessageChange.fire({ kind: StatusMessageChangeType.ADD, item }); + this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.ADD, item }); return toDisposable(() => { // Only reset status message if the item is still the one we had remembered if (this._statusMessage === item) { this._statusMessage = undefined; - this._onDidStatusMessageChange.fire({ kind: StatusMessageChangeType.REMOVE, item }); + this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.REMOVE, item }); } }); } @@ -258,9 +262,9 @@ export interface INotificationViewItem { readonly expanded: boolean; readonly canCollapse: boolean; - readonly onDidExpansionChange: Event; + readonly onDidChangeExpansion: Event; readonly onDidClose: Event; - readonly onDidLabelChange: Event; + readonly onDidChangeLabel: Event; expand(): void; collapse(skipEvents?: boolean): void; @@ -309,8 +313,8 @@ export interface INotificationViewItemProgress extends INotificationProgress { export class NotificationViewItemProgress extends Disposable implements INotificationViewItemProgress { private readonly _state: INotificationViewItemProgressState; - private readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; constructor() { super(); @@ -388,31 +392,26 @@ export interface IMessageLink { export interface INotificationMessage { raw: string; original: NotificationMessage; - value: string; - links: IMessageLink[]; + linkedText: LinkedText; } export class NotificationViewItem extends Disposable implements INotificationViewItem { private static readonly MAX_MESSAGE_LENGTH = 1000; - // Example link: "Some message with [link text](http://link.href)." - // RegEx: [, anything not ], ], (, http://|https://|command:, no whitespace) - private static readonly LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: "([^"]+)")?\)/gi; - private _expanded: boolean | undefined; private _actions: INotificationActions | undefined; private _progress: NotificationViewItemProgress | undefined; - private readonly _onDidExpansionChange: Emitter = this._register(new Emitter()); - readonly onDidExpansionChange: Event = this._onDidExpansionChange.event; + private readonly _onDidChangeExpansion = this._register(new Emitter()); + readonly onDidChangeExpansion = this._onDidChangeExpansion.event; - private readonly _onDidClose: Emitter = this._register(new Emitter()); - readonly onDidClose: Event = this._onDidClose.event; + private readonly _onDidClose = this._register(new Emitter()); + readonly onDidClose = this._onDidClose.event; - private readonly _onDidLabelChange: Emitter = this._register(new Emitter()); - readonly onDidLabelChange: Event = this._onDidLabelChange.event; + private readonly _onDidChangeLabel = this._register(new Emitter()); + readonly onDidChangeLabel = this._onDidChangeLabel.event; static create(notification: INotification, filter: NotificationsFilter = NotificationsFilter.OFF): INotificationViewItem | undefined { if (!notification || !notification.message || isPromiseCanceledError(notification.message)) { @@ -464,23 +463,9 @@ export class NotificationViewItem extends Disposable implements INotificationVie message = message.replace(/(\r\n|\n|\r)/gm, ' ').trim(); // Parse Links - const links: IMessageLink[] = []; - message.replace(NotificationViewItem.LINK_REGEX, (matchString: string, name: string, href: string, title: string, offset: number) => { - let massagedTitle: string; - if (title && title.length > 0) { - massagedTitle = title; - } else if (startsWith(href, 'command:')) { - massagedTitle = localize('executeCommand', "Click to execute command '{0}'", href.substr('command:'.length)); - } else { - massagedTitle = href; - } + const linkedText = parseLinkedText(message); - links.push({ name, href, title: massagedTitle, offset, length: matchString.length }); - - return matchString; - }); - - return { raw, value: message, links, original: input }; + return { raw, linkedText, original: input }; } private constructor( @@ -561,7 +546,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie get progress(): INotificationViewItemProgress { if (!this._progress) { this._progress = this._register(new NotificationViewItemProgress()); - this._register(this._progress.onDidChange(() => this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.PROGRESS }))); + this._register(this._progress.onDidChange(() => this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.PROGRESS }))); } return this._progress; @@ -581,7 +566,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie updateSeverity(severity: Severity): void { this._severity = severity; - this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.SEVERITY }); + this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.SEVERITY }); } updateMessage(input: NotificationMessage): void { @@ -591,13 +576,13 @@ export class NotificationViewItem extends Disposable implements INotificationVie } this._message = message; - this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.MESSAGE }); + this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.MESSAGE }); } updateActions(actions?: INotificationActions): void { this.setActions(actions); - this._onDidLabelChange.fire({ kind: NotificationViewItemLabelKind.ACTIONS }); + this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.ACTIONS }); } expand(): void { @@ -606,7 +591,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie } this._expanded = true; - this._onDidExpansionChange.fire(); + this._onDidChangeExpansion.fire(); } collapse(skipEvents?: boolean): void { @@ -617,7 +602,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie this._expanded = false; if (!skipEvents) { - this._onDidExpansionChange.fire(); + this._onDidChangeExpansion.fire(); } } @@ -644,7 +629,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie return false; } - if (this._message.value !== other.message.value) { + if (this._message.raw !== other.message.raw) { return false; } @@ -656,8 +641,8 @@ export class NotificationViewItem extends Disposable implements INotificationVie export class ChoiceAction extends Action { - private readonly _onDidRun = new Emitter(); - readonly onDidRun: Event = this._onDidRun.event; + private readonly _onDidRun = this._register(new Emitter()); + readonly onDidRun = this._onDidRun.event; private readonly _keepOpen: boolean; @@ -679,12 +664,6 @@ export class ChoiceAction extends Action { get keepOpen(): boolean { return this._keepOpen; } - - dispose(): void { - super.dispose(); - - this._onDidRun.dispose(); - } } class StatusMessageViewItem { diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts index 5bd454a9e3..e9c3a09950 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import * as objects from 'vs/base/common/objects'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { basename, extname, relativePath } from 'vs/base/common/resources'; import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -106,8 +106,8 @@ export class ResourceGlobMatcher extends Disposable { private static readonly NO_ROOT: string | null = null; - private readonly _onExpressionChange: Emitter = this._register(new Emitter()); - readonly onExpressionChange: Event = this._onExpressionChange.event; + private readonly _onExpressionChange = this._register(new Emitter()); + readonly onExpressionChange = this._onExpressionChange.event; private readonly mapRootToParsedExpression: Map = new Map(); private readonly mapRootToExpressionConfig: Map = new Map(); diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index e30c349f63..44c2fa5c8f 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -213,6 +213,7 @@ export interface IViewDescriptorCollection extends IDisposable { export interface IViewContentDescriptor { readonly content: string; + readonly when?: ContextKeyExpr | 'default'; } export interface IViewsRegistry { @@ -235,9 +236,13 @@ export interface IViewsRegistry { getViewContainer(id: string): ViewContainer | null; - readonly onDidChangeEmptyViewContent: Event; - registerEmptyViewContent(id: string, viewContent: IViewContentDescriptor): IDisposable; - getEmptyViewContent(id: string): IViewContentDescriptor[]; + readonly onDidChangeViewWelcomeContent: Event; + registerViewWelcomeContent(id: string, viewContent: IViewContentDescriptor): IDisposable; + getViewWelcomeContent(id: string): IViewContentDescriptor[]; +} + +function compareViewContentDescriptors(a: IViewContentDescriptor, b: IViewContentDescriptor): number { + return a.content < b.content ? -1 : 1; } class ViewsRegistry extends Disposable implements IViewsRegistry { @@ -251,12 +256,12 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { private readonly _onDidChangeContainer: Emitter<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }> = this._register(new Emitter<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }>()); readonly onDidChangeContainer: Event<{ views: IViewDescriptor[], from: ViewContainer, to: ViewContainer }> = this._onDidChangeContainer.event; - private readonly _onDidChangeEmptyViewContent: Emitter = this._register(new Emitter()); - readonly onDidChangeEmptyViewContent: Event = this._onDidChangeEmptyViewContent.event; + private readonly _onDidChangeViewWelcomeContent: Emitter = this._register(new Emitter()); + readonly onDidChangeViewWelcomeContent: Event = this._onDidChangeViewWelcomeContent.event; private _viewContainers: ViewContainer[] = []; private _views: Map = new Map(); - private _emptyViewContents = new SetMap(); + private _viewWelcomeContents = new SetMap(); registerViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { this.addViews(views, viewContainer); @@ -306,19 +311,20 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { return null; } - registerEmptyViewContent(id: string, viewContent: IViewContentDescriptor): IDisposable { - this._emptyViewContents.add(id, viewContent); - this._onDidChangeEmptyViewContent.fire(id); + registerViewWelcomeContent(id: string, viewContent: IViewContentDescriptor): IDisposable { + this._viewWelcomeContents.add(id, viewContent); + this._onDidChangeViewWelcomeContent.fire(id); return toDisposable(() => { - this._emptyViewContents.delete(id, viewContent); - this._onDidChangeEmptyViewContent.fire(id); + this._viewWelcomeContents.delete(id, viewContent); + this._onDidChangeViewWelcomeContent.fire(id); }); } - getEmptyViewContent(id: string): IViewContentDescriptor[] { + getViewWelcomeContent(id: string): IViewContentDescriptor[] { const result: IViewContentDescriptor[] = []; - this._emptyViewContents.forEach(id, descriptor => result.push(descriptor)); + result.sort(compareViewContentDescriptors); + this._viewWelcomeContents.forEach(id, descriptor => result.push(descriptor)); return result; } @@ -330,8 +336,8 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { this._viewContainers.push(viewContainer); } for (const viewDescriptor of viewDescriptors) { - if (views.some(v => v.id === viewDescriptor.id)) { - throw new Error(localize('duplicateId', "A view with id '{0}' is already registered in the container '{1}'", viewDescriptor.id, viewContainer.id)); + if (this.getView(viewDescriptor.id) !== null) { + throw new Error(localize('duplicateId', "A view with id '{0}' is already registered", viewDescriptor.id)); } views.push(viewDescriptor); } diff --git a/src/vs/workbench/contrib/backup/common/backupRestorer.ts b/src/vs/workbench/contrib/backup/common/backupRestorer.ts index de01656f87..e8d7d7f39b 100644 --- a/src/vs/workbench/contrib/backup/common/backupRestorer.ts +++ b/src/vs/workbench/contrib/backup/common/backupRestorer.ts @@ -70,7 +70,7 @@ export class BackupRestorer implements IWorkbenchContribution { private findEditorByResource(resource: URI): IEditorInput | undefined { for (const editor of this.editorService.editors) { - if (isEqual(editor.getResource(), resource)) { + if (isEqual(editor.resource, resource)) { return editor; } } diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts index 07a93b6e0a..911024914e 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts @@ -125,7 +125,7 @@ suite.skip('BackupRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydresser let counter = 0; for (const editor of editorService.editors) { - const resource = editor.getResource(); + const resource = editor.resource; if (isEqual(resource, untitledFile1)) { const model = await accessor.textFileService.untitled.resolve({ untitledResource: resource }); assert.equal(model.textEditorModel.getValue(), 'untitled-1'); diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index 292fabdfff..86ae339ee9 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -170,13 +170,13 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.getResource()), true); + assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.resource), true); untitledModel.dispose(); await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.getResource()), false); + assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.resource), false); part.dispose(); tracker.dispose(); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts index 6782e99b8f..070f7ef05c 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts @@ -64,9 +64,9 @@ class UXState { let resource: URI | undefined; if (input instanceof DiffEditorInput) { - resource = input.modifiedInput.getResource(); + resource = input.modifiedInput.resource; } else { - resource = input.getResource(); + resource = input.resource; } if (resource?.scheme === BulkEditPreviewProvider.Schema) { diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts index cc6b775696..5b2b782fcf 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts @@ -24,7 +24,7 @@ suite('BulkEditPreview', function () { setup(function () { const fileService: IFileService = new class extends mock() { - onFileChanges = Event.None; + onDidFilesChange = Event.None; async exists() { return true; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index 38ae1273b4..8620f607ab 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -9,6 +9,7 @@ import './diffEditorHelper'; import './inspectKeybindings'; import './largeFileOptimizations'; import './inspectEditorTokens/inspectEditorTokens'; +import './saveParticipants'; import './toggleMinimap'; import './toggleMultiCursorModifier'; import './toggleRenderControlCharacter'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts new file mode 100644 index 0000000000..529991bc20 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -0,0 +1,342 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import * as strings from 'vs/base/common/strings'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ITextModel } from 'vs/editor/common/model'; +import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes'; +import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; +import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; +import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; +import { IResolvedTextFileEditorModel, ITextFileService, ITextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution, Extensions as WorkbenchContributionsExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; + +/* + * An update participant that ensures any un-tracked changes are synced to the JSON file contents for a + * Notebook before save occurs. While every effort is made to ensure model changes are notified and a listener + * updates the backing model in-place, this is a backup mechanism to hard-update the file before save in case + * some are missed. + */ +class NotebookUpdateParticipant implements ITextFileSaveParticipant { // {{SQL CARBON EDIT}} add notebook participant + + constructor( + @INotebookService private notebookService: INotebookService + ) { + // Nothing + } + + public participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise { + let uri = model.resource; + let notebookEditor = this.notebookService.findNotebookEditor(uri); + if (notebookEditor) { + notebookEditor.notebookParams.input.updateModel(); + } + return Promise.resolve(); + } +} + +class TrimWhitespaceParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService + ) { + // Nothing + } + + async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + if (this.configurationService.getValue('files.trimTrailingWhitespace', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { + this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO); + } + } + + private doTrimTrailingWhitespace(model: ITextModel, isAutoSaved: boolean): void { + let prevSelection: Selection[] = []; + let cursors: Position[] = []; + + const editor = findEditor(model, this.codeEditorService); + if (editor) { + // Find `prevSelection` in any case do ensure a good undo stack when pushing the edit + // Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump + prevSelection = editor.getSelections(); + if (isAutoSaved) { + cursors = prevSelection.map(s => s.getPosition()); + const snippetsRange = SnippetController2.get(editor).getSessionEnclosingRange(); + if (snippetsRange) { + for (let lineNumber = snippetsRange.startLineNumber; lineNumber <= snippetsRange.endLineNumber; lineNumber++) { + cursors.push(new Position(lineNumber, model.getLineMaxColumn(lineNumber))); + } + } + } + } + + const ops = trimTrailingWhitespace(model, cursors); + if (!ops.length) { + return; // Nothing to do + } + + model.pushEditOperations(prevSelection, ops, (_edits) => prevSelection); + } +} + +function findEditor(model: ITextModel, codeEditorService: ICodeEditorService): IActiveCodeEditor | null { + let candidate: IActiveCodeEditor | null = null; + + if (model.isAttachedToEditor()) { + for (const editor of codeEditorService.listCodeEditors()) { + if (editor.hasModel() && editor.getModel() === model) { + if (editor.hasTextFocus()) { + return editor; // favour focused editor if there are multiple + } + + candidate = editor; + } + } + } + + return candidate; +} + +export class FinalNewLineParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService + ) { + // Nothing + } + + async participate(model: IResolvedTextFileEditorModel, _env: { reason: SaveReason; }): Promise { + if (this.configurationService.getValue('files.insertFinalNewline', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { + this.doInsertFinalNewLine(model.textEditorModel); + } + } + + private doInsertFinalNewLine(model: ITextModel): void { + const lineCount = model.getLineCount(); + const lastLine = model.getLineContent(lineCount); + const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1; + + if (!lineCount || lastLineIsEmptyOrWhitespace) { + return; + } + + const edits = [EditOperation.insert(new Position(lineCount, model.getLineMaxColumn(lineCount)), model.getEOL())]; + const editor = findEditor(model, this.codeEditorService); + if (editor) { + editor.executeEdits('insertFinalNewLine', edits, editor.getSelections()); + } else { + model.pushEditOperations([], edits, () => null); + } + } +} + +export class TrimFinalNewLinesParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService + ) { + // Nothing + } + + async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + if (this.configurationService.getValue('files.trimFinalNewlines', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { + this.doTrimFinalNewLines(model.textEditorModel, env.reason === SaveReason.AUTO); + } + } + + /** + * returns 0 if the entire file is empty or whitespace only + */ + private findLastLineWithContent(model: ITextModel): number { + for (let lineNumber = model.getLineCount(); lineNumber >= 1; lineNumber--) { + const lineContent = model.getLineContent(lineNumber); + if (strings.lastNonWhitespaceIndex(lineContent) !== -1) { + // this line has content + return lineNumber; + } + } + // no line has content + return 0; + } + + private doTrimFinalNewLines(model: ITextModel, isAutoSaved: boolean): void { + const lineCount = model.getLineCount(); + + // Do not insert new line if file does not end with new line + if (lineCount === 1) { + return; + } + + let prevSelection: Selection[] = []; + let cannotTouchLineNumber = 0; + const editor = findEditor(model, this.codeEditorService); + if (editor) { + prevSelection = editor.getSelections(); + if (isAutoSaved) { + for (let i = 0, len = prevSelection.length; i < len; i++) { + const positionLineNumber = prevSelection[i].positionLineNumber; + if (positionLineNumber > cannotTouchLineNumber) { + cannotTouchLineNumber = positionLineNumber; + } + } + } + } + + const lastLineNumberWithContent = this.findLastLineWithContent(model); + const deleteFromLineNumber = Math.max(lastLineNumberWithContent + 1, cannotTouchLineNumber + 1); + const deletionRange = model.validateRange(new Range(deleteFromLineNumber, 1, lineCount, model.getLineMaxColumn(lineCount))); + + if (deletionRange.isEmpty()) { + return; + } + + model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], _edits => prevSelection); + + if (editor) { + editor.setSelections(prevSelection); + } + } +} + +class FormatOnSaveParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + // Nothing + } + + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { + const model = editorModel.textEditorModel; + const overrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri }; + + if (env.reason === SaveReason.AUTO || !this.configurationService.getValue('editor.formatOnSave', overrides)) { + return undefined; + } + + progress.report({ message: localize('formatting', "Formatting") }); + const editorOrModel = findEditor(model, this.codeEditorService) || model; + await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, token); + } +} + +class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { + if (env.reason === SaveReason.AUTO) { + return undefined; + } + const model = editorModel.textEditorModel; + + const settingsOverrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.resource }; + const setting = this.configurationService.getValue<{ [kind: string]: boolean }>('editor.codeActionsOnSave', settingsOverrides); + if (!setting) { + return undefined; + } + + const codeActionsOnSave = Object.keys(setting) + .filter(x => setting[x]).map(x => new CodeActionKind(x)) + .sort((a, b) => { + if (CodeActionKind.SourceFixAll.contains(a)) { + if (CodeActionKind.SourceFixAll.contains(b)) { + return 0; + } + return -1; + } + if (CodeActionKind.SourceFixAll.contains(b)) { + return 1; + } + return 0; + }); + + if (!codeActionsOnSave.length) { + return undefined; + } + + const excludedActions = Object.keys(setting) + .filter(x => setting[x] === false) + .map(x => new CodeActionKind(x)); + + progress.report({ message: localize('codeaction', "Quick Fixes") }); + await this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, token); + } + + private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], token: CancellationToken): Promise { + for (const codeActionKind of codeActionsOnSave) { + const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, token); + try { + await this.applyCodeActions(actionsToRun.validActions); + } catch { + // Failure to apply a code action should not block other on save actions + } finally { + actionsToRun.dispose(); + } + } + } + + private async applyCodeActions(actionsToRun: readonly CodeAction[]) { + for (const action of actionsToRun) { + await this.instantiationService.invokeFunction(applyCodeAction, action); + } + } + + private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], token: CancellationToken) { + return getCodeActions(model, model.getFullModelRange(), { + type: CodeActionTriggerType.Auto, + filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, + }, token); + } +} + +export class SaveParticipantsContribution extends Disposable implements IWorkbenchContribution { + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITextFileService private readonly textFileService: ITextFileService + ) { + super(); + + this.registerSaveParticipants(); + } + + private registerSaveParticipants(): void { + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(TrimWhitespaceParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(CodeActionOnSaveParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(FormatOnSaveParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(FinalNewLineParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(TrimFinalNewLinesParticipant))); + this._register(this.textFileService.files.addSaveParticipant(this.instantiationService.createInstance(NotebookUpdateParticipant))); + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchContributionsExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(SaveParticipantsContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts similarity index 98% rename from src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts rename to src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index ab84e2bd88..be095eb104 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -5,9 +5,9 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { FinalNewLineParticipant, TrimFinalNewLinesParticipant } from 'vs/workbench/api/browser/mainThreadSaveParticipant'; +import { FinalNewLineParticipant, TrimFinalNewLinesParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index faf93aac53..c50f0b18fc 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -167,7 +167,7 @@ export class CommentsPanel extends ViewPane { const range = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].range : element.range; const activeEditor = this.editorService.activeEditor; - let currentActiveResource = activeEditor ? activeEditor.getResource() : undefined; + let currentActiveResource = activeEditor ? activeEditor.resource : undefined; if (currentActiveResource && currentActiveResource.toString() === element.resource.toString()) { const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 33a41d323f..fcc60a30eb 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -56,7 +56,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ group = editorGroupService.getGroup(editorContext.groupId); } else if (!resource) { if (editorService.activeEditor) { - resource = editorService.activeEditor.getResource(); + resource = editorService.activeEditor.resource; group = editorGroupService.activeGroup; } } @@ -184,7 +184,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { const activeGroup = activeControl.group; const activeEditor = activeControl.input; - const targetResource = activeEditor.getResource(); + const targetResource = activeEditor.resource; if (!targetResource) { return; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index d12643dc9b..79b96a8f96 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -26,8 +26,11 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { public static typeId = 'workbench.editors.webviewEditor'; private readonly _editorResource: URI; + get resource() { return this._editorResource; } + private _model?: ICustomEditorModel; + constructor( resource: URI, viewType: string, @@ -50,17 +53,13 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return CustomFileEditorInput.typeId; } - public getResource(): URI { - return this._editorResource; - } - public supportsSplitEditor() { return true; } @memoize getName(): string { - return basename(this.labelService.getUriLabel(this.getResource())); + return basename(this.labelService.getUriLabel(this.resource)); } @memoize @@ -71,7 +70,7 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { matches(other: IEditorInput): boolean { return this === other || (other instanceof CustomFileEditorInput && this.viewType === other.viewType - && isEqual(this.getResource(), other.getResource())); + && isEqual(this.resource, other.resource)); } @memoize @@ -81,12 +80,12 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @memoize private get mediumTitle(): string { - return this.labelService.getUriLabel(this.getResource(), { relative: true }); + return this.labelService.getUriLabel(this.resource, { relative: true }); } @memoize private get longTitle(): string { - return this.labelService.getUriLabel(this.getResource()); + return this.labelService.getUriLabel(this.resource); } public getTitle(verbosity?: Verbosity): string { @@ -157,7 +156,7 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { } public async resolve(): Promise { - this._model = await this.customEditorService.models.resolve(this.getResource(), this.viewType); + this._model = await this.customEditorService.models.resolve(this.resource, this.viewType); this._register(this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); if (this.isDirty()) { this._onDidChangeDirty.fire(); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index fa3d7219e6..1efaa6c366 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -25,7 +25,7 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { public serialize(input: CustomFileEditorInput): string | undefined { const data = { ...this.toJson(input), - editorResource: input.getResource().toJSON() + editorResource: input.resource.toJSON() }; try { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index f939513953..8018636927 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -125,7 +125,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._register(this._editorInfoStore.onChange(() => this.updateContexts())); this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); - this._register(fileService.onAfterOperation(e => { + this._register(fileService.onDidRunOperation(e => { if (e.isOperation(FileOperation.MOVE)) { this.handleMovedFileInOpenedFileEditors(e.resource, e.target.resource); } @@ -141,7 +141,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ if (!(activeInput instanceof CustomFileEditorInput)) { return undefined; } - const resource = activeInput.getResource(); + const resource = activeInput.resource; return { resource, viewType: activeInput.viewType }; } @@ -174,7 +174,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ let currentlyOpenedEditorType: undefined | string; for (const editor of group ? group.editors : []) { - if (editor.getResource() && isEqual(editor.getResource(), resource)) { + if (editor.resource && isEqual(editor.resource, resource)) { currentlyOpenedEditorType = editor instanceof CustomFileEditorInput ? editor.viewType : defaultEditorId; break; } @@ -246,7 +246,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ const targetGroup = group || this.editorGroupService.activeGroup; // Try to replace existing editors for resource - const existingEditors = targetGroup.editors.filter(editor => editor.getResource() && isEqual(editor.getResource(), resource)); + const existingEditors = targetGroup.editors.filter(editor => editor.resource && isEqual(editor.resource, resource)); if (existingEditors.length) { const existing = existingEditors[0]; if (!input.matches(existing)) { @@ -267,7 +267,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private updateContexts() { const activeControl = this.editorService.activeControl; - const resource = activeControl?.input.getResource(); + const resource = activeControl?.input.resource; if (!resource) { this._hasCustomEditor.reset(); this._focusedCustomEditorIsEditable.reset(); @@ -350,7 +350,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo // Unlike normal editor inputs, we do not want to share custom editor inputs // between multiple editors / groups. return { - override: this.customEditorService.openWith(editor.getResource(), editor.viewType, options, group) + override: this.customEditorService.openWith(editor.resource, editor.viewType, options, group) }; } } @@ -359,7 +359,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return this.onDiffEditorOpening(editor, options, group); } - const resource = editor.getResource(); + const resource = editor.resource; if (resource) { return this.onResourceEditorOpening(resource, editor, options, group); } @@ -382,7 +382,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo // If there is, we want to open that instead of creating a new editor. // This ensures that we preserve whatever type of editor was previously being used // when the user switches back to it. - const existingEditorForResource = group.editors.find(editor => isEqual(resource, editor.getResource())); + const existingEditorForResource = group.editors.find(editor => isEqual(resource, editor.resource)); if (existingEditorForResource) { return { override: this.editorService.openEditor(existingEditorForResource, { ...options, ignoreOverrides: true }, group) @@ -441,7 +441,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo if (subInput instanceof CustomFileEditorInput) { return undefined; } - const resource = subInput.getResource(); + const resource = subInput.resource; if (!resource) { return undefined; } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 208ce32f86..9a12015a80 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -117,7 +117,6 @@ export class DebugService implements IDebugService { this.model = new DebugModel(this.loadBreakpoints(), this.loadFunctionBreakpoints(), this.loadExceptionBreakpoints(), this.loadDataBreakpoints(), this.loadWatchExpressions(), this.textFileService); - this.toDispose.push(this.model); const setBreakpointsExistContext = () => this.breakpointsExist.set(!!(this.model.getBreakpoints().length || this.model.getDataBreakpoints().length || this.model.getFunctionBreakpoints().length)); this.breakpointsExist = CONTEXT_BREAKPOINTS_EXIST.bindTo(contextKeyService); setBreakpointsExistContext(); @@ -125,8 +124,8 @@ export class DebugService implements IDebugService { this.viewModel = new ViewModel(contextKeyService); this.taskRunner = this.instantiationService.createInstance(DebugTaskRunner); - this.toDispose.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); - this.lifecycleService.onShutdown(this.dispose, this); + this.toDispose.push(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); + this.toDispose.push(this.lifecycleService.onShutdown(this.dispose, this)); this.toDispose.push(this.extensionHostDebugService.onAttachSession(event => { const session = this.model.getSession(event.sessionId, true); @@ -522,7 +521,6 @@ export class DebugService implements IDebugService { await this.focusStackFrame(undefined, undefined, session); } } catch (err) { - session.shutdown(); if (this.viewModel.focusedSession === session) { await this.focusStackFrame(undefined); } @@ -567,7 +565,6 @@ export class DebugService implements IDebugService { this.notificationService.error(err); } } - session.shutdown(); this.endInitializingState(); this._onDidEndSession.fire(session); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index ecb2c09e70..cf35bfe141 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -33,6 +33,7 @@ import { variableSetEmitter } from 'vs/workbench/contrib/debug/browser/variables import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; export class DebugSession implements IDebugSession { @@ -74,7 +75,8 @@ export class DebugSession implements IDebugSession { @IProductService private readonly productService: IProductService, @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, @IOpenerService private readonly openerService: IOpenerService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, + @ILifecycleService lifecycleService: ILifecycleService ) { this.id = generateUuid(); this._options = options || {}; @@ -83,7 +85,15 @@ export class DebugSession implements IDebugSession { } else { this.repl = (this.parentSession as DebugSession).repl; } - this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire()); + + const toDispose: IDisposable[] = []; + toDispose.push(this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire())); + if (lifecycleService) { + toDispose.push(lifecycleService.onShutdown(() => { + this.shutdown(); + dispose(toDispose); + })); + } } getId(): string { @@ -213,6 +223,7 @@ export class DebugSession implements IDebugSession { } catch (err) { this.initialized = true; this._onDidChangeState.fire(); + this.shutdown(); throw err; } } @@ -227,8 +238,12 @@ export class DebugSession implements IDebugSession { // __sessionID only used for EH debugging (but we add it always for now...) config.__sessionId = this.getId(); - await this.raw.launchOrAttach(config); - + try { + await this.raw.launchOrAttach(config); + } catch (err) { + this.shutdown(); + throw err; + } } /** @@ -892,19 +907,22 @@ export class DebugSession implements IDebugSession { this.rawListeners.push(this.raw.onDidExitAdapter(event => { this.initialized = true; this.model.setBreakpointSessionData(this.getId(), this.capabilities, undefined); + this.shutdown(); this._onDidEndAdapter.fire(event); })); } - shutdown(): void { + // Disconnects and clears state. Session can be initialized again for a new connection. + private shutdown(): void { dispose(this.rawListeners); - if (this.raw) { - this.raw.disconnect(); - this.raw.dispose(); - } - this.raw = undefined; this.fetchThreadsScheduler = undefined; this.model.clearThreads(this.getId(), true); + if (this.raw) { + const raw = this.raw; + this.raw = undefined; + raw.disconnect(); + raw.dispose(); + } this._onDidChangeState.fire(); } diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 265e05ae19..e25e3da074 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -270,7 +270,7 @@ export class RawDebugSession implements IDisposable { if (this.capabilities.supportsTerminateRequest) { if (!this.terminated) { this.terminated = true; - return this.send('terminate', { restart }); + return this.send('terminate', { restart }, undefined, 500); } return this.disconnect(restart); } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index b5173fa979..bcd5174de7 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -200,9 +200,6 @@ export interface IDebugSession extends ITreeElement { readonly onDidLoadedSource: Event; readonly onDidCustomEvent: Event; - // Disconnects and clears state. Session can be initialized again for a new connection. - shutdown(): void; - // DAP request initialize(dbgr: IDebugger): Promise; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 478ac5e0bc..37b586ec6a 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { URI as uri } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; -import * as lifecycle from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -819,7 +818,6 @@ export class ThreadAndSessionIds implements ITreeElement { export class DebugModel implements IDebugModel { private sessions: IDebugSession[]; - private toDispose: lifecycle.IDisposable[]; private schedulers = new Map(); private breakpointsActivated = true; private readonly _onDidChangeBreakpoints = new Emitter(); @@ -835,7 +833,6 @@ export class DebugModel implements IDebugModel { private textFileService: ITextFileService ) { this.sessions = []; - this.toDispose = []; } getId(): string { @@ -1227,10 +1224,4 @@ export class DebugModel implements IDebugModel { }); this._onDidChangeCallStack.fire(undefined); } - - dispose(): void { - // Make sure to shutdown each session, such that no debugged process is left laying around - this.sessions.forEach(s => s.shutdown()); - this.toDispose = lifecycle.dispose(this.toDispose); - } } diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 43b7922921..16d06f535a 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + /** Declaration module describing the VS Code debug protocol. Auto-generated from json schema. Do not edit manually. */ @@ -98,7 +99,7 @@ declare module DebugProtocol { The sequence of events/requests is as follows: - adapters sends 'initialized' event (after the 'initialize' request has returned) - frontend sends zero or more 'setBreakpoints' requests - - frontend sends one 'setFunctionBreakpoints' request + - frontend sends one 'setFunctionBreakpoints' request (if capability 'supportsFunctionBreakpoints' is true) - frontend sends a 'setExceptionBreakpoints' request if one or more 'exceptionBreakpointFilters' have been defined (or if 'supportsConfigurationDoneRequest' is not defined or false) - frontend sends other future configuration requests - frontend sends one 'configurationDone' request to indicate the end of the configuration. @@ -201,6 +202,15 @@ declare module DebugProtocol { category?: string; /** The output to report. */ output: string; + /** Support for keeping an output log organized by grouping related messages. + 'start': Start a new group in expanded mode. Subsequent output events are members of the group and should be shown indented. + The 'output' attribute becomes the name of the group and is not indented. + 'startCollapsed': Start a new group in collapsed mode. Subsequent output events are members of the group and should be shown indented (as soon as the group is expanded). + The 'output' attribute becomes the name of the group and is not indented. + 'end': End the current group and decreases the indentation of subsequent output events. + A non empty 'output' attribute is shown as the unindented end of the group. + */ + group?: 'start' | 'startCollapsed' | 'end'; /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing 'variablesReference' to the 'variables' request. The value should be less than or equal to 2147483647 (2^31 - 1). */ variablesReference?: number; /** An optional source location where the output was produced. */ diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 8e8bf57e07..1d453fa81e 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -19,7 +19,7 @@ import { OverviewRulerLane } from 'vs/editor/common/model'; import { MarkdownString } from 'vs/base/common/htmlContent'; function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { - return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); } function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakpointData[]): void { diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index 2a4c97aa14..c94584335b 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -18,7 +18,7 @@ import { getContext, getContextForContributedActions } from 'vs/workbench/contri import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug/browser/debugService'; export function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { - return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame, secondStackFrame: StackFrame } { @@ -363,7 +363,7 @@ suite('Debug - CallStack', () => { get state(): State { return State.Stopped; } - }({ resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + }({ resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); const runningSession = createMockSession(model); model.addSession(runningSession); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 1946b3b251..90c4b69cd6 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -327,8 +327,6 @@ export class MockSession implements IDebugSession { goto(threadId: number, targetId: number): Promise { throw new Error('Method not implemented.'); } - - shutdown(): void { } } export class MockRawSession { diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts index 72101321d4..2ffcc96c34 100644 --- a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts @@ -30,7 +30,7 @@ suite.skip('Debug - ANSI Handling', () => { */ setup(() => { model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); - session = new DebugSession({ resolved: { name: 'test', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + session = new DebugSession({ resolved: { name: 'test', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); const instantiationService: TestInstantiationService = workbenchInstantiationService(); linkDetector = instantiationService.createInstance(LinkDetector); diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index 7680311273..d535b42482 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -21,6 +21,8 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; import { RunOnceWorker } from 'vs/base/common/async'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { equals } from 'vs/base/common/objects'; export const enum ExperimentState { Evaluating, @@ -49,7 +51,7 @@ export interface IExperimentActionPromptProperties { } export interface IExperimentActionPromptCommand { - text: string | { [key: string]: string }; + text: string | { [key: string]: string; }; externalLink?: string; curatedExtensionsKey?: string; curatedExtensionsList?: string[]; @@ -81,32 +83,78 @@ interface IExperimentStorageState { lastEditedDate?: string; } +/** + * Current version of the experiment schema in this VS Code build. This *must* + * be incremented when adding a condition, otherwise experiments might activate + * on older versions of VS Code where not intended. + */ +export const currentSchemaVersion = 1; + interface IRawExperiment { id: string; + schemaVersion: number; enabled?: boolean; condition?: { insidersOnly?: boolean; newUser?: boolean; displayLanguage?: string; + // Evaluates to true iff all the given user settings are deeply equal + userSetting?: { [key: string]: unknown; }; + // Start the experiment if the number of activation events have happened over the last week: + activationEvent?: { + event: string; + uniqueDays?: number; + minEvents: number; + }; installedExtensions?: { excludes?: string[]; includes?: string[]; - }, + }; fileEdits?: { filePathPattern?: string; workspaceIncludes?: string[]; workspaceExcludes?: string[]; minEditCount: number; - }, + }; experimentsPreviouslyRun?: { excludes?: string[]; includes?: string[]; - } + }; userProbability?: number; }; action?: IExperimentAction; } +interface IActivationEventRecord { + count: number[]; + mostRecentBucket: number; +} + +const experimentEventStorageKey = (event: string) => 'experimentEventRecord-' + event.replace(/[^0-9a-z]/ig, '-'); + +/** + * Updates the activation record to shift off days outside the window + * we're interested in. + */ +export const getCurrentActivationRecord = (previous?: IActivationEventRecord, dayWindow = 7): IActivationEventRecord => { + const oneDay = 1000 * 60 * 60 * 24; + const now = Date.now(); + if (!previous) { + return { count: new Array(dayWindow).fill(0), mostRecentBucket: now }; + } + + // get the number of days, up to dayWindow, that passed since the last bucket update + const shift = Math.min(dayWindow, Math.floor((now - previous.mostRecentBucket) / oneDay)); + if (!shift) { + return previous; + } + + return { + count: new Array(shift).fill(0).concat(previous.count.slice(0, -shift)), + mostRecentBucket: previous.mostRecentBucket + shift * oneDay, + }; +}; + export class ExperimentService extends Disposable implements IExperimentService { _serviceBrand: undefined; private _experiments: IExperiment[] = []; @@ -125,11 +173,13 @@ export class ExperimentService extends Disposable implements IExperimentService @IRequestService private readonly requestService: IRequestService, @IConfigurationService private readonly configurationService: IConfigurationService, @IProductService private readonly productService: IProductService, - @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService + @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService, + @IExtensionService private readonly extensionService: IExtensionService ) { super(); - this._loadExperimentsPromise = Promise.resolve(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() => this.loadExperiments()); + this._loadExperimentsPromise = Promise.resolve(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() => + this.loadExperiments()); } public getExperimentById(id: string): Promise { @@ -178,8 +228,8 @@ export class ExperimentService extends Disposable implements IExperimentService if (context.res.statusCode !== 200) { return null; } - const result: any = await asJson(context); - return result && Array.isArray(result['experiments']) ? result['experiments'] : []; + const result = await asJson<{ experiments?: IRawExperiment }>(context); + return result && Array.isArray(result.experiments) ? result.experiments : []; } catch (_e) { // Bad request or invalid JSON return null; @@ -207,6 +257,10 @@ export class ExperimentService extends Disposable implements IExperimentService return Promise.resolve(null); } + // Don't look at experiments with newer schema versions. We can't + // understand them, trying to process them might even cause errors. + rawExperiments = rawExperiments.filter(e => (e.schemaVersion || 0) <= currentSchemaVersion); + // Clear disbaled/deleted experiments from storage const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase()); @@ -223,6 +277,15 @@ export class ExperimentService extends Disposable implements IExperimentService this.storageService.remove('allExperiments', StorageScope.GLOBAL); } + const activationEvents = new Set(rawExperiments.map(exp => exp.condition?.activationEvent?.event).filter(evt => !!evt)); + if (activationEvents.size) { + this._register(this.extensionService.onWillActivateByEvent(evt => { + if (activationEvents.has(evt.event)) { + this.recordActivatedEvent(evt.event); + } + })); + } + const promises = rawExperiments.map(experiment => { const processedExperiment: IExperiment = { id: experiment.id, @@ -276,9 +339,9 @@ export class ExperimentService extends Disposable implements IExperimentService }); return Promise.all(promises).then(() => { type ExperimentsClassification = { - experiments: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + experiments: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; }; - this.telemetryService.publicLog2<{ experiments: IExperiment[] }, ExperimentsClassification>('experiments', { experiments: this._experiments }); + this.telemetryService.publicLog2<{ experiments: IExperiment[]; }, ExperimentsClassification>('experiments', { experiments: this._experiments }); }); }); } @@ -298,7 +361,7 @@ export class ExperimentService extends Disposable implements IExperimentService } private checkExperimentDependencies(experiment: IRawExperiment): boolean { - const experimentsPreviouslyRun = experiment.condition ? experiment.condition.experimentsPreviouslyRun : undefined; + const experimentsPreviouslyRun = experiment.condition?.experimentsPreviouslyRun; if (experimentsPreviouslyRun) { const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); let includeCheck = true; @@ -318,6 +381,33 @@ export class ExperimentService extends Disposable implements IExperimentService return true; } + private recordActivatedEvent(event: string) { + const key = experimentEventStorageKey(event); + const record = getCurrentActivationRecord(safeParse(this.storageService.get(key, StorageScope.GLOBAL), undefined)); + record.count[0]++; + this.storageService.store(key, JSON.stringify(record), StorageScope.GLOBAL); + } + + private checkActivationEventFrequency(experiment: IRawExperiment) { + const setting = experiment.condition?.activationEvent; + if (!setting) { + return true; + } + + const { count } = getCurrentActivationRecord(safeParse(this.storageService.get(experimentEventStorageKey(setting.event), StorageScope.GLOBAL), undefined)); + + let total = 0; + let uniqueDays = 0; + for (const entry of count) { + if (entry > 0) { + uniqueDays++; + total += entry; + } + } + + return total >= setting.minEvents && (!setting.uniqueDays || uniqueDays >= setting.uniqueDays); + } + private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): Promise { if (processedExperiment.state !== ExperimentState.Evaluating) { return Promise.resolve(processedExperiment.state); @@ -336,6 +426,16 @@ export class ExperimentService extends Disposable implements IExperimentService return Promise.resolve(ExperimentState.NoRun); } + for (const [key, value] of Object.entries(experiment.condition?.userSetting || {})) { + if (!equals(this.configurationService.getValue(key), value)) { + return Promise.resolve(ExperimentState.NoRun); + } + } + + if (!this.checkActivationEventFrequency(experiment)) { + return Promise.resolve(ExperimentState.Evaluating); + } + if (this.productService.quality === 'stable' && condition.insidersOnly === true) { return Promise.resolve(ExperimentState.NoRun); } diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index ca334a3b48..8d8ac3f22c 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ExperimentActionType, ExperimentState, IExperiment, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; +import * as sinon from 'sinon'; +import { ExperimentActionType, ExperimentState, IExperiment, ExperimentService, getCurrentActivationRecord, currentSchemaVersion } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService, TestExtensionService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IExtensionManagementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -27,6 +28,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IWillActivateEvent, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; interface ExperimentSettings { enabled?: boolean; @@ -34,7 +36,7 @@ interface ExperimentSettings { state?: ExperimentState; } -let experimentData: { [i: string]: any } = { +let experimentData: { [i: string]: any; } = { experiments: [] }; @@ -61,6 +63,7 @@ suite('Experiment Service', () => { let instantiationService: TestInstantiationService; let testConfigurationService: TestConfigurationService; let testObject: ExperimentService; + let activationEvent: Emitter; let installEvent: Emitter, didInstallEvent: Emitter, uninstallEvent: Emitter, @@ -72,7 +75,10 @@ suite('Experiment Service', () => { didInstallEvent = new Emitter(); uninstallEvent = new Emitter(); didUninstallEvent = new Emitter(); + activationEvent = new Emitter(); + instantiationService.stub(IExtensionService, TestExtensionService); + instantiationService.stub(IExtensionService, 'onWillActivateByEvent', activationEvent.event); instantiationService.stub(IExtensionManagementService, ExtensionManagementService); instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); @@ -161,6 +167,36 @@ suite('Experiment Service', () => { }); }); + test('filters out experiments with newer schema versions', async () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + // no version == 0 + }, + { + id: 'experiment2', + schemaVersion: currentSchemaVersion, + }, + { + id: 'experiment3', + schemaVersion: currentSchemaVersion + 1, + }, + ] + }; + + testObject = instantiationService.createInstance(TestExperimentService); + const actual = await Promise.all([ + testObject.getExperimentById('experiment1'), + testObject.getExperimentById('experiment2'), + testObject.getExperimentById('experiment3'), + ]); + + assert.equal(actual[0]?.id, 'experiment1'); + assert.equal(actual[1]?.id, 'experiment2'); + assert.equal(actual[2], undefined); + }); + test('Insiders only experiment shouldnt be enabled in stable', () => { experimentData = { experiments: [ @@ -270,6 +306,217 @@ suite('Experiment Service', () => { }); }); + test('Activation event experiment with not enough events should be evaluating', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + activationEvent: { + event: 'my:event', + minEvents: 5, + } + } + } + ] + }; + + instantiationService.stub(IStorageService, 'get', (a: string, b: StorageScope, c?: string) => { + return a === 'experimentEventRecord-my-event' + ? JSON.stringify({ count: [2], mostRecentBucket: Date.now() }) + : undefined; + }); + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.Evaluating); + }); + }); + + test('Activation event works with enough events', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + activationEvent: { + event: 'my:event', + minEvents: 5, + } + } + } + ] + }; + + instantiationService.stub(IStorageService, 'get', (a: string, b: StorageScope, c?: string) => { + return a === 'experimentEventRecord-my-event' + ? JSON.stringify({ count: [10], mostRecentBucket: Date.now() }) + : undefined; + }); + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.Run); + }); + }); + + test('Activation event does not work with old data', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + activationEvent: { + event: 'my:event', + minEvents: 5, + } + } + } + ] + }; + + instantiationService.stub(IStorageService, 'get', (a: string, b: StorageScope, c?: string) => { + return a === 'experimentEventRecord-my-event' + ? JSON.stringify({ count: [10], mostRecentBucket: Date.now() - (1000 * 60 * 60 * 24 * 10) }) + : undefined; + }); + + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.Evaluating); + }); + }); + + test('Parses activation records correctly', () => { + const timers = sinon.useFakeTimers(); // so Date.now() is stable + const oneDay = 1000 * 60 * 60 * 24; + teardown(() => timers.restore()); + + let rec = getCurrentActivationRecord(); + + // good default: + assert.deepEqual(rec, { + count: [0, 0, 0, 0, 0, 0, 0], + mostRecentBucket: Date.now(), + }); + + rec.count[0] = 1; + timers.tick(1); + rec = getCurrentActivationRecord(rec); + + // does not advance unnecessarily + assert.deepEqual(getCurrentActivationRecord(rec), { + count: [1, 0, 0, 0, 0, 0, 0], + mostRecentBucket: Date.now() - 1, + }); + + // advances time + timers.tick(oneDay * 3); + rec = getCurrentActivationRecord(rec); + assert.deepEqual(getCurrentActivationRecord(rec), { + count: [0, 0, 0, 1, 0, 0, 0], + mostRecentBucket: Date.now() - 1, + }); + + // rotates off time + timers.tick(oneDay * 4); + rec.count[0] = 2; + rec = getCurrentActivationRecord(rec); + assert.deepEqual(getCurrentActivationRecord(rec), { + count: [0, 0, 0, 0, 2, 0, 0], + mostRecentBucket: Date.now() - 1, + }); + }); + + test('Activation event updates', async () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + activationEvent: { + event: 'my:event', + minEvents: 5, + } + } + } + ] + }; + + instantiationService.stub(IStorageService, 'get', (a: string, b: StorageScope, c?: string) => { + return a === 'experimentEventRecord-my-event' + ? JSON.stringify({ count: [10, 0, 0, 0, 0, 0, 0], mostRecentBucket: Date.now() - (1000 * 60 * 60 * 24 * 2) }) + : undefined; + }); + + let didGetCall = false; + instantiationService.stub(IStorageService, 'store', (key: string, value: string, scope: StorageScope) => { + if (key.includes('experimentEventRecord')) { + didGetCall = true; + assert.equal(key, 'experimentEventRecord-my-event'); + assert.deepEqual(JSON.parse(value).count, [1, 0, 10, 0, 0, 0, 0]); + assert.equal(scope, StorageScope.GLOBAL); + } + }); + + testObject = instantiationService.createInstance(TestExperimentService); + await testObject.getExperimentById('experiment1'); // ensure loaded + activationEvent.fire({ event: 'not our event', activation: Promise.resolve() }); + activationEvent.fire({ event: 'my:event', activation: Promise.resolve() }); + assert(didGetCall); + }); + + test('Experiment not matching user setting should be disabled', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + userSetting: { neat: true } + } + } + ] + }; + + instantiationService.stub(IConfigurationService, 'getValue', + (key: string) => key === 'neat' ? false : undefined); + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.NoRun); + }); + }); + + test('Experiment matching user setting should be enabled', () => { + experimentData = { + experiments: [ + { + id: 'experiment1', + enabled: true, + condition: { + userSetting: { neat: true } + } + } + ] + }; + + instantiationService.stub(IConfigurationService, 'getValue', + (key: string) => key === 'neat' ? true : undefined); + testObject = instantiationService.createInstance(TestExperimentService); + return testObject.getExperimentById('experiment1').then(result => { + assert.equal(result.enabled, true); + assert.equal(result.state, ExperimentState.Run); + }); + }); + test('Experiment with no matching display language should be disabled', () => { experimentData = { experiments: [ diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 24e309a997..e0564ea843 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -14,7 +14,7 @@ import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } fro import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, @@ -47,6 +47,7 @@ import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewConta import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -298,6 +299,30 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: 'workbench.extensions.search', + description: { + description: localize('workbench.extensions.search.description', "Search for a specific extension"), + args: [ + { + name: localize('workbench.extensions.search.arg.name', "Query to use in search"), + schema: { 'type': 'string' } + } + ] + }, + handler: async (accessor, query: string = '') => { + const viewletService = accessor.get(IViewletService); + const viewlet = await viewletService.openViewlet(VIEWLET_ID, true); + + if (!viewlet) { + return; + } + + (viewlet.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); + viewlet.focus(); + } +}); + // File menu registration MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { diff --git a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts index 9dedc39b68..dbd1861fa1 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts @@ -13,6 +13,11 @@ export class ExtensionsInput extends EditorInput { static readonly ID = 'workbench.extensions.input2'; get extension(): IExtension { return this._extension; } + readonly resource = URI.from({ + scheme: 'extension', + path: this.extension.identifier.id + }); + constructor( private _extension: IExtension, ) { @@ -49,11 +54,4 @@ export class ExtensionsInput extends EditorInput { supportsSplitEditor(): boolean { return false; } - - getResource(): URI { - return URI.from({ - scheme: 'extension', - path: this.extension.identifier.id - }); - } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts index 505183761c..7c0a2f1649 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts @@ -11,6 +11,11 @@ export class RuntimeExtensionsInput extends EditorInput { static readonly ID = 'workbench.runtimeExtensions.input'; + readonly resource = URI.from({ + scheme: 'runtime-extensions', + path: 'default' + }); + constructor() { super(); } @@ -37,11 +42,4 @@ export class RuntimeExtensionsInput extends EditorInput { supportsSplitEditor(): boolean { return false; } - - getResource(): URI { - return URI.from({ - scheme: 'runtime-extensions', - path: 'default' - }); - } } diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index a856e509d1..542ed3e58c 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -56,10 +56,10 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut private registerListeners(): void { // Update editors from operation changes - this._register(this.fileService.onAfterOperation(e => this.onFileOperation(e))); + this._register(this.fileService.onDidRunOperation(e => this.onFileOperation(e))); // Update editors from disk changes - this._register(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); // Ensure dirty text file and untitled models are always opened as editors this._register(this.textFileService.files.onDidChangeDirty(m => this.ensureDirtyFilesAreOpenedWorker.work(m.resource))); @@ -104,7 +104,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut if (editor instanceof FileEditorInput || editor instanceof QueryEditorInput) { // {{SQL CARBON EDIT}} #TODO we can remove this edit by just implementing handlemove // Update Editor if file (or any parent of the input) got renamed or moved - const resource = editor.getResource(); + const resource = editor.resource; if (isEqualOrParent(resource, oldResource)) { let reopenFileResource: URI; if (oldResource.toString() === resource.toString()) { @@ -163,7 +163,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut for (const editor of editors) { if (editor?.input && editor.group === group) { - const editorResource = editor.input.getResource(); + const editorResource = editor.input.resource; if (editorResource && resource.toString() === editorResource.toString()) { const control = editor.getControl(); if (isCodeEditor(control)) { @@ -190,7 +190,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut } } - private onFileChanges(e: FileChangesEvent): void { + private onDidFilesChange(e: FileChangesEvent): void { if (e.gotDeleted()) { this.handleDeletes(e, true); } @@ -199,7 +199,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut private handleDeletes(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void { const nonDirtyFileEditors = this.getNonDirtyFileEditors(); nonDirtyFileEditors.forEach(async editor => { - const resource = editor.getResource(); + const resource = editor.resource; // Handle deletes in opened editors depending on: // - the user has not disabled the setting closeOnFileDelete diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index e5891efb9d..00fe780618 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -61,7 +61,7 @@ export class TextFileEditor extends BaseTextEditor { this.updateRestoreViewStateConfiguration(); // Clear view state for deleted files - this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e))); + this._register(this.fileService.onDidFilesChange(e => this.onFilesChanged(e))); } private onFilesChanged(e: FileChangesEvent): void { @@ -142,7 +142,7 @@ export class TextFileEditor extends BaseTextEditor { textEditor.setModel(textFileModel.textEditorModel); // Always restore View State if any associated - const editorViewState = this.loadTextEditorViewState(input.getResource()); + const editorViewState = this.loadTextEditorViewState(input.resource); if (editorViewState) { textEditor.restoreViewState(editorViewState); } @@ -180,14 +180,14 @@ export class TextFileEditor extends BaseTextEditor { } // Offer to create a file from the error if we have a file not found and the name is valid - if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.getResource()))) { + if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.resource))) { throw createErrorWithActions(toErrorMessage(error), { actions: [ new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, async () => { - await this.textFileService.create(input.getResource()); + await this.textFileService.create(input.resource); return this.editorService.openEditor({ - resource: input.getResource(), + resource: input.resource, options: { pinned: true // new file gets pinned by default } @@ -227,10 +227,10 @@ export class TextFileEditor extends BaseTextEditor { await this.group.closeEditor(this.input); // Best we can do is to reveal the folder in the explorer - if (this.contextService.isInsideWorkspace(input.getResource())) { + if (this.contextService.isInsideWorkspace(input.resource)) { await this.viewletService.openViewlet(VIEWLET_ID); - this.explorerService.select(input.getResource(), true); + this.explorerService.select(input.resource, true); } } @@ -278,12 +278,12 @@ export class TextFileEditor extends BaseTextEditor { // If the user configured to not restore view state, we clear the view // state unless the editor is still opened in the group. if (!this.restoreViewState && (!this.group || !this.group.isOpened(input))) { - this.clearTextEditorViewState([input.getResource()], this.group); + this.clearTextEditorViewState([input.resource], this.group); } // Otherwise we save the view state to restore it later else if (!input.isDisposed()) { - this.saveTextEditorViewState(input.getResource()); + this.saveTextEditorViewState(input.resource); } } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 0767fbfcee..615ecdb9d3 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -64,7 +64,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider)); // Set as save error handler to service for text files - this.textFileService.saveErrorHandler = this; + this.textFileService.files.saveErrorHandler = this; this.registerListeners(); } @@ -81,10 +81,10 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa const activeInput = this.editorService.activeEditor; if (activeInput instanceof DiffEditorInput && activeInput.originalInput instanceof ResourceEditorInput && activeInput.modifiedInput instanceof FileEditorInput) { - const resource = activeInput.originalInput.getResource(); + const resource = activeInput.originalInput.resource; if (resource?.scheme === CONFLICT_RESOLUTION_SCHEME) { isActiveEditorSaveConflictResolution = true; - activeConflictResolutionResource = activeInput.modifiedInput.getResource(); + activeConflictResolutionResource = activeInput.modifiedInput.resource; } } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index d8986dd228..07cf1ed4f0 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -46,6 +46,7 @@ import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { sequence } from 'vs/base/common/async'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -165,7 +166,7 @@ export class GlobalNewUntitledFileAction extends Action { } } -async function deleteFiles(workingCopyService: IWorkingCopyService, textFileService: ITextFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { +async function deleteFiles(workingCopyService: IWorkingCopyService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { let primaryButton: string; if (useTrash) { primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); @@ -265,7 +266,7 @@ async function deleteFiles(workingCopyService: IWorkingCopyService, textFileServ // Call function try { - await Promise.all(distinctElements.map(e => textFileService.delete(e.resource, { useTrash: useTrash, recursive: true }))); + await Promise.all(distinctElements.map(e => workingCopyFileService.delete(e.resource, { useTrash: useTrash, recursive: true }))); } catch (error) { // Handle error to delete file(s) from a modal confirmation dialog @@ -295,7 +296,7 @@ async function deleteFiles(workingCopyService: IWorkingCopyService, textFileServ skipConfirm = true; - return deleteFiles(workingCopyService, textFileService, dialogService, configurationService, elements, useTrash, skipConfirm); + return deleteFiles(workingCopyService, workingCopyFileService, dialogService, configurationService, elements, useTrash, skipConfirm); } } } @@ -493,7 +494,7 @@ export class GlobalCompareResourcesAction extends Action { async run(): Promise { const activeInput = this.editorService.activeEditor; - const activeResource = activeInput ? activeInput.getResource() : undefined; + const activeResource = activeInput ? activeInput.resource : undefined; if (activeResource) { // Compare with next editor that opens @@ -503,7 +504,7 @@ export class GlobalCompareResourcesAction extends Action { toDispose.dispose(); // Open editor as diff - const resource = editor.getResource(); + const resource = editor.resource; if (resource) { return { override: this.editorService.openEditor({ @@ -955,7 +956,7 @@ CommandsRegistry.registerCommand({ export const renameHandler = (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const textFileService = accessor.get(ITextFileService); + const workingCopyFileService = accessor.get(IWorkingCopyFileService); const notificationService = accessor.get(INotificationService); const stats = explorerService.getContext(false); @@ -972,7 +973,7 @@ export const renameHandler = (accessor: ServicesAccessor) => { const targetResource = resources.joinPath(parentResource, value); if (stat.resource.toString() !== targetResource.toString()) { try { - await textFileService.move(stat.resource, targetResource); + await workingCopyFileService.move(stat.resource, targetResource); refreshIfSeparator(value, explorerService); } catch (e) { notificationService.error(e); @@ -988,7 +989,7 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IWorkingCopyService), accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); + await deleteFiles(accessor.get(IWorkingCopyService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); } }; @@ -997,7 +998,7 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IWorkingCopyService), accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); + await deleteFiles(accessor.get(IWorkingCopyService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); } }; @@ -1023,7 +1024,7 @@ export const cutFileHandler = (accessor: ServicesAccessor) => { export const DOWNLOAD_COMMAND_ID = 'explorer.download'; const downloadFileHandler = (accessor: ServicesAccessor) => { const fileService = accessor.get(IFileService); - const textFileService = accessor.get(ITextFileService); + const workingCopyFileService = accessor.get(IWorkingCopyFileService); const fileDialogService = accessor.get(IFileDialogService); const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true); @@ -1058,7 +1059,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { defaultUri }); if (destination) { - await textFileService.copy(s.resource, destination, true); + await workingCopyFileService.copy(s.resource, destination, true); } else { // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 canceled = true; @@ -1076,7 +1077,7 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { const clipboardService = accessor.get(IClipboardService); const explorerService = accessor.get(IExplorerService); const fileService = accessor.get(IFileService); - const textFileService = accessor.get(ITextFileService); + const workingCopyFileService = accessor.get(IWorkingCopyFileService); const notificationService = accessor.get(INotificationService); const editorService = accessor.get(IEditorService); const configurationService = accessor.get(IConfigurationService); @@ -1108,9 +1109,9 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { // Move/Copy File if (pasteShouldMove) { - return await textFileService.move(fileToPaste, targetFile); + return await workingCopyFileService.move(fileToPaste, targetFile); } else { - return await textFileService.copy(fileToPaste, targetFile); + return await workingCopyFileService.copy(fileToPaste, targetFile); } } catch (e) { onError(notificationService, new Error(nls.localize('fileDeleted', "The file to paste has been deleted or moved since you copied it. {0}", getErrorMessage(e)))); diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index c018e480cf..95b0897d71 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -280,7 +280,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: async (accessor) => { const editorService = accessor.get(IEditorService); const activeInput = editorService.activeEditor; - const resource = activeInput ? activeInput.getResource() : null; + const resource = activeInput ? activeInput.resource : null; const resources = resource ? [resource] : []; await resourcesToClipboard(resources, false, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService)); } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 2e53285b03..9c37802a0d 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -128,7 +128,7 @@ class FileEditorInputFactory implements IEditorInputFactory { serialize(editorInput: EditorInput): string { const fileEditorInput = editorInput; - const resource = fileEditorInput.getResource(); + const resource = fileEditorInput.resource; const fileInput: ISerializedFileInput = { resourceJSON: resource.toJSON(), encoding: fileEditorInput.getEncoding(), diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 2ed39d7ff8..b3f30602de 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -48,7 +48,6 @@ flex: 0; /* do not steal space when label is hidden because we are in edit mode */ } - .explorer-viewlet .pane-header .count { min-width: fit-content; min-width: -moz-fit-content; diff --git a/src/vs/workbench/contrib/files/browser/media/fileactions.css b/src/vs/workbench/contrib/files/browser/media/fileactions.css index 38b225f329..4fcfead9b6 100644 --- a/src/vs/workbench/contrib/files/browser/media/fileactions.css +++ b/src/vs/workbench/contrib/files/browser/media/fileactions.css @@ -3,6 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .codicon-close::before { +.open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .codicon-close::before { content: "\ea71"; } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 061e53509a..99a0fa1cdd 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -37,7 +37,7 @@ import { Schemas } from 'vs/base/common/network'; import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { IDialogService, IConfirmation, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { URI } from 'vs/base/common/uri'; @@ -641,7 +641,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { @IFileService private fileService: IFileService, @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private instantiationService: IInstantiationService, - @ITextFileService private textFileService: ITextFileService, + @IWorkingCopyFileService private workingCopyFileService: IWorkingCopyFileService, @IHostService private hostService: IHostService, @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, @IWorkingCopyService private workingCopyService: IWorkingCopyService @@ -953,7 +953,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } const copyTarget = joinPath(target.resource, basename(sourceFile)); - const stat = await this.textFileService.copy(sourceFile, copyTarget, true); + const stat = await this.workingCopyFileService.copy(sourceFile, copyTarget, true); // if we only add one file, just open it directly if (resources.length === 1 && !stat.isDirectory) { this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); @@ -1040,7 +1040,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Reuse duplicate action if user copies if (isCopy) { const incrementalNaming = this.configurationService.getValue().explorer.incrementalNaming; - const stat = await this.textFileService.copy(source.resource, findValidPasteFileTarget(this.explorerService, target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)); + const stat = await this.workingCopyFileService.copy(source.resource, findValidPasteFileTarget(this.explorerService, target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)); if (!stat.isDirectory) { await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); } @@ -1056,7 +1056,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } try { - await this.textFileService.move(source.resource, targetResource); + await this.workingCopyFileService.move(source.resource, targetResource); } catch (error) { // Conflict if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { @@ -1065,7 +1065,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const { confirmed } = await this.dialogService.confirm(confirm); if (confirmed) { try { - await this.textFileService.move(source.resource, targetResource, true /* overwrite */); + await this.workingCopyFileService.move(source.resource, targetResource, true /* overwrite */); } catch (error) { this.notificationService.error(error); } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 5ea64f16c0..cba0aa7b2b 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -380,7 +380,7 @@ export class OpenEditorsView extends ViewPane { const element = e.element; const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true, arg: element instanceof OpenEditor ? element.editor.getResource() : {} }, actions, this.contextMenuService); + const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true, arg: element instanceof OpenEditor ? element.editor.resource : {} }, actions, this.contextMenuService); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index f1b7807a88..e0bf4a2961 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -10,13 +10,15 @@ import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel' import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { ITextFileService, ModelState, LoadReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IReference, dispose } from 'vs/base/common/lifecycle'; +import { IReference, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FILE_EDITOR_INPUT_ID, TEXT_FILE_EDITOR_ID, BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { isEqual } from 'vs/base/common/resources'; +import { Event } from 'vs/base/common/event'; const enum ForceOpenAs { None, @@ -34,8 +36,11 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi private forceOpenAs: ForceOpenAs = ForceOpenAs.None; + private model: ITextFileEditorModel | undefined = undefined; private cachedTextFileModelReference: IReference | undefined = undefined; + private modelListeners: DisposableStore = this._register(new DisposableStore()); + constructor( resource: URI, preferredEncoding: string | undefined, @@ -60,10 +65,48 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi } } + protected registerListeners(): void { + super.registerListeners(); + + // Attach to model that matches our resource once created + this._register(this.textFileService.files.onDidCreate(model => this.onDidCreateTextFileModel(model))); + } + + private onDidCreateTextFileModel(model: ITextFileEditorModel): void { + + // Once the text file model is created, we keep it inside + // the input to be able to implement some methods properly + if (isEqual(model.resource, this.resource)) { + this.model = model; + + this.registerModelListeners(model); + } + } + + private registerModelListeners(model: ITextFileEditorModel): void { + + // Clear any old + this.modelListeners.clear(); + + // re-emit some events from the model + this.modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this.modelListeners.add(model.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); + + // important: treat save errors as potential dirty change because + // a file that is in save conflict or error will report dirty even + // if auto save is turned on. + this.modelListeners.add(model.onDidSaveError(() => this._onDidChangeDirty.fire())); + + // remove model association once it gets disposed + Event.once(model.onDispose)(() => { + this.modelListeners.clear(); + this.model = undefined; + }); + } + getEncoding(): string | undefined { - const textModel = this.textFileService.files.get(this.resource); - if (textModel) { - return textModel.getEncoding(); + if (this.model) { + return this.model.getEncoding(); } return this.preferredEncoding; @@ -76,15 +119,14 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi setEncoding(encoding: string, mode: EncodingMode): void { this.setPreferredEncoding(encoding); - const textModel = this.textFileService.files.get(this.resource); - if (textModel) { - textModel.setEncoding(encoding, mode); - } + this.model?.setEncoding(encoding, mode); } setPreferredEncoding(encoding: string): void { this.preferredEncoding = encoding; - this.setForceOpenAsText(); // encoding is a good hint to open the file as text + + // encoding is a good hint to open the file as text + this.setForceOpenAsText(); } getPreferredMode(): string | undefined { @@ -94,15 +136,14 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi setMode(mode: string): void { this.setPreferredMode(mode); - const textModel = this.textFileService.files.get(this.resource); - if (textModel) { - textModel.setMode(mode); - } + this.model?.setMode(mode); } setPreferredMode(mode: string): void { this.preferredMode = mode; - this.setForceOpenAsText(); // mode is a good hint to open the file as text + + // mode is a good hint to open the file as text + this.setForceOpenAsText(); } setForceOpenAsText(): void { @@ -126,13 +167,18 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi } private decorateLabel(label: string): string { - const model = this.textFileService.files.get(this.resource); + const orphaned = this.model?.hasState(ModelState.ORPHAN); + const readonly = this.isReadonly(); - if (model?.hasState(ModelState.ORPHAN)) { + if (orphaned && readonly) { + return localize('orphanedReadonlyFile', "{0} (deleted, read-only)", label); + } + + if (orphaned) { return localize('orphanedFile', "{0} (deleted)", label); } - if (this.isReadonly()) { + if (readonly) { return localize('readonlyFile', "{0} (read-only)", label); } @@ -140,21 +186,19 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi } isDirty(): boolean { - const model = this.textFileService.files.get(this.resource); - if (!model) { - return false; + return !!(this.model?.isDirty()); + } + + isReadonly(): boolean { + if (this.model) { + return this.model.isReadonly(); } - return model.isDirty(); + return super.isReadonly(); } isSaving(): boolean { - const model = this.textFileService.files.get(this.resource); - if (!model) { - return false; - } - - if (model.hasState(ModelState.SAVED) || model.hasState(ModelState.CONFLICT) || model.hasState(ModelState.ERROR)) { + if (this.model?.hasState(ModelState.SAVED) || this.model?.hasState(ModelState.CONFLICT) || this.model?.hasState(ModelState.ERROR)) { return false; // require the model to be dirty and not in conflict or error state } @@ -198,7 +242,7 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi // resolve() ensures we are not creating model references for these kind of resources. // In addition we have a bit of payload to take into account (encoding, reload) that the text resolver does not handle yet. if (!this.cachedTextFileModelReference) { - this.cachedTextFileModelReference = await this.createTextModelReference(); + this.cachedTextFileModelReference = await this.textModelResolverService.createModelReference(this.resource) as IReference; } return this.cachedTextFileModelReference.object; @@ -217,38 +261,12 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi } } - private async createTextModelReference(): Promise> { - const reference = await this.textModelResolverService.createModelReference(this.resource) as IReference; - - // Fire an initial dirty change if the model is already dirty - const model = reference.object; - if (model.isDirty()) { - this._onDidChangeDirty.fire(); - } - - this.registerModelListeners(model); - - return reference; - } - - private registerModelListeners(model: ITextFileEditorModel): void { - - // re-emit some events from the model - this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._register(model.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); - - // important: treat save errors as potential dirty change because - // a file that is in save conflict or error will report dirty even - // if auto save is turned on. - this._register(model.onDidSaveError(() => this._onDidChangeDirty.fire())); - } - private async doResolveAsBinary(): Promise { return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load(); } isResolved(): boolean { - return !!this.textFileService.files.get(this.resource); + return !!this.model; } matches(otherInput: unknown): boolean { @@ -265,6 +283,9 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi dispose(): void { + // Model + this.model = undefined; + // Model reference dispose(this.cachedTextFileModelReference); this.cachedTextFileModelReference = undefined; diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index cbff88967e..c3aa6c0f8b 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -56,8 +56,8 @@ export class ExplorerService implements IExplorerService { this.model = new ExplorerModel(this.contextService, this.fileService); this.disposables.add(this.model); - this.disposables.add(this.fileService.onAfterOperation(e => this.onFileOperation(e))); - this.disposables.add(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this.disposables.add(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e))); + this.disposables.add(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); this.disposables.add(Event.any<{ scheme: string }>(this.fileService.onDidChangeFileSystemProviderRegistrations, this.fileService.onDidChangeFileSystemProviderCapabilities)(e => { let affected = false; @@ -203,7 +203,7 @@ export class ExplorerService implements IExplorerService { refresh(): void { this.model.roots.forEach(r => r.forgetChildren()); this._onDidChangeItem.fire({ recursive: true }); - const resource = this.editorService.activeEditor ? this.editorService.activeEditor.getResource() : undefined; + const resource = this.editorService.activeEditor ? this.editorService.activeEditor.resource : undefined; const autoReveal = this.configurationService.getValue().explorer.autoReveal; if (resource && autoReveal) { @@ -214,7 +214,7 @@ export class ExplorerService implements IExplorerService { // File events - private onFileOperation(e: FileOperationEvent): void { + private onDidRunOperation(e: FileOperationEvent): void { // Add if (e.isOperation(FileOperation.CREATE) || e.isOperation(FileOperation.COPY)) { const addedElement = e.target; @@ -294,7 +294,7 @@ export class ExplorerService implements IExplorerService { } } - private onFileChanges(e: FileChangesEvent): void { + private onDidFilesChange(e: FileChangesEvent): void { // Check if an explorer refresh is necessary (delayed to give internal events a chance to react first) // Note: there is no guarantee when the internal events are fired vs real ones. Code has to deal with the fact that one might // be fired first over the other or not at all. diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 46f43df02a..dd558a613f 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -171,7 +171,7 @@ export class TextFileContentProvider extends Disposable implements ITextModelCon // Make sure to keep contents up to date when it changes if (!this.fileWatcherDisposable.value) { - this.fileWatcherDisposable.value = this.fileService.onFileChanges(changes => { + this.fileWatcherDisposable.value = this.fileService.onDidFilesChange(changes => { if (changes.contains(savedFileResource, FileChangeType.UPDATED)) { this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes } diff --git a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts index 665c3523de..74d40a3c0b 100644 --- a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts @@ -48,7 +48,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); const activeInput = editorService.activeEditor; - const resource = activeInput ? activeInput.getResource() : null; + const resource = activeInput ? activeInput.resource : null; const resources = resource ? [resource] : []; revealResourcesInOS(resources, accessor.get(IElectronService), accessor.get(INotificationService), accessor.get(IWorkspaceContextService)); } diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 7c764816fa..84772f989f 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -52,8 +52,8 @@ suite('Files - FileEditorInput', () => { assert.strictEqual('file.js', input.getName()); - assert.strictEqual(toResource.call(this, '/foo/bar/file.js').fsPath, input.getResource().fsPath); - assert(input.getResource() instanceof URI); + assert.strictEqual(toResource.call(this, '/foo/bar/file.js').fsPath, input.resource.fsPath); + assert(input.resource instanceof URI); input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar.html'), undefined, undefined); @@ -187,4 +187,24 @@ suite('Files - FileEditorInput', () => { assert.ok(resolved); resolved.dispose(); }); + + test('attaches to model when created and reports dirty', async function () { + const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined); + + let listenerCount = 0; + const listener = input.onDidChangeDirty(() => { + listenerCount++; + }); + + // instead of going through file input resolve method + // we resolve the model directly through the service + const model = await accessor.textFileService.files.resolve(input.resource); + model.textEditorModel?.setValue('hello world'); + + assert.equal(listenerCount, 1); + assert.ok(input.isDirty()); + + input.dispose(); + listener.dispose(); + }); }); diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index d2b83e2c89..e0778014ca 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -80,7 +80,7 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } const watcher = this.fileService.watch(dirname(file)); - const disposable = this.fileService.onFileChanges(e => { + const disposable = this.fileService.onDidFilesChange(e => { if (e.contains(file, FileChangeType.ADDED) || e.contains(file, FileChangeType.UPDATED)) { watcher.dispose(); disposable.dispose(); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 658fb09fd6..9814a2a734 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -517,7 +517,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private setCurrentActiveEditor(): void { const activeEditor = this.editorService.activeEditor; - this.currentActiveResource = activeEditor ? withUndefinedAsNull(activeEditor.getResource()) : null; + this.currentActiveResource = activeEditor ? withUndefinedAsNull(activeEditor.resource) : null; } private onSelected(): void { diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index a19ffb0fd6..57d0794cbb 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -28,6 +28,8 @@ export class LogViewerInput extends ResourceEditorInput { static readonly ID = 'workbench.editorinputs.output'; + readonly resource = this.outputChannelDescriptor.file; + constructor( private readonly outputChannelDescriptor: IFileOutputChannelDescriptor, @ITextModelService textModelResolverService: ITextModelService, @@ -56,10 +58,6 @@ export class LogViewerInput extends ResourceEditorInput { getTypeId(): string { return LogViewerInput.ID; } - - getResource(): URI { - return this.outputChannelDescriptor.file; - } } export class LogViewer extends AbstractTextResourceEditor { diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index edc687ef7a..f397b6474f 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -196,7 +196,7 @@ class DefaultPreferencesEditorInputFactory implements IEditorInputFactory { serialize(editorInput: EditorInput): string { const input = editorInput; - const serialized: ISerializedDefaultPreferencesEditorInput = { resource: input.getResource().toString() }; + const serialized: ISerializedDefaultPreferencesEditorInput = { resource: input.resource.toString() }; return JSON.stringify(serialized); } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index d687d8af69..6680f7fdf4 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -840,10 +840,10 @@ class SideBySidePreferencesWidget extends Widget { setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<{ defaultPreferencesRenderer?: IPreferencesRenderer, editablePreferencesRenderer?: IPreferencesRenderer; }> { this.getOrCreateEditablePreferencesEditor(editablePreferencesEditorInput); - this.settingsTargetsWidget.settingsTarget = this.getSettingsTarget(editablePreferencesEditorInput.getResource()!); + this.settingsTargetsWidget.settingsTarget = this.getSettingsTarget(editablePreferencesEditorInput.resource!); return Promise.all([ - this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.getResource()!, options, token), - this.updateInput(this.editablePreferencesEditor!, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.getResource()!, options, token) + this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.resource!, options, token), + this.updateInput(this.editablePreferencesEditor!, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.resource!, options, token) ]) .then(([defaultPreferencesRenderer, editablePreferencesRenderer]) => { if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index 79e66623a4..8280119dc8 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -63,7 +63,7 @@ export class PreferencesContribution implements IWorkbenchContribution { } private onEditorOpening(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined { - const resource = editor.getResource(); + const resource = editor.resource; if ( !resource || !endsWith(resource.path, 'settings.json') || // resource must end in settings.json diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 4de5e4b9e7..01efac2831 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -59,7 +59,7 @@ export class SCMStatusController implements IWorkbenchContribution { return false; } - const resource = this.editorService.activeEditor.getResource(); + const resource = this.editorService.activeEditor.resource; if (!resource) { return false; diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 995d0d11a7..9ba3c58e37 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -8,17 +8,6 @@ flex: 1; } -.scm-viewlet .empty-message { - box-sizing: border-box; - height: 100%; - padding: 10px 22px 0 22px; -} - -.scm-viewlet:not(.empty) .empty-message, -.scm-viewlet.empty .monaco-pane-view { - display: none; -} - .scm-viewlet .scm-status { height: 100%; position: relative; diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index 4359467732..b9489a5bb4 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -615,7 +615,7 @@ export class RepositoryPane extends ViewPane { protected contextKeyService: IContextKeyService; private commitTemplate = ''; - isEmpty() { return true; } + shouldShowWelcome() { return true; } constructor( readonly repository: ISCMRepository, diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index f83b53f2c1..d82db1bb98 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -6,12 +6,11 @@ import 'vs/css!./media/scmViewlet'; import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { append, $, toggleClass, addClasses } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { VIEWLET_ID, ISCMService, ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -25,13 +24,16 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IViewsRegistry, Extensions, IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewsRegistry, Extensions, IViewDescriptorService, IViewDescriptor } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { RepositoryPane, RepositoryViewDescriptor } from 'vs/workbench/contrib/scm/browser/repositoryPane'; import { MainPaneDescriptor, MainPane, IViewModel } from 'vs/workbench/contrib/scm/browser/mainPane'; -import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer, IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import type { IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { debounce } from 'vs/base/common/decorators'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { addClass } from 'vs/base/browser/dom'; export interface ISpliceEvent { index: number; @@ -39,12 +41,45 @@ export interface ISpliceEvent { elements: T[]; } +export class EmptyPane extends ViewPane { + + static readonly ID = 'workbench.scm'; + static readonly TITLE = localize('scm providers', "Source Control Providers"); + + constructor( + options: IViewPaneOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IInstantiationService instantiationService: IInstantiationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + } + + shouldShowWelcome(): boolean { + return true; + } +} + +export class EmptyPaneDescriptor implements IViewDescriptor { + readonly id = EmptyPane.ID; + readonly name = EmptyPane.TITLE; + readonly ctorDescriptor = new SyncDescriptor(EmptyPane); + readonly canToggleVisibility = true; + readonly hideByDefault = false; + readonly order = -1000; + readonly workspace = true; + readonly when = ContextKeyExpr.equals('scm.providerCount', 0); +} + export class SCMViewPaneContainer extends ViewPaneContainer implements IViewModel { private static readonly STATE_KEY = 'workbench.scm.views.state'; - private el!: HTMLElement; - private message: HTMLElement; private menus: SCMMenus; private _repositories: ISCMRepository[] = []; @@ -94,9 +129,14 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode this.menus = instantiationService.createInstance(SCMMenus, undefined); this._register(this.menus.onDidChangeTitle(this.updateTitleArea, this)); - this.message = $('.empty-message', { tabIndex: 0 }, localize('no open repo', "No source control providers registered.")); - const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + + viewsRegistry.registerViewWelcomeContent(EmptyPane.ID, { + content: localize('no open repo', "No source control providers registered."), + when: 'default' + }); + + viewsRegistry.registerViews([new EmptyPaneDescriptor()], this.viewContainer); viewsRegistry.registerViews([new MainPaneDescriptor(this)], this.viewContainer); this._register(configurationService.onDidChangeConfiguration(e => { @@ -113,11 +153,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode create(parent: HTMLElement): void { super.create(parent); - - this.el = parent; - addClasses(parent, 'scm-viewlet', 'empty'); - append(parent, this.message); - + addClass(parent, 'scm-viewlet'); this._register(this.scmService.onDidAddRepository(this.onDidAddRepository, this)); this._register(this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this)); this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); @@ -156,9 +192,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode } private onDidChangeRepositories(): void { - const repositoryCount = this.repositories.length; - toggleClass(this.el, 'empty', repositoryCount === 0); - this.repositoryCountKey.set(repositoryCount); + this.repositoryCountKey.set(this.repositories.length); } private onDidShowView(e: IAddedViewDescriptorRef[]): void { @@ -187,23 +221,19 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode } focus(): void { - if (this.repositoryCountKey.get()! === 0) { - this.message.focus(); - } else { - const repository = this.visibleRepositories[0]; + const repository = this.visibleRepositories[0]; - if (repository) { - const pane = this.panes - .filter(pane => pane instanceof RepositoryPane && pane.repository === repository)[0] as RepositoryPane | undefined; + if (repository) { + const pane = this.panes + .filter(pane => pane instanceof RepositoryPane && pane.repository === repository)[0] as RepositoryPane | undefined; - if (pane) { - pane.focus(); - } else { - super.focus(); - } + if (pane) { + pane.focus(); } else { super.focus(); } + } else { + super.focus(); } } diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index cf9235415f..547e39282f 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -741,7 +741,7 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { private hasToOpenFile(): boolean { const activeEditor = this.editorService.activeEditor; - const file = activeEditor ? activeEditor.getResource() : undefined; + const file = activeEditor ? activeEditor.resource : undefined; if (file) { return file.toString() === this.element.parent().resource.toString(); } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 214ea9f2e9..081a650a1d 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -212,7 +212,7 @@ export class SearchView extends ViewPane { this.memento = new Memento(this.id, storageService); this.viewletState = this.memento.getMemento(StorageScope.WORKSPACE); - this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e))); + this._register(this.fileService.onDidFilesChange(e => this.onFilesChanged(e))); this._register(this.textFileService.untitled.onDidDisposeModel(e => this.onUntitledDidDispose(e))); this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState())); this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory())); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index fea5fd4ef8..57aa23a65a 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -63,7 +63,7 @@ class SearchEditorContribution implements IWorkbenchContribution { }); this.editorService.overrideOpenEditor((editor, options, group) => { - const resource = editor.getResource(); + const resource = editor.resource; if (!resource || !(endsWith(resource.path, '.code-search') || resource.scheme === SearchEditorConstants.SearchEditorScheme) || !(editor instanceof FileEditorInput || (resource.scheme === SearchEditorConstants.SearchEditorScheme))) { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 6dc60fcbaa..467be0ae45 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -483,7 +483,7 @@ export class SearchEditor extends BaseTextEditor { } private loadViewState() { - const resource = assertIsDefined(this.input?.getResource()); + const resource = assertIsDefined(this.input?.resource); return this.loadTextEditorViewState(resource) as SearchEditorViewState; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 5b8cd30234..a9ec34269e 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -102,7 +102,7 @@ export class SearchEditorInput extends EditorInput { const input = this; const workingCopyAdapter = new class implements IWorkingCopy { - readonly resource = input.getResource(); + readonly resource = input.resource; get name() { return input.getName(); } readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : 0; readonly onDidChangeDirty = input.onDidChangeDirty; @@ -116,10 +116,6 @@ export class SearchEditorInput extends EditorInput { this.workingCopyService.registerWorkingCopy(workingCopyAdapter); } - getResource() { - return this.resource; - } - async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { if ((await this.headerModel).isDisposed() || (await this.contentsModel).isDisposed()) { return undefined; } // {{SQL CARBON EDIT}} strict-null-check diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 768602e844..c9bde5b96f 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -116,7 +116,7 @@ namespace snippetExt { function watch(service: IFileService, resource: URI, callback: (type: FileChangeType, resource: URI) => any): IDisposable { return combinedDisposable( service.watch(resource), - service.onFileChanges(e => { + service.onDidFilesChange(e => { for (const change of e.changes) { if (resources.isEqualOrParent(change.resource, resource)) { callback(change.type, change.resource); @@ -277,7 +277,7 @@ class SnippetsService implements ISnippetsService { this._initFolderSnippets(SnippetSource.Workspace, snippetFolder, bucket); } else { // watch - bucket.add(this._fileService.onFileChanges(e => { + bucket.add(this._fileService.onDidFilesChange(e => { if (e.contains(snippetFolder, FileChangeType.ADDED)) { this._initFolderSnippets(SnippetSource.Workspace, snippetFolder, bucket); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 80bb4817f9..26c947f928 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -122,6 +122,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled()); this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflictsSources))); + this._register(this.userDataAuthTokenService.onTokenFailed(_ => this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId))); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled))); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e))); this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e))); @@ -540,13 +541,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } }); if (result.confirmed) { - await this.disableSync(); if (result.checkboxChecked) { this.telemetryService.publicLog2('sync/turnOffEveryWhere'); await this.userDataSyncService.reset(); } else { await this.userDataSyncService.resetLocal(); } + await this.signOut(); + this.disableSync(); } } @@ -567,7 +569,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo try { await this.setActiveAccount(await this.authenticationService.login(this.userDataSyncStore!.authenticationProviderId, ['https://management.core.windows.net/.default', 'offline_access'])); } catch (e) { - this.notificationService.error(e); + this.notificationService.error(localize('loginFailed', "Logging in failed: {0}", e)); throw e; } } @@ -583,12 +585,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const previewResource = source === SyncSource.Settings ? this.workbenchEnvironmentService.settingsSyncPreviewResource : source === SyncSource.Keybindings ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource : null; - return previewResource ? this.editorService.editors.filter(input => input instanceof DiffEditorInput && isEqual(previewResource, input.master.getResource()))[0] : undefined; + return previewResource ? this.editorService.editors.filter(input => input instanceof DiffEditorInput && isEqual(previewResource, input.master.resource))[0] : undefined; } private getAllConflictsEditorInputs(): IEditorInput[] { return this.editorService.editors.filter(input => { - const resource = input instanceof DiffEditorInput ? input.master.getResource() : input.getResource(); + const resource = input instanceof DiffEditorInput ? input.master.resource : input.resource; return isEqual(resource, this.workbenchEnvironmentService.settingsSyncPreviewResource) || isEqual(resource, this.workbenchEnvironmentService.keybindingsSyncPreviewResource); }); } @@ -671,7 +673,15 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); const stopSyncCommandId = 'workbench.userData.actions.stopSync'; - CommandsRegistry.registerCommand(stopSyncCommandId, () => this.turnOff()); + CommandsRegistry.registerCommand(stopSyncCommandId, async () => { + try { + await this.turnOff(); + } catch (e) { + if (!isPromiseCanceledError(e)) { + this.notificationService.error(localize('turn off failed', "Error while turning off sync: {0}", toErrorMessage(e))); + } + } + }); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', command: { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index 84c358ae7e..6b21810acf 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -48,7 +48,7 @@ export class UserDataSyncTrigger extends Disposable { if (editorInput instanceof KeybindingsEditorInput) { return true; } - const resource = editorInput.getResource(); + const resource = editorInput.resource; if (isEqual(resource, this.workbenchEnvironmentService.settingsResource)) { return true; } diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index 0222506818..e043441bf5 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -26,6 +26,11 @@ export class WebviewInput extends EditorInput { private readonly _onDisposeWebview = this._register(new Emitter()); readonly onDisposeWebview = this._onDisposeWebview.event; + readonly resource = URI.from({ + scheme: WebviewPanelResourceScheme, + path: `webview-panel/webview-${this.id}` + }); + constructor( public readonly id: string, public readonly viewType: string, @@ -52,13 +57,6 @@ export class WebviewInput extends EditorInput { return WebviewInput.typeId; } - public getResource(): URI { - return URI.from({ - scheme: WebviewPanelResourceScheme, - path: `webview-panel/webview-${this.id}` - }); - } - public getName(): string { return this._name; } diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts new file mode 100644 index 0000000000..3c93e35f75 --- /dev/null +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcome.contribution.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { ViewsWelcomeContribution } from 'vs/workbench/contrib/welcome/common/viewsWelcomeContribution'; +import { ViewsWelcomeExtensionPoint, viewsWelcomeExtensionPointDescriptor } from 'vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +const extensionPoint = ExtensionsRegistry.registerExtensionPoint(viewsWelcomeExtensionPointDescriptor); + +class WorkbenchConfigurationContribution { + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + instantiationService.createInstance(ViewsWelcomeContribution, extensionPoint); + } +} + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(WorkbenchConfigurationContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts new file mode 100644 index 0000000000..dbfb91e1cb --- /dev/null +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ViewsWelcomeExtensionPoint, ViewWelcome, viewsWelcomeExtensionPointDescriptor } from './viewsWelcomeExtensionPoint'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as ViewContainerExtensions, IViewsRegistry } from 'vs/workbench/common/views'; +import { localize } from 'vs/nls'; + +const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + +export class ViewsWelcomeContribution extends Disposable implements IWorkbenchContribution { + + private viewWelcomeContents = new Map(); + + constructor(extensionPoint: IExtensionPoint) { + super(); + + extensionPoint.setHandler((_, { added, removed }) => { + for (const contribution of removed) { + // Proposed API check + if (!contribution.description.enableProposedApi) { + continue; + } + + for (const welcome of contribution.value) { + const disposable = this.viewWelcomeContents.get(welcome); + + if (disposable) { + disposable.dispose(); + } + } + } + + for (const contribution of added) { + // Proposed API check + if (!contribution.description.enableProposedApi) { + contribution.collector.error(localize('proposedAPI.invalid', "The '{0}' contribution is a proposed API and is only available when running out of dev or with the following command line switch: --enable-proposed-api {1}", viewsWelcomeExtensionPointDescriptor.extensionPoint, contribution.description.identifier.value)); + continue; + } + + for (const welcome of contribution.value) { + const disposable = viewsRegistry.registerViewWelcomeContent(welcome.view, { + content: welcome.contents, + when: ContextKeyExpr.deserialize(welcome.when) + }); + + this.viewWelcomeContents.set(welcome, disposable); + } + } + }); + } +} diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts new file mode 100644 index 0000000000..49283c570f --- /dev/null +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; + +export enum ViewsWelcomeExtensionPointFields { + view = 'view', + contents = 'contents', + when = 'when', +} + +export interface ViewWelcome { + readonly [ViewsWelcomeExtensionPointFields.view]: string; + readonly [ViewsWelcomeExtensionPointFields.contents]: string; + readonly [ViewsWelcomeExtensionPointFields.when]: string; +} + +export type ViewsWelcomeExtensionPoint = ViewWelcome[]; + +const viewsWelcomeExtensionPointSchema = Object.freeze({ + type: 'array', + description: nls.localize('contributes.viewsWelcome', "Contributed views welcome content."), + items: { + type: 'object', + description: nls.localize('contributes.viewsWelcome.view', "Contributed welcome content for a specific view."), + required: [ + ViewsWelcomeExtensionPointFields.view, + ViewsWelcomeExtensionPointFields.contents + ], + properties: { + [ViewsWelcomeExtensionPointFields.view]: { + type: 'string', + description: nls.localize('contributes.viewsWelcome.view.view', "View identifier for this welcome content."), + }, + [ViewsWelcomeExtensionPointFields.contents]: { + type: 'string', + description: nls.localize('contributes.viewsWelcome.view.contents', "Welcome content."), + }, + [ViewsWelcomeExtensionPointFields.when]: { + type: 'string', + description: nls.localize('contributes.viewsWelcome.view.when', "When clause for this welcome content."), + }, + } + } +}); + +export const viewsWelcomeExtensionPointDescriptor = { + extensionPoint: 'viewsWelcome', + jsonSchema: viewsWelcomeExtensionPointSchema +}; diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts index f7b6446009..f0be8dad96 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts @@ -53,6 +53,8 @@ export class WalkThroughInput extends EditorInput { private maxTopScroll = 0; private maxBottomScroll = 0; + readonly resource = this.options.resource; + constructor( private options: WalkThroughInputOptions, @ITextModelService private readonly textModelResolverService: ITextModelService @@ -60,10 +62,6 @@ export class WalkThroughInput extends EditorInput { super(); } - getResource(): URI { - return this.options.resource; - } - getTypeId(): string { return this.options.typeId; } @@ -99,7 +97,7 @@ export class WalkThroughInput extends EditorInput { if (!this.promise) { this.promise = this.textModelResolverService.createModelReference(this.options.resource) .then(ref => { - if (strings.endsWith(this.getResource().path, '.html')) { + if (strings.endsWith(this.resource.path, '.html')) { return new WalkThroughModel(ref, []); } diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index 33801fb1ff..6262b6cbf7 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -280,7 +280,7 @@ export class WalkThroughPart extends BaseEditor { } const content = model.main.textEditorModel.getValue(EndOfLinePreference.LF); - if (!strings.endsWith(input.getResource().path, '.md')) { + if (!strings.endsWith(input.resource.path, '.md')) { this.content.innerHTML = content; this.updateSizeClasses(); this.decorateContent(); diff --git a/src/vs/workbench/services/accessibility/node/accessibilityService.ts b/src/vs/workbench/services/accessibility/node/accessibilityService.ts index ae308e1d8d..8911b52f0b 100644 --- a/src/vs/workbench/services/accessibility/node/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/node/accessibilityService.ts @@ -78,11 +78,13 @@ class LinuxAccessibilityContribution implements IWorkbenchContribution { @IAccessibilityService accessibilityService: AccessibilityService, @IEnvironmentService environmentService: IEnvironmentService ) { - accessibilityService.onDidChangeScreenReaderOptimized(async () => { + const forceRendererAccessibility = () => { if (accessibilityService.isScreenReaderOptimized()) { - await jsonEditingService.write(environmentService.argvResource, [{ key: 'force-renderer-accessibility', value: true }], true); + jsonEditingService.write(environmentService.argvResource, [{ key: 'force-renderer-accessibility', value: true }], true); } - }); + }; + forceRendererAccessibility(); + accessibilityService.onDidChangeScreenReaderOptimized(forceRendererAccessibility); } } diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 0cff714edd..3bed0f5baa 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -25,6 +25,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; @@ -237,6 +238,7 @@ class BulkEdit { @ILogService private readonly _logService: ILogService, @IFileService private readonly _fileService: IFileService, @ITextFileService private readonly _textFileService: ITextFileService, + @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { this._editor = editor; @@ -309,7 +311,7 @@ class BulkEdit { if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) { continue; // not overwriting, but ignoring, and the target file exists } - await this._textFileService.move(edit.oldUri, edit.newUri, options.overwrite); + await this._workingCopyFileService.move(edit.oldUri, edit.newUri, options.overwrite); } else if (!edit.newUri && edit.oldUri) { // delete file @@ -318,7 +320,7 @@ class BulkEdit { if (useTrash && !(this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) { useTrash = false; // not supported by provider } - await this._textFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive }); + await this._workingCopyFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive }); } else if (!options.ignoreIfNotExists) { throw new Error(`${edit.oldUri} does not exist and can not be deleted`); } diff --git a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts b/src/vs/workbench/services/bulkEdit/browser/conflicts.ts index bb00b17ac7..34b87c1ddc 100644 --- a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts +++ b/src/vs/workbench/services/bulkEdit/browser/conflicts.ts @@ -50,7 +50,7 @@ export class ConflictDetector { } // listen to file changes - this._disposables.add(fileService.onFileChanges(e => { + this._disposables.add(fileService.onDidFilesChange(e => { for (let change of e.changes) { if (modelService.getModel(change.resource)) { diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 5acc32f7c8..68fd49c545 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -86,7 +86,7 @@ class FileServiceBasedConfigurationWithNames extends Disposable { this._cache = new ConfigurationModel(); this.changeEventTriggerScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); - this._register(this.fileService.onFileChanges((e) => this.handleFileEvents(e))); + this._register(this.fileService.onDidFilesChange((e) => this.handleFileEvents(e))); } async loadConfiguration(): Promise { @@ -268,7 +268,7 @@ class FileServiceBasedRemoteUserConfiguration extends Disposable { super(); this.parser = new ConfigurationModelParser(this.configurationResource.toString(), this.scopes); - this._register(fileService.onFileChanges(e => this.handleFileEvents(e))); + this._register(fileService.onDidFilesChange(e => this.handleFileEvents(e))); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); this._register(toDisposable(() => { this.stopWatchingResource(); @@ -523,7 +523,7 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable implements IWork this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(''); this.workspaceSettings = new ConfigurationModel(); - this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e))); + this._register(fileService.onDidFilesChange(e => this.handleWorkspaceFileEvents(e))); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); this.workspaceConfigWatcher = this._register(this.watchWorkspaceConfigurationFile()); } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 886c5674e1..4a106b8061 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -679,8 +679,8 @@ export class EditorService extends Disposable implements EditorServiceImpl { } private toSideBySideLabel(leftInput: EditorInput, rightInput: EditorInput, divider: string): string | undefined { - const leftResource = leftInput.getResource(); - const rightResource = rightInput.getResource(); + const leftResource = leftInput.resource; + const rightResource = rightInput.resource; // Without any resource, do not try to compute a label if (!leftResource || !rightResource) { diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 65d8c6e405..f0a075bc4c 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -41,7 +41,7 @@ class TestEditorControl extends BaseEditor { class TestEditorInput extends EditorInput implements IFileEditorInput { - constructor(private resource: URI) { super(); } + constructor(public resource: URI) { super(); } getTypeId() { return TEST_EDITOR_INPUT_ID; } resolve(): Promise { return Promise.resolve(null); } @@ -51,7 +51,6 @@ class TestEditorInput extends EditorInput implements IFileEditorInput { setPreferredEncoding(encoding: string) { } setMode(mode: string) { } setPreferredMode(mode: string) { } - getResource(): URI { return this.resource; } setForceOpenAsBinary(): void { } } @@ -73,7 +72,7 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite serialize(editorInput: EditorInput): string { const testEditorInput = editorInput; const testInput: ISerializedTestEditorInput = { - resource: testEditorInput.getResource().toString() + resource: testEditorInput.resource.toString() }; return JSON.stringify(testInput); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 311197389a..04091b48f0 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -67,7 +67,6 @@ class TestEditorInput extends EditorInput implements IFileEditorInput { setPreferredEncoding(encoding: string) { } setMode(mode: string) { } setPreferredMode(mode: string) { } - getResource(): URI { return this.resource; } setForceOpenAsBinary(): void { } setFailToOpen(): void { this.fails = true; @@ -299,7 +298,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite let input = service.createInput({ resource: toResource.call(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } }); assert(input instanceof FileEditorInput); let contentInput = input; - assert.strictEqual(contentInput.getResource().fsPath, toResource.call(this, '/index.html').fsPath); + assert.strictEqual(contentInput.resource.fsPath, toResource.call(this, '/index.html').fsPath); // Untyped Input (file, encoding) input = service.createInput({ resource: toResource.call(this, '/index.html'), encoding: 'utf16le', options: { selection: { startLineNumber: 1, startColumn: 1 } } }); diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index 9f7a127695..cfaf89d0fa 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -56,7 +56,6 @@ class TestEditorInput extends EditorInput implements IFileEditorInput { setPreferredEncoding(encoding: string) { } setMode(mode: string) { } setPreferredMode(mode: string) { } - getResource(): URI { return this.resource; } setForceOpenAsBinary(): void { } isDirty(): boolean { return this.dirty; } setDirty(): void { this.dirty = true; } diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 6ce0d5ee32..613dc2232e 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -13,7 +13,7 @@ import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementSer import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationHandle, INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IURLHandler, IURLService, IOpenURLOptions } from 'vs/platform/url/common/url'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -26,6 +26,7 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_SECONDS = 30 * 1000; @@ -100,7 +101,8 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { @IHostService private readonly hostService: IHostService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IStorageService private readonly storageService: IStorageService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProgressService private readonly progressService: IProgressService ) { this.storage = new ConfirmedExtensionIdStorage(storageService); @@ -281,32 +283,20 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { return; } - let notificationHandle: INotificationHandle | null = this.notificationService.notify({ severity: Severity.Info, message: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) }); - notificationHandle.progress.infinite(); - notificationHandle.onDidClose(() => notificationHandle = null); - try { - await this.extensionManagementService.installFromGallery(galleryExtension); - const reloadMessage = localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString()); - const reloadActionLabel = localize('Reload', "Reload Window and Open"); + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) + }, () => this.extensionManagementService.installFromGallery(galleryExtension)); - if (notificationHandle) { - notificationHandle.progress.done(); - notificationHandle.updateMessage(reloadMessage); - notificationHandle.updateActions({ - primary: [new Action('reloadWindow', reloadActionLabel, undefined, true, () => this.reloadAndHandle(uri))] - }); - } else { - this.notificationService.prompt(Severity.Info, reloadMessage, [{ label: reloadActionLabel, run: () => this.reloadAndHandle(uri) }], { sticky: true }); - } - } catch (e) { - if (notificationHandle) { - notificationHandle.progress.done(); - notificationHandle.updateSeverity(Severity.Error); - notificationHandle.updateMessage(e); - } else { - this.notificationService.error(e); - } + this.notificationService.prompt( + Severity.Info, + localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString()), + [{ label: localize('Reload', "Reload Window and Open"), run: () => this.reloadAndHandle(uri) }], + { sticky: true } + ); + } catch (error) { + this.notificationService.error(error); } } } diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index fe8b539508..07721ce29d 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -125,7 +125,7 @@ export class HistoryService extends Disposable implements IHistoryService { this._register(this.editorService.onDidOpenEditorFail(event => this.remove(event.editor))); this._register(this.editorService.onDidCloseEditor(event => this.onEditorClosed(event))); this._register(this.storageService.onWillSaveState(() => this.saveState())); - this._register(this.fileService.onFileChanges(event => this.onFileChanges(event))); + this._register(this.fileService.onDidFilesChange(event => this.onDidFilesChange(event))); this._register(this.resourceFilter.onExpressionChange(() => this.removeExcludedFromHistory())); this._register(this.editorService.onDidMostRecentlyActiveEditorsChange(() => this.handleEditorEventInRecentEditorsStack())); @@ -220,7 +220,7 @@ export class HistoryService extends Disposable implements IHistoryService { return identifier.editor.matches(editor.input); } - private onFileChanges(e: FileChangesEvent): void { + private onDidFilesChange(e: FileChangesEvent): void { if (e.gotDeleted()) { this.remove(e); // remove from history files that got deleted or moved } @@ -511,7 +511,7 @@ export class HistoryService extends Disposable implements IHistoryService { } private preferResourceInput(input: IEditorInput): IEditorInput | IResourceInput { - const resource = input.getResource(); + const resource = input.resource; if (resource && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData)) { // for now, only prefer well known schemes that we control to prevent // issues such as https://github.com/microsoft/vscode/issues/85204 @@ -586,7 +586,7 @@ export class HistoryService extends Disposable implements IHistoryService { } if (arg2 instanceof EditorInput) { - const inputResource = arg2.getResource(); + const inputResource = arg2.resource; if (!inputResource) { return false; } @@ -615,7 +615,7 @@ export class HistoryService extends Disposable implements IHistoryService { // Track closing of editor to support to reopen closed editors (unless editor was replaced) if (!event.replaced) { - const resource = event.editor ? event.editor.getResource() : undefined; + const resource = event.editor ? event.editor.resource : undefined; const supportsReopen = resource && this.fileService.canHandleResource(resource); // we only support file'ish things to reopen if (resource && supportsReopen) { @@ -661,7 +661,7 @@ export class HistoryService extends Disposable implements IHistoryService { private containsRecentlyClosedFile(group: IEditorGroup, recentlyClosedEditor: IRecentlyClosedFile): boolean { for (const editor of group.editors) { - if (isEqual(editor.getResource(), recentlyClosedEditor.resource)) { + if (isEqual(editor.resource, recentlyClosedEditor.resource)) { return true; } } diff --git a/src/vs/workbench/services/history/test/browser/history.test.ts b/src/vs/workbench/services/history/test/browser/history.test.ts index 291cceb008..edefe24836 100644 --- a/src/vs/workbench/services/history/test/browser/history.test.ts +++ b/src/vs/workbench/services/history/test/browser/history.test.ts @@ -56,7 +56,6 @@ class TestEditorInput extends EditorInput implements IFileEditorInput { setPreferredEncoding(encoding: string) { } setMode(mode: string) { } setPreferredMode(mode: string) { } - getResource(): URI { return this.resource; } setForceOpenAsBinary(): void { } } @@ -175,7 +174,7 @@ suite.skip('HistoryService', function () { // {{SQL CARBON EDIT}} TODO @anthonyd const input1 = new TestEditorInput(URI.parse('foo://bar1')); await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - assert.equal(historyService.getLastActiveFile('foo')?.toString(), input1.getResource().toString()); + assert.equal(historyService.getLastActiveFile('foo')?.toString(), input1.resource.toString()); part.dispose(); }); diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index fb842606bd..2e22787ca5 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -635,7 +635,7 @@ class UserKeybindings extends Disposable { this._onDidChange.fire(); } }), 50)); - this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.keybindingsResource))(() => this.reloadConfigurationScheduler.schedule())); + this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.keybindingsResource))(() => this.reloadConfigurationScheduler.schedule())); } async initialize(): Promise { diff --git a/src/vs/workbench/services/keybinding/browser/keymapService.ts b/src/vs/workbench/services/keybinding/browser/keymapService.ts index 414c9531f5..bd962440e0 100644 --- a/src/vs/workbench/services/keybinding/browser/keymapService.ts +++ b/src/vs/workbench/services/keybinding/browser/keymapService.ts @@ -474,7 +474,7 @@ class UserKeyboardLayout extends Disposable { } }), 50)); - this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.keyboardLayoutResource))(() => this.reloadConfigurationScheduler.schedule())); + this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.keyboardLayoutResource))(() => this.reloadConfigurationScheduler.schedule())); } async initialize(): Promise { diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 410ae74d49..c50f1d4631 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -51,6 +51,7 @@ import { TestWindowConfiguration, TestTextFileService } from 'vs/workbench/test/ import { ILabelService } from 'vs/platform/label/common/label'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { WorkingCopyFileService, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; class TestEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -108,6 +109,8 @@ suite('KeybindingsEditing', () => { fileService.registerProvider(Schemas.file, diskFileSystemProvider); fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); instantiationService.stub(IFileService, fileService); + instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService()); + instantiationService.stub(IWorkingCopyFileService, instantiationService.createInstance(WorkingCopyFileService)); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); instantiationService.stub(IBackupFileService, new TestBackupFileService()); diff --git a/src/vs/workbench/services/output/electron-browser/outputChannelModelService.ts b/src/vs/workbench/services/output/electron-browser/outputChannelModelService.ts index b09c208b0d..1317262f22 100644 --- a/src/vs/workbench/services/output/electron-browser/outputChannelModelService.ts +++ b/src/vs/workbench/services/output/electron-browser/outputChannelModelService.ts @@ -52,7 +52,7 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implement this.rotatingFilePath = resources.joinPath(rotatingFilePathDirectory, `${id}.1.log`); this._register(fileService.watch(rotatingFilePathDirectory)); - this._register(fileService.onFileChanges(e => { + this._register(fileService.onDidFilesChange(e => { if (e.contains(this.rotatingFilePath)) { this.resettingDelayer.trigger(() => this.resetModel()); } diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 06b2695930..6dab8c3f5b 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -207,7 +207,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } const editorInput = this.getActiveSettingsEditorInput() || this.lastOpenedSettingsInput; - const resource = editorInput ? editorInput.master.getResource()! : this.userSettingsResource; + const resource = editorInput ? editorInput.master.resource! : this.userSettingsResource; const target = this.getConfigurationTargetFromSettingsResource(resource); return this.openOrSwitchSettings(target, resource, { query: query }); } @@ -332,7 +332,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Promise { const editorInput = this.getActiveSettingsEditorInput(group); if (editorInput) { - const editorInputResource = editorInput.master.getResource(); + const editorInputResource = editorInput.master.resource; if (editorInputResource && editorInputResource.fsPath !== resource.fsPath) { return this.doSwitchSettings(configurationTarget, resource, editorInput, group, options); } diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts index 998987c127..6d72472eac 100644 --- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -75,6 +75,8 @@ export class KeybindingsEditorInput extends EditorInput { searchOptions: IKeybindingsEditorSearchOptions | null = null; + readonly resource = undefined; + constructor(@IInstantiationService instantiationService: IInstantiationService) { super(); this.keybindingsModel = instantiationService.createInstance(KeybindingsEditorModel, OS); @@ -101,7 +103,8 @@ export class SettingsEditor2Input extends EditorInput { static readonly ID: string = 'workbench.input.settings2'; private readonly _settingsModel: Settings2EditorModel; - private resource: URI = URI.from({ + + readonly resource: URI = URI.from({ scheme: 'vscode-settings', path: `settingseditor` }); @@ -129,8 +132,4 @@ export class SettingsEditor2Input extends EditorInput { resolve(): Promise { return Promise.resolve(this._settingsModel); } - - getResource(): URI { - return this.resource; - } } diff --git a/src/vs/workbench/services/progress/browser/media/progressService.css b/src/vs/workbench/services/progress/browser/media/progressService.css index 69d2202c42..8c37926b70 100644 --- a/src/vs/workbench/services/progress/browser/media/progressService.css +++ b/src/vs/workbench/services/progress/browser/media/progressService.css @@ -3,14 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.progress { - padding-left: 5px; -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.progress .spinner-container { - padding-right: 5px; -} - .monaco-workbench .progress-badge > .badge-content::before { mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4="); -webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4="); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 398098b646..133ada177b 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -12,9 +12,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { StatusbarAlignment, IStatusbarService } from 'vs/workbench/services/statusbar/common/statusbar'; import { timeout } from 'vs/base/common/async'; import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity'; -import { INotificationService, Severity, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity, INotificationHandle } from 'vs/platform/notification/common/notification'; import { Action } from 'vs/base/common/actions'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Dialog } from 'vs/base/browser/ui/dialog/dialog'; @@ -143,10 +143,72 @@ export class ProgressService extends Disposable implements IProgressService { } } - private withNotificationProgress

, R = unknown>(options: IProgressNotificationOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: (choice?: number) => void): P { - const toDispose = new DisposableStore(); + private withNotificationProgress

, R = unknown>(options: IProgressNotificationOptions, callback: (progress: IProgress) => P, onDidCancel?: (choice?: number) => void): P { + + const progressStateModel = new class extends Disposable { + + private readonly _onDidReport = this._register(new Emitter()); + readonly onDidReport = this._onDidReport.event; + + private readonly _onDispose = this._register(new Emitter()); + readonly onDispose = this._onDispose.event; + + private _step: IProgressStep | undefined = undefined; + get step() { return this._step; } + + private _done = false; + get done() { return this._done; } + + readonly promise: P; + + constructor() { + super(); + + this.promise = callback(this); + + this.promise.finally(() => { + this.dispose(); + }); + } + + report(step: IProgressStep): void { + this._step = step; + + this._onDidReport.fire(step); + } + + cancel(choice?: number): void { + onDidCancel?.(choice); + + this.dispose(); + } + + dispose(): void { + this._done = true; + this._onDispose.fire(); + + super.dispose(); + } + }; + + const createWindowProgress = () => { + this.withWindowProgress({ + location: ProgressLocation.Window, + title: options.title + }, progress => { + if (progressStateModel.step) { + progress.report(progressStateModel.step); + } + + const disposable = progressStateModel.onDidReport(step => progress.report(step)); + Event.once(progressStateModel.onDispose)(() => disposable.dispose()); + + return progressStateModel.promise; + }); + }; const createNotification = (message: string, increment?: number): INotificationHandle => { + const notificationDisposables = new DisposableStore(); const primaryActions = options.primaryActions ? Array.from(options.primaryActions) : []; const secondaryActions = options.secondaryActions ? Array.from(options.secondaryActions) : []; @@ -158,16 +220,11 @@ export class ProgressService extends Disposable implements IProgressService { super(`progress.button.${button}`, button, undefined, true); } - run(): Promise { - if (typeof onDidCancel === 'function') { - onDidCancel(index); - } - - return Promise.resolve(undefined); + async run(): Promise { + progressStateModel.cancel(index); } }; - - toDispose.add(buttonAction); + notificationDisposables.add(buttonAction); primaryActions.push(buttonAction); }); @@ -179,31 +236,35 @@ export class ProgressService extends Disposable implements IProgressService { super('progress.cancel', localize('cancel', "Cancel"), undefined, true); } - run(): Promise { - if (typeof onDidCancel === 'function') { - onDidCancel(); - } - - return Promise.resolve(undefined); + async run(): Promise { + progressStateModel.cancel(); } }; - toDispose.add(cancelAction); + notificationDisposables.add(cancelAction); primaryActions.push(cancelAction); } - const actions: INotificationActions = { primary: primaryActions, secondary: secondaryActions }; const handle = this.notificationService.notify({ severity: Severity.Info, message, source: options.source, - actions + actions: { primary: primaryActions, secondary: secondaryActions } }); updateProgress(handle, increment); Event.once(handle.onDidClose)(() => { - toDispose.dispose(); + + // Switch to window based progress once the notification + // is being closed even though still running and not + // cancelled. + if (!progressStateModel.done) { + createWindowProgress(); + } + + // Clear disposables + notificationDisposables.dispose(); }); return handle; @@ -218,60 +279,54 @@ export class ProgressService extends Disposable implements IProgressService { } }; - let handle: INotificationHandle | undefined; - let handleSoon: any | undefined; - + let notificationHandle: INotificationHandle | undefined; + let notificationTimeout: any | undefined; let titleAndMessage: string | undefined; // hoisted to make sure a delayed notification shows the most recent message - const updateNotification = (message?: string, increment?: number): void => { + const updateNotification = (step?: IProgressStep): void => { // full message (inital or update) - if (message && options.title) { - titleAndMessage = `${options.title}: ${message}`; // always prefix with overall title if we have it (https://github.com/Microsoft/vscode/issues/50932) + if (step?.message && options.title) { + titleAndMessage = `${options.title}: ${step.message}`; // always prefix with overall title if we have it (https://github.com/Microsoft/vscode/issues/50932) } else { - titleAndMessage = options.title || message; + titleAndMessage = options.title || step?.message; } - if (!handle && titleAndMessage) { + if (!notificationHandle && titleAndMessage) { + // create notification now or after a delay if (typeof options.delay === 'number' && options.delay > 0) { - if (typeof handleSoon !== 'number') { - handleSoon = setTimeout(() => handle = createNotification(titleAndMessage!, increment), options.delay); + if (typeof notificationTimeout !== 'number') { + notificationTimeout = setTimeout(() => notificationHandle = createNotification(titleAndMessage!, step?.increment), options.delay); } } else { - handle = createNotification(titleAndMessage, increment); + notificationHandle = createNotification(titleAndMessage, step?.increment); } } - if (handle) { + if (notificationHandle) { if (titleAndMessage) { - handle.updateMessage(titleAndMessage); + notificationHandle.updateMessage(titleAndMessage); } - if (typeof increment === 'number') { - updateProgress(handle, increment); + + if (typeof step?.increment === 'number') { + updateProgress(notificationHandle, step.increment); } } }; // Show initially - updateNotification(); - - // Update based on progress - const promise = callback({ - report: progress => { - updateNotification(progress.message, progress.increment); - } - }); + updateNotification(progressStateModel.step); + const listener = progressStateModel.onDidReport(step => updateNotification(step)); + Event.once(progressStateModel.onDispose)(() => listener.dispose()); // Show progress for at least 800ms and then hide once done or canceled - Promise.all([timeout(800), promise]).finally(() => { - clearTimeout(handleSoon); - if (handle) { - handle.close(); - } + Promise.all([timeout(800), progressStateModel.promise]).finally(() => { + clearTimeout(notificationTimeout); + notificationHandle?.close(); }); - return promise; + return progressStateModel.promise; } private withViewletProgress

, R = unknown>(viewletId: string, task: (progress: IProgress) => P, options: IProgressCompositeOptions): P { diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index ca9f187181..9f29e61cf2 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -5,22 +5,21 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { Emitter, AsyncEmitter } from 'vs/base/common/event'; -import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModel, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions, ITextFileEditorModelManager, ISaveParticipant } from 'vs/workbench/services/textfile/common/textfiles'; +import { AsyncEmitter } from 'vs/base/common/event'; +import { ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, TextFileCreateEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files'; +import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions } from 'vs/platform/files/common/files'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUntitledTextEditorService, IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { isEqualOrParent, isEqual, joinPath, dirname, basename, toLocalResource } from 'vs/base/common/resources'; +import { isEqual, joinPath, dirname, basename, toLocalResource } from 'vs/base/common/resources'; import { IDialogService, IFileDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; import { VSBuffer } from 'vs/base/common/buffer'; import { ITextSnapshot, ITextModel } from 'vs/editor/common/model'; @@ -31,10 +30,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { coalesce } from 'vs/base/common/arrays'; import { suggestFilename } from 'vs/base/common/mime'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { isValidBasename } from 'vs/base/common/extpath'; @@ -47,11 +43,11 @@ export abstract class AbstractTextFileService extends Disposable implements ITex //#region events - private _onWillRunOperation = this._register(new AsyncEmitter()); - readonly onWillRunOperation = this._onWillRunOperation.event; + private _onWillCreateTextFile = this._register(new AsyncEmitter()); + readonly onWillCreateTextFile = this._onWillCreateTextFile.event; - private _onDidRunOperation = this._register(new Emitter()); - readonly onDidRunOperation = this._onDidRunOperation.event; + private _onDidCreateTextFile = this._register(new AsyncEmitter()); + readonly onDidCreateTextFile = this._onDidCreateTextFile.event; //#endregion @@ -59,18 +55,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex readonly untitled: IUntitledTextEditorModelManager = this.untitledTextEditorService; - saveErrorHandler = (() => { - const notificationService = this.notificationService; - - return { - onSaveError(error: Error, model: ITextFileEditorModel): void { - notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", model.name, toErrorMessage(error, false))); - } - }; - })(); - - saveParticipant: ISaveParticipant | undefined = undefined; - abstract get encoding(): IResourceEncodings; constructor( @@ -86,7 +70,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService, @ITextModelService private readonly textModelService: ITextModelService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, - @INotificationService private readonly notificationService: INotificationService, @IRemotePathService private readonly remotePathService: IRemotePathService ) { super(); @@ -100,7 +83,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex this.lifecycleService.onShutdown(this.dispose, this); } - //#region text file read / write + //#region text file read / write / create async read(resource: URI, options?: IReadTextFileOptions): Promise { const content = await this.fileService.readFile(resource, options); @@ -156,19 +139,12 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } } - async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { - return this.fileService.writeFile(resource, toBufferOrReadable(value), options); - } - - //#endregion - - //#region text file IO primitives (create, move, copy, delete) - async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise { // before event - await this._onWillRunOperation.fireAsync({ operation: FileOperation.CREATE, target: resource }, CancellationToken.None); + await this._onWillCreateTextFile.fireAsync({ resource }, CancellationToken.None); + // create file on disk const stat = await this.doCreate(resource, value, options); // If we had an existing model for the given resource, load @@ -181,7 +157,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } // after event - this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.CREATE, resource)); + await this._onDidCreateTextFile.fireAsync({ resource }, CancellationToken.None); return stat; } @@ -190,127 +166,13 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return this.fileService.createFile(resource, toBufferOrReadable(value), options); } - async move(source: URI, target: URI, overwrite?: boolean): Promise { - return this.moveOrCopy(source, target, true, overwrite); - } - - async copy(source: URI, target: URI, overwrite?: boolean): Promise { - return this.moveOrCopy(source, target, false, overwrite); - } - - private async moveOrCopy(source: URI, target: URI, move: boolean, overwrite?: boolean): Promise { - - // before event - await this._onWillRunOperation.fireAsync({ operation: move ? FileOperation.MOVE : FileOperation.COPY, target, source }, CancellationToken.None); - - // find all models that related to either source or target (can be many if resource is a folder) - const sourceModels: ITextFileEditorModel[] = []; - const targetModels: ITextFileEditorModel[] = []; - for (const model of this.getFileModels()) { - const resource = model.resource; - - if (isEqualOrParent(resource, target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)) { - targetModels.push(model); - } - - if (isEqualOrParent(resource, source)) { - sourceModels.push(model); - } - } - - // remember each source model to load again after move is done - // with optional content to restore if it was dirty - type ModelToRestore = { resource: URI; snapshot?: ITextSnapshot; encoding?: string; mode?: string }; - const modelsToRestore: ModelToRestore[] = []; - for (const sourceModel of sourceModels) { - const sourceModelResource = sourceModel.resource; - - // If the source is the actual model, just use target as new resource - let modelToRestoreResource: URI; - if (isEqual(sourceModelResource, source)) { - modelToRestoreResource = target; - } - - // Otherwise a parent folder of the source is being moved, so we need - // to compute the target resource based on that - else { - modelToRestoreResource = joinPath(target, sourceModelResource.path.substr(source.path.length + 1)); - } - - const modelToRestore: ModelToRestore = { resource: modelToRestoreResource, encoding: sourceModel.getEncoding() }; - if (sourceModel.isDirty()) { - modelToRestore.snapshot = sourceModel.createSnapshot(); - } - - modelsToRestore.push(modelToRestore); - } - - // handle dirty models depending on the operation: - // - move: revert both source and target (if any) - // - copy: revert target (if any) - const dirtyModelsToRevert = (move ? [...sourceModels, ...targetModels] : [...targetModels]).filter(model => model.isDirty()); - await this.doRevertFiles(dirtyModelsToRevert.map(dirtyModel => dirtyModel.resource), { soft: true }); - - // now we can rename the source to target via file operation - let stat: IFileStatWithMetadata; - try { - if (move) { - stat = await this.fileService.move(source, target, overwrite); - } else { - stat = await this.fileService.copy(source, target, overwrite); - } - } catch (error) { - - // in case of any error, ensure to set dirty flag back - dirtyModelsToRevert.forEach(dirtyModel => dirtyModel.setDirty(true)); - - throw error; - } - - // finally, restore models that we had loaded previously - await Promise.all(modelsToRestore.map(async modelToRestore => { - - // restore the model, forcing a reload. this is important because - // we know the file has changed on disk after the move and the - // model might have still existed with the previous state. this - // ensures we are not tracking a stale state. - const restoredModel = await this.files.resolve(modelToRestore.resource, { reload: { async: false }, encoding: modelToRestore.encoding, mode: modelToRestore.mode }); - - // restore previous dirty content if any and ensure to mark - // the model as dirty - if (modelToRestore.snapshot && restoredModel.isResolved()) { - this.modelService.updateModel(restoredModel.textEditorModel, createTextBufferFactoryFromSnapshot(modelToRestore.snapshot)); - - restoredModel.setDirty(true); - } - })); - - // after event - this._onDidRunOperation.fire(new FileOperationDidRunEvent(move ? FileOperation.MOVE : FileOperation.COPY, target, source)); - - return stat; - } - - async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise { - - // before event - await this._onWillRunOperation.fireAsync({ operation: FileOperation.DELETE, target: resource }, CancellationToken.None); - - // Check for any existing dirty file model for the resource - // and do a soft revert before deleting to be able to close - // any opened editor with these files - const dirtyFiles = this.getDirtyFileModels().map(dirtyFileModel => dirtyFileModel.resource).filter(dirty => isEqualOrParent(dirty, resource)); - await this.doRevertFiles(dirtyFiles, { soft: true }); - - // Now actually delete from disk - await this.fileService.del(resource, options); - - // after event - this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.DELETE, resource)); + async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { + return this.fileService.writeFile(resource, toBufferOrReadable(value), options); } //#endregion + //#region save async save(resource: URI, options?: ITextFileSaveOptions): Promise { @@ -353,18 +215,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return undefined; } - private getFileModels(resources?: URI[]): ITextFileEditorModel[] { - if (Array.isArray(resources)) { - return coalesce(resources.map(resource => this.files.get(resource))); - } - - return this.files.getAll(); - } - - private getDirtyFileModels(resources?: URI[]): ITextFileEditorModel[] { - return this.getFileModels(resources).filter(model => model.isDirty()); - } - async saveAs(source: URI, target?: URI, options?: ITextFileSaveOptions): Promise { // Get to target resource @@ -612,34 +462,12 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } // File - return !(await this.doRevertFiles([resource], options)).results.some(result => result.error); - } + const model = this.files.get(resource); + if (model && (model.isDirty() || options?.force)) { + return model.revert(options); + } - private async doRevertFiles(resources: URI[], options?: IRevertOptions): Promise { - const fileModels = options?.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources); - - const mapResourceToResult = new ResourceMap(); - fileModels.forEach(fileModel => { - mapResourceToResult.set(fileModel.resource, { - source: fileModel.resource - }); - }); - - await Promise.all(fileModels.map(async model => { - - // Revert through model - await model.revert(options); - - // If model is still dirty, mark the resulting operation as error - if (model.isDirty()) { - const result = mapResourceToResult.get(model.resource); - if (result) { - result.error = true; - } - } - })); - - return { results: mapResourceToResult.values() }; + return false; } //#endregion diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index fdff42bff3..ff06385046 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -110,11 +110,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private registerListeners(): void { - this._register(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); this._register(this.filesConfigurationService.onFilesAssociationChange(e => this.onFilesAssociationChange())); } - private async onFileChanges(e: FileChangesEvent): Promise { + private async onDidFilesChange(e: FileChangesEvent): Promise { let fileEventImpactsModel = false; let newInOrphanModeGuess: boolean | undefined; @@ -616,7 +616,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.textEditorModel.pushStackElement(); } - const saveParticipantCancellation = new CancellationTokenSource(); + const saveCancellation = new CancellationTokenSource(); return (this.saveSequentializer as TaskSequentializer).setPending(versionId, (async () => { // {{SQL CARBON EDIT}} strict-null-checks @@ -625,14 +625,26 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // In addition we update our version right after in case it changed because of a model change // // Save participants can also be skipped through API. - if (this.isResolved() && this.textFileService.saveParticipant && !options.skipSaveParticipants) { + if (this.isResolved() && !options.skipSaveParticipants) { try { - await this.textFileService.saveParticipant.participate(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveParticipantCancellation.token); + await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); } catch (error) { - // Ignore + this.logService.error(`[text file model] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString()); } } + // It is possible that a subsequent save is cancelling this + // running save. As such we return early when we detect that + // However, we do not pass the token into the file service + // because that is an atomic operation currently without + // cancellation support, so we dispose the cancellation if + // it was not cancelled yet. + if (saveCancellation.token.isCancellationRequested) { + return; + } else { + saveCancellation.dispose(); + } + // We have to protect against being disposed at this point. It could be that the save() operation // was triggerd followed by a dispose() operation right after without waiting. Typically we cannot // be disposed if we are dirty, but if we are not dirty, save() and dispose() can still be triggered @@ -687,7 +699,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.handleSaveError(error, versionId, options); } })()); - })(), () => saveParticipantCancellation.cancel()); + })(), () => saveCancellation.cancel()); } private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveOptions): void { @@ -726,7 +738,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Show to user - this.textFileService.saveErrorHandler.onSaveError(error, this); + this.textFileService.files.saveErrorHandler.onSaveError(error, this); // Emit as event this._onDidSaveError.fire(); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 12b47bc0c8..4794fd7d9c 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -3,21 +3,35 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ITextFileEditorModel, ITextFileEditorModelManager, IModelLoadOrCreateOptions, ITextFileModelLoadEvent, ITextFileModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel, ITextFileEditorModelManager, IModelLoadOrCreateOptions, ITextFileModelLoadEvent, ITextFileModelSaveEvent, ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; -import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files'; +import { IFileService, FileChangesEvent, FileOperation } from 'vs/platform/files/common/files'; import { distinct, coalesce } from 'vs/base/common/arrays'; import { ResourceQueue } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { TextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textFileSaveParticipant'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { ITextSnapshot, ITextBufferFactory } from 'vs/editor/common/model'; +import { joinPath, isEqualOrParent, isEqual } from 'vs/base/common/resources'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { + private readonly _onDidCreate = this._register(new Emitter()); + readonly onDidCreate = this._onDidCreate.event; + private readonly _onDidLoad = this._register(new Emitter()); readonly onDidLoad = this._onDidLoad.event; @@ -36,8 +50,15 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE private readonly _onDidChangeEncoding = this._register(new Emitter()); readonly onDidChangeEncoding = this._onDidChangeEncoding.event; - private readonly _onDidChangeOrphaned = this._register(new Emitter()); - readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; + saveErrorHandler = (() => { + const notificationService = this.notificationService; + + return { + onSaveError(error: Error, model: ITextFileEditorModel): void { + notificationService.error(localize('genericSaveError', "Failed to save '{0}': {1}", model.name, toErrorMessage(error, false))); + } + }; + })(); private readonly mapResourceToModel = new ResourceMap(); private readonly mapResourceToModelListeners = new ResourceMap(); @@ -49,7 +70,9 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE constructor( @ILifecycleService private readonly lifecycleService: ILifecycleService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IFileService private readonly fileService: IFileService + @IFileService private readonly fileService: IFileService, + @INotificationService private readonly notificationService: INotificationService, + @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService ) { super(); @@ -59,13 +82,18 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE private registerListeners(): void { // Update models from file change events - this._register(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + + // Working copy operations + this._register(this.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => this.onWillRunWorkingCopyFileOperation(e))); + this._register(this.workingCopyFileService.onDidFailWorkingCopyFileOperation(e => this.onDidFailWorkingCopyFileOperation(e))); + this._register(this.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => this.onDidRunWorkingCopyFileOperation(e))); // Lifecycle this.lifecycleService.onShutdown(this.dispose, this); } - private onFileChanges(e: FileChangesEvent): void { + private onDidFilesChange(e: FileChangesEvent): void { // Collect distinct (saved) models to update. // @@ -94,6 +122,116 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } + private readonly mapCorrelationIdToModelsToRestore = new Map(); + + private onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { + + // Move / Copy: remember models to restore after the operation + const source = e.source; + if (source && (e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) { + + // find all models that related to either source or target (can be many if resource is a folder) + const sourceModels: ITextFileEditorModel[] = []; + const targetModels: ITextFileEditorModel[] = []; + for (const model of this.getAll()) { + const resource = model.resource; + + if (isEqualOrParent(resource, e.target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)) { + targetModels.push(model); + } + + if (isEqualOrParent(resource, source)) { + sourceModels.push(model); + } + } + + // remember each source model to load again after move is done + // with optional content to restore if it was dirty + const modelsToRestore: { source: URI, target: URI, snapshot?: ITextSnapshot; mode?: string; encoding?: string; }[] = []; + for (const sourceModel of sourceModels) { + const sourceModelResource = sourceModel.resource; + + // If the source is the actual model, just use target as new resource + let targetModelResource: URI; + if (isEqual(sourceModelResource, e.source)) { + targetModelResource = e.target; + } + + // Otherwise a parent folder of the source is being moved, so we need + // to compute the target resource based on that + else { + targetModelResource = joinPath(e.target, sourceModelResource.path.substr(source.path.length + 1)); + } + + modelsToRestore.push({ + source: sourceModelResource, + target: targetModelResource, + mode: sourceModel.getMode(), + encoding: sourceModel.getEncoding(), + snapshot: sourceModel.isDirty() ? sourceModel.createSnapshot() : undefined + }); + } + + this.mapCorrelationIdToModelsToRestore.set(e.correlationId, modelsToRestore); + } + } + + private onDidFailWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { + + // Move / Copy: restore dirty flag on models to restore that were dirty + if ((e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) { + const modelsToRestore = this.mapCorrelationIdToModelsToRestore.get(e.correlationId); + if (modelsToRestore) { + this.mapCorrelationIdToModelsToRestore.delete(e.correlationId); + + modelsToRestore.forEach(model => { + // snapshot presence means this model used to be dirty + if (model.snapshot) { + this.get(model.source)?.setDirty(true); + } + }); + } + } + } + + private onDidRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { + + // Move / Copy: restore models that were loaded before the operation took place + if ((e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) { + e.waitUntil((async () => { + const modelsToRestore = this.mapCorrelationIdToModelsToRestore.get(e.correlationId); + if (modelsToRestore) { + this.mapCorrelationIdToModelsToRestore.delete(e.correlationId); + + await Promise.all(modelsToRestore.map(async modelToRestore => { + + // restore the model, forcing a reload. this is important because + // we know the file has changed on disk after the move and the + // model might have still existed with the previous state. this + // ensures we are not tracking a stale state. + const restoredModel = await this.resolve(modelToRestore.target, { reload: { async: false }, encoding: modelToRestore.encoding }); + + // restore previous dirty content if any and ensure to mark the model as dirty + let textBufferFactory: ITextBufferFactory | undefined = undefined; + if (modelToRestore.snapshot) { + textBufferFactory = createTextBufferFactoryFromSnapshot(modelToRestore.snapshot); + } + + // restore previous mode only if the mode is now unspecified + let preferredMode: string | undefined = undefined; + if (restoredModel.getMode() === PLAINTEXT_MODE_ID && modelToRestore.mode !== PLAINTEXT_MODE_ID) { + preferredMode = modelToRestore.mode; + } + + if (textBufferFactory || preferredMode) { + restoredModel.updateTextEditorModel(textBufferFactory, preferredMode); + } + })); + } + })()); + } + } + get(resource: URI): ITextFileEditorModel | undefined { return this.mapResourceToModel.get(resource); } @@ -107,9 +245,10 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } let modelPromise: Promise; + let model = this.get(resource); + let didCreateModel = false; // Model exists - let model = this.get(resource); if (model) { if (options?.reload) { @@ -130,6 +269,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Model does not exist else { + didCreateModel = true; + const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined, options ? options.mode : undefined); modelPromise = model.load(options); @@ -141,7 +282,6 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE listeners.add(model.onDidSave(reason => this._onDidSave.fire({ model: newModel, reason }))); listeners.add(model.onDidRevert(() => this._onDidRevert.fire(newModel))); listeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(newModel))); - listeners.add(model.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire(newModel))); this.mapResourceToModelListeners.set(resource, listeners); } @@ -149,17 +289,17 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Store pending loads to avoid race conditions this.mapResourceToPendingModelLoaders.set(resource, modelPromise); + // Signal as event if we created the model + if (didCreateModel) { + this._onDidCreate.fire(model); + } + try { const resolvedModel = await modelPromise; // Make known to manager (if not already known) this.add(resource, resolvedModel); - // Model can be dirty if a backup was restored, so we make sure to have this event delivered - if (resolvedModel.isDirty()) { - this._onDidChangeDirty.fire(resolvedModel); - } - // Remove from pending loads this.mapResourceToPendingModelLoaders.delete(resource); @@ -168,6 +308,11 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE resolvedModel.setMode(options.mode); } + // Model can be dirty if a backup was restored, so we make sure to have this event delivered + if (resolvedModel.isDirty()) { + this._onDidChangeDirty.fire(resolvedModel); + } + return resolvedModel; } catch (error) { @@ -227,6 +372,20 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } + //#region Save participants + + private readonly saveParticipants = this._register(this.instantiationService.createInstance(TextFileSaveParticipant)); + + addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable { + return this.saveParticipants.addSaveParticipant(participant); + } + + runSaveParticipants(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise { + return this.saveParticipants.participate(model, context, token); + } + + //#endregion + clear(): void { // model caches diff --git a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts new file mode 100644 index 0000000000..439dfcf457 --- /dev/null +++ b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { raceCancellation } from 'vs/base/common/async'; +import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; +import { localize } from 'vs/nls'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; +import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; + +export class TextFileSaveParticipant extends Disposable { + + private readonly saveParticipants: ITextFileSaveParticipant[] = []; + + constructor( + @IProgressService private readonly progressService: IProgressService, + @ILogService private readonly logService: ILogService + ) { + super(); + } + + addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable { + this.saveParticipants.push(participant); + + return toDisposable(() => this.saveParticipants.splice(this.saveParticipants.indexOf(participant), 1)); + } + + participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise { + const cts = new CancellationTokenSource(token); + + return this.progressService.withProgress({ + title: localize('saveParticipants', "Running Save Participants for '{0}'", model.name), + location: ProgressLocation.Notification, + cancellable: true, + delay: model.isDirty() ? 3000 : 5000 + }, async progress => { + + // undoStop before participation + model.textEditorModel.pushStackElement(); + + for (const saveParticipant of this.saveParticipants) { + if (cts.token.isCancellationRequested) { + break; + } + + try { + const promise = saveParticipant.participate(model, context, progress, cts.token); + await raceCancellation(promise, cts.token); + } catch (err) { + this.logService.warn(err); + } + } + + // undoStop after participation + model.textEditorModel.pushStackElement(); + }, () => { + // user cancel + cts.dispose(true); + }); + } + + dispose(): void { + this.saveParticipants.splice(0, this.saveParticipants.length); + } +} diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index cbc64b6f05..8f0b069bfa 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -7,7 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { Event, IWaitUntil } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IEncodingSupport, IModeSupport, ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; -import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult, FileOperation } from 'vs/platform/files/common/files'; +import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { ITextBufferFactory, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; @@ -17,23 +17,18 @@ import { isNative } from 'vs/base/common/platform'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; export const ITextFileService = createDecorator('textFileService'); +export interface TextFileCreateEvent extends IWaitUntil { + readonly resource: URI; +} + export interface ITextFileService extends IDisposable { _serviceBrand: undefined; - /** - * An event that is fired before attempting a certain file operation. - */ - readonly onWillRunOperation: Event; - - /** - * An event that is fired after a file operation has been performed. - */ - readonly onDidRunOperation: Event; - /** * Access to the manager of text file editor models providing further * methods to work with them. @@ -51,17 +46,6 @@ export interface ITextFileService extends IDisposable { */ readonly encoding: IResourceEncodings; - /** - * The handler that should be called when saving fails. Can be overridden - * to handle save errors in a custom way. - */ - saveErrorHandler: ISaveErrorHandler; - - /** - * The save participant if any. By default, no save participant is registered. - */ - saveParticipant: ISaveParticipant | undefined; - /** * A resource is dirty if it has unsaved changes or is an untitled file not yet saved. * @@ -111,41 +95,21 @@ export interface ITextFileService extends IDisposable { */ write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise; + /** + * An event that is fired before attempting to create a text file. + */ + readonly onWillCreateTextFile: Event; + + /** + * An event that is fired after a text file has been created. + */ + readonly onDidCreateTextFile: Event; + /** * Create a file. If the file exists it will be overwritten with the contents if * the options enable to overwrite. */ create(resource: URI, contents?: string | ITextSnapshot, options?: { overwrite?: boolean }): Promise; - - /** - * Move a file. If the file is dirty, its contents will be preserved and restored. - */ - move(source: URI, target: URI, overwrite?: boolean): Promise; - - /** - * Copy a file. If the file is dirty, its contents will be preserved and restored. - */ - copy(source: URI, target: URI, overwrite?: boolean): Promise; - - /** - * Delete a file. If the file is dirty, it will get reverted and then deleted from disk. - */ - delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; -} - -export interface FileOperationWillRunEvent extends IWaitUntil { - operation: FileOperation; - target: URI; - source?: URI; -} - -export class FileOperationDidRunEvent { - - constructor( - readonly operation: FileOperation, - readonly target: URI, - readonly source?: URI | undefined - ) { } } export interface IReadTextFileOptions extends IReadFileOptions { @@ -226,14 +190,6 @@ export interface ISaveErrorHandler { onSaveError(error: Error, model: ITextFileEditorModel): void; } -export interface ISaveParticipant { - - /** - * Participate in a save of a model. Allows to change the model before it is being saved to disk. - */ - participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason }, token: CancellationToken): Promise; -} - /** * States the text file editor model can be in. */ @@ -357,21 +313,40 @@ export interface ITextFileModelLoadEvent { reason: LoadReason; } +export interface ITextFileSaveParticipant { + + /** + * Participate in a save of a model. Allows to change the model + * before it is being saved to disk. + */ + participate( + model: IResolvedTextFileEditorModel, + context: { reason: SaveReason }, + progress: IProgress, + token: CancellationToken + ): Promise; +} + export interface ITextFileEditorModelManager { + readonly onDidCreate: Event; readonly onDidLoad: Event; readonly onDidChangeDirty: Event; readonly onDidSaveError: Event; readonly onDidSave: Event; readonly onDidRevert: Event; readonly onDidChangeEncoding: Event; - readonly onDidChangeOrphaned: Event; + + saveErrorHandler: ISaveErrorHandler; get(resource: URI): ITextFileEditorModel | undefined; getAll(): ITextFileEditorModel[]; resolve(resource: URI, options?: IModelLoadOrCreateOptions): Promise; + addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable; + runSaveParticipants(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise + disposeModel(model: ITextFileEditorModel): void; } @@ -415,6 +390,8 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport updatePreferredEncoding(encoding: string | undefined): void; + updateTextEditorModel(newValue?: ITextBufferFactory, preferredMode?: string): void; + save(options?: ITextFileSaveOptions): Promise; load(options?: ILoadOptions): Promise; diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index fa8242712c..d1c5cf1307 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -37,7 +37,6 @@ import { assign } from 'vs/base/common/objects'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; export class NativeTextFileService extends AbstractTextFileService { @@ -56,10 +55,9 @@ export class NativeTextFileService extends AbstractTextFileService { @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @ITextModelService textModelService: ITextModelService, @ICodeEditorService codeEditorService: ICodeEditorService, - @INotificationService notificationService: INotificationService, @IRemotePathService remotePathService: IRemotePathService ) { - super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, notificationService, remotePathService); + super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, remotePathService); } private _encoding: EncodingOracle | undefined; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index db989be8f0..95764b835f 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -502,55 +502,98 @@ suite('Files - TextFileEditorModel', () => { eventCounter++; }); - accessor.textFileService.saveParticipant = { + const participant = accessor.textFileService.files.addSaveParticipant({ participate: async model => { assert.ok(model.isDirty()); model.textEditorModel!.setValue('bar'); assert.ok(model.isDirty()); eventCounter++; } - }; + }); await model.load(); model.textEditorModel!.setValue('foo'); await model.save(); - model.dispose(); assert.equal(eventCounter, 2); + + participant.dispose(); + model.textEditorModel!.setValue('bar'); + + await model.save(); + assert.equal(eventCounter, 3); + + model.dispose(); + }); + + test('Save Participant - skip', async function () { + let eventCounter = 0; + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async model => { + eventCounter++; + } + }); + + await model.load(); + model.textEditorModel!.setValue('foo'); + + await model.save({ skipSaveParticipants: true }); + assert.equal(eventCounter, 0); + + participant.dispose(); + model.dispose(); }); test('Save Participant, async participant', async function () { + let eventCounter = 0; const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - accessor.textFileService.saveParticipant = { - participate: (model) => { + model.onDidSave(e => { + assert.ok(!model.isDirty()); + eventCounter++; + }); + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: model => { + assert.ok(model.isDirty()); + model.textEditorModel!.setValue('bar'); + assert.ok(model.isDirty()); + eventCounter++; + return timeout(10); } - }; + }); await model.load(); model.textEditorModel!.setValue('foo'); const now = Date.now(); await model.save(); + assert.equal(eventCounter, 2); assert.ok(Date.now() - now >= 10); + model.dispose(); + participant.dispose(); }); test('Save Participant, bad participant', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - accessor.textFileService.saveParticipant = { + const participant = accessor.textFileService.files.addSaveParticipant({ participate: async model => { new Error('boom'); } - }; + }); await model.load(); model.textEditorModel!.setValue('foo'); await model.save(); + model.dispose(); + participant.dispose(); }); test('Save Participant, participant cancelled when saved again', async function () { @@ -558,12 +601,15 @@ suite('Files - TextFileEditorModel', () => { let participations: boolean[] = []; - accessor.textFileService.saveParticipant = { - participate: async model => { + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async (model, context, progress, token) => { await timeout(10); - participations.push(true); + + if (!token.isCancellationRequested) { + participations.push(true); + } } - }; + }); await model.load(); @@ -574,12 +620,16 @@ suite('Files - TextFileEditorModel', () => { const p2 = model.save(); model.textEditorModel!.setValue('foo 2'); - await model.save(); + const p3 = model.save(); - await p1; - await p2; + model.textEditorModel!.setValue('foo 3'); + const p4 = model.save(); + + await Promise.all([p1, p2, p3, p4]); assert.equal(participations.length, 1); + model.dispose(); + participant.dispose(); }); test('Save Participant, calling save from within is unsupported but does not explode (sync save)', async function () { @@ -602,7 +652,7 @@ suite('Files - TextFileEditorModel', () => { let savePromise: Promise; let breakLoop = false; - accessor.textFileService.saveParticipant = { + const participant = accessor.textFileService.files.addSaveParticipant({ participate: async model => { if (breakLoop) { return; @@ -618,12 +668,14 @@ suite('Files - TextFileEditorModel', () => { // assert that this is the same promise as the outer one assert.equal(savePromise, newSavePromise); } - }; + }); await model.load(); model.textEditorModel!.setValue('foo'); savePromise = model.save(); await savePromise; + + participant.dispose(); } }); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts index 9f92353c23..d426d9af5d 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -13,6 +13,7 @@ import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/file import { IModelService } from 'vs/editor/common/services/modelService'; import { toResource } from 'vs/base/test/common/utils'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; class ServiceAccessor { constructor( @@ -92,6 +93,11 @@ suite('Files - TextFileEditorModelManager', () => { const resource = URI.file('/test.html'); const encoding = 'utf8'; + const events: ITextFileEditorModel[] = []; + const listener = manager.onDidCreate(model => { + events.push(model); + }); + const model = await manager.resolve(resource, { encoding }); assert.ok(model); assert.equal(model.getEncoding(), encoding); @@ -105,6 +111,12 @@ suite('Files - TextFileEditorModelManager', () => { assert.notEqual(model3, model2); assert.equal(manager.get(resource), model3); model3.dispose(); + + assert.equal(events.length, 2); + assert.equal(events[0].resource.toString(), model.resource.toString()); + assert.equal(events[1].resource.toString(), model2.resource.toString()); + + listener.dispose(); }); test('removed from cache when model disposed', function () { diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index 843b74148d..f85beabf8e 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -2,8 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; -import { URI } from 'vs/base/common/uri'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { TestLifecycleService, TestContextService, TestFileService, TestFilesConfigurationService, TestFileDialogService, TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; @@ -17,11 +17,13 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; class ServiceAccessor { constructor( @ILifecycleService public lifecycleService: TestLifecycleService, @ITextFileService public textFileService: TestTextFileService, + @IWorkingCopyFileService public workingCopyFileService: IWorkingCopyFileService, @IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService, @IWorkspaceContextService public contextService: TestContextService, @IModelService public modelService: ModelServiceImpl, @@ -43,9 +45,7 @@ suite('Files - TextFileService', () => { }); teardown(() => { - if (model) { - model.dispose(); - } + model?.dispose(); (accessor.textFileService.files).dispose(); }); @@ -133,71 +133,21 @@ suite('Files - TextFileService', () => { model!.textEditorModel!.setValue('foo'); assert.ok(accessor.textFileService.isDirty(model.resource)); + let eventCounter = 0; + + accessor.textFileService.onWillCreateTextFile(e => { + assert.equal(e.resource.toString(), model.resource.toString()); + eventCounter++; + }); + + accessor.textFileService.onDidCreateTextFile(e => { + assert.equal(e.resource.toString(), model.resource.toString()); + eventCounter++; + }); await accessor.textFileService.create(model.resource, 'Foo'); assert.ok(!accessor.textFileService.isDirty(model.resource)); + + assert.equal(eventCounter, 2); }); - - test('delete - dirty file', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.files).add(model.resource, model); - - await model.load(); - model!.textEditorModel!.setValue('foo'); - assert.ok(accessor.textFileService.isDirty(model.resource)); - - await accessor.textFileService.delete(model.resource); - assert.ok(!accessor.textFileService.isDirty(model.resource)); - }); - - test('move - dirty file', async function () { - await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), true); - }); - - test('move - dirty file (target exists and is dirty)', async function () { - await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), true, true); - }); - - test('copy - dirty file', async function () { - await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), false); - }); - - test('copy - dirty file (target exists and is dirty)', async function () { - await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), false, true); - }); - - async function testMoveOrCopy(source: URI, target: URI, move: boolean, targetDirty?: boolean): Promise { - let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined); - let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined); - (accessor.textFileService.files).add(sourceModel.resource, sourceModel); - (accessor.textFileService.files).add(targetModel.resource, targetModel); - - await sourceModel.load(); - sourceModel.textEditorModel!.setValue('foo'); - assert.ok(accessor.textFileService.isDirty(sourceModel.resource)); - - if (targetDirty) { - await targetModel.load(); - targetModel.textEditorModel!.setValue('bar'); - assert.ok(accessor.textFileService.isDirty(targetModel.resource)); - } - - if (move) { - await accessor.textFileService.move(sourceModel.resource, targetModel.resource, true); - } else { - await accessor.textFileService.copy(sourceModel.resource, targetModel.resource, true); - } - - assert.equal(targetModel.textEditorModel!.getValue(), 'foo'); - - if (move) { - assert.ok(!accessor.textFileService.isDirty(sourceModel.resource)); - } else { - assert.ok(accessor.textFileService.isDirty(sourceModel.resource)); - } - assert.ok(accessor.textFileService.isDirty(targetModel.resource)); - - sourceModel.dispose(); - targetModel.dispose(); - } }); diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index a2337d9447..e82d51fcb9 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -115,7 +115,7 @@ suite('Workbench - TextModelResolverService', () => { const input = instantiationService.createInstance(UntitledTextEditorInput, untitledModel); await input.resolve(); - const ref = await accessor.textModelResolverService.createModelReference(input.getResource()); + const ref = await accessor.textModelResolverService.createModelReference(input.resource); const model = ref.object; assert.equal(untitledModel, model); const editorModel = model.textEditorModel; diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 6878a0786e..1715f504e9 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -254,7 +254,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } }); - this.fileService.onFileChanges(async e => { + this.fileService.onDidFilesChange(async e => { if (this.watchedColorThemeLocation && this.currentColorTheme && e.contains(this.watchedColorThemeLocation, FileChangeType.UPDATED)) { this.reloadCurrentColorTheme(); } diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 0a6a9ecaa7..4ce72561cd 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -51,26 +51,26 @@ suite('Untitled text editors', () => { const input1 = instantiationService.createInstance(UntitledTextEditorInput, service.create()); await input1.resolve(); - assert.equal(service.get(input1.getResource()), input1.model); + assert.equal(service.get(input1.resource), input1.model); - assert.ok(service.get(input1.getResource())); + assert.ok(service.get(input1.resource)); assert.ok(!service.get(URI.file('testing'))); const input2 = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - assert.equal(service.get(input2.getResource()), input2.model); + assert.equal(service.get(input2.resource), input2.model); // get() - assert.equal(service.get(input1.getResource()), input1.model); - assert.equal(service.get(input2.getResource()), input2.model); + assert.equal(service.get(input1.resource), input1.model); + assert.equal(service.get(input2.resource), input2.model); // revert() await input1.revert(0); assert.ok(input1.isDisposed()); - assert.ok(!service.get(input1.getResource())); + assert.ok(!service.get(input1.resource)); // dirty const model = await input2.resolve(); - assert.equal(await service.resolve({ untitledResource: input2.getResource() }), model); + assert.equal(await service.resolve({ untitledResource: input2.resource }), model); assert.ok(service.get(model.resource)); assert.ok(!input2.isDirty()); @@ -81,29 +81,29 @@ suite('Untitled text editors', () => { const resource = await resourcePromise; - assert.equal(resource.toString(), input2.getResource().toString()); + assert.equal(resource.toString(), input2.resource.toString()); assert.ok(input2.isDirty()); - assert.ok(workingCopyService.isDirty(input2.getResource())); + assert.ok(workingCopyService.isDirty(input2.resource)); assert.equal(workingCopyService.dirtyCount, 1); await input1.revert(0); await input2.revert(0); - assert.ok(!service.get(input1.getResource())); - assert.ok(!service.get(input2.getResource())); + assert.ok(!service.get(input1.resource)); + assert.ok(!service.get(input2.resource)); assert.ok(!input2.isDirty()); assert.ok(!model.isDirty()); - assert.ok(!workingCopyService.isDirty(input2.getResource())); + assert.ok(!workingCopyService.isDirty(input2.resource)); assert.equal(workingCopyService.dirtyCount, 0); assert.equal(await input1.revert(0), false); assert.ok(input1.isDisposed()); - assert.ok(!service.get(input1.getResource())); + assert.ok(!service.get(input1.resource)); input2.dispose(); - assert.ok(!service.get(input2.getResource())); + assert.ok(!service.get(input2.resource)); }); function awaitDidChangeDirty(service: IUntitledTextEditorService): Promise { @@ -176,9 +176,9 @@ suite('Untitled text editors', () => { const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - const model3 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ untitledResource: input.getResource() })).resolve(); + const model3 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ untitledResource: input.resource })).resolve(); - assert.equal(model3.resource.toString(), input.getResource().toString()); + assert.equal(model3.resource.toString(), input.resource.toString()); const file = URI.file(join('C:\\', '/foo/file44.txt')); const model4 = await instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file })).resolve(); @@ -330,7 +330,7 @@ suite('Untitled text editors', () => { service.onDidChangeEncoding(r => { counter++; - assert.equal(r.toString(), input.getResource().toString()); + assert.equal(r.toString(), input.resource.toString()); }); // encoding @@ -349,7 +349,7 @@ suite('Untitled text editors', () => { service.onDidChangeLabel(r => { counter++; - assert.equal(r.toString(), input.getResource().toString()); + assert.equal(r.toString(), input.resource.toString()); }); // label @@ -368,7 +368,7 @@ suite('Untitled text editors', () => { service.onDidDisposeModel(r => { counter++; - assert.equal(r.toString(), input.getResource().toString()); + assert.equal(r.toString(), input.resource.toString()); }); const model = await input.resolve(); diff --git a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts index 49f6f6a802..b4007a7297 100644 --- a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts +++ b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts @@ -348,7 +348,7 @@ suite('FileUserDataProvider - Watching', () => { test('file added change event', done => { const expected = joinPath(userDataResource, 'settings.json'); const target = joinPath(localUserDataResource, 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.ADDED)) { done(); } @@ -362,7 +362,7 @@ suite('FileUserDataProvider - Watching', () => { test('file updated change event', done => { const expected = joinPath(userDataResource, 'settings.json'); const target = joinPath(localUserDataResource, 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.UPDATED)) { done(); } @@ -376,7 +376,7 @@ suite('FileUserDataProvider - Watching', () => { test('file deleted change event', done => { const expected = joinPath(userDataResource, 'settings.json'); const target = joinPath(localUserDataResource, 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.DELETED)) { done(); } @@ -390,7 +390,7 @@ suite('FileUserDataProvider - Watching', () => { test('file under folder created change event', done => { const expected = joinPath(userDataResource, 'snippets', 'settings.json'); const target = joinPath(localUserDataResource, 'snippets', 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.ADDED)) { done(); } @@ -404,7 +404,7 @@ suite('FileUserDataProvider - Watching', () => { test('file under folder updated change event', done => { const expected = joinPath(userDataResource, 'snippets', 'settings.json'); const target = joinPath(localUserDataResource, 'snippets', 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.UPDATED)) { done(); } @@ -418,7 +418,7 @@ suite('FileUserDataProvider - Watching', () => { test('file under folder deleted change event', done => { const expected = joinPath(userDataResource, 'snippets', 'settings.json'); const target = joinPath(localUserDataResource, 'snippets', 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.DELETED)) { done(); } @@ -432,7 +432,7 @@ suite('FileUserDataProvider - Watching', () => { test('event is not triggered if file is not under user data', async () => { const target = joinPath(dirname(localUserDataResource), 'settings.json'); let triggered = false; - testObject.onFileChanges(() => triggered = true); + testObject.onDidFilesChange(() => triggered = true); fileEventEmitter.fire([{ resource: target, type: FileChangeType.DELETED @@ -446,7 +446,7 @@ suite('FileUserDataProvider - Watching', () => { test('backup file created change event', done => { const expected = joinPath(userDataResource, BACKUPS, 'settings.json'); const target = joinPath(localBackupsResource, 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.ADDED)) { done(); } @@ -460,7 +460,7 @@ suite('FileUserDataProvider - Watching', () => { test('backup file update change event', done => { const expected = joinPath(userDataResource, BACKUPS, 'settings.json'); const target = joinPath(localBackupsResource, 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.UPDATED)) { done(); } @@ -474,7 +474,7 @@ suite('FileUserDataProvider - Watching', () => { test('backup file delete change event', done => { const expected = joinPath(userDataResource, BACKUPS, 'settings.json'); const target = joinPath(localBackupsResource, 'settings.json'); - testObject.onFileChanges(e => { + testObject.onDidFilesChange(e => { if (e.contains(expected, FileChangeType.DELETED)) { done(); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts index 2d04540e6a..454933e372 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts @@ -18,11 +18,15 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut private _onDidChangeToken: Emitter = this._register(new Emitter()); readonly onDidChangeToken: Event = this._onDidChangeToken.event; + private _onTokenFailed: Emitter = this._register(new Emitter()); + readonly onTokenFailed: Event = this._onTokenFailed.event; + constructor( @ISharedProcessService sharedProcessService: ISharedProcessService, ) { super(); this.channel = sharedProcessService.getChannel('authToken'); + this._register(this.channel.listen('onTokenFailed')(_ => this.sendTokenFailed())); } getToken(): Promise { @@ -32,6 +36,10 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut setToken(token: string | undefined): Promise { return this.channel.call('setToken', token); } + + sendTokenFailed(): void { + this._onTokenFailed.fire(); + } } registerSingleton(IUserDataAuthTokenService, UserDataAuthTokenService); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts new file mode 100644 index 0000000000..181293caca --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Event, AsyncEmitter, IWaitUntil } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IFileService, FileOperation, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { isEqualOrParent, isEqual } from 'vs/base/common/resources'; + +export const IWorkingCopyFileService = createDecorator('workingCopyFileService'); + +export interface WorkingCopyFileEvent extends IWaitUntil { + + /** + * An identifier to correlate the operation through the + * different event types (before, after, error). + */ + readonly correlationId: number; + + /** + * The file operation that is taking place. + */ + readonly operation: FileOperation; + + /** + * The resource the event is about. + */ + readonly target: URI; + + /** + * A property that is defined for move operations. + */ + readonly source?: URI; +} + +/** + * A service that allows to perform file operations with working copy support. + * Any operation that would leave a stale dirty working copy behind will make + * sure to revert the working copy first. + * + * On top of that events are provided to participate in each state of the + * operation to perform additional work. + */ +export interface IWorkingCopyFileService { + + _serviceBrand: undefined; + + //#region Events + + /** + * An event that is fired before attempting a certain working copy IO operation. + * + * Participants can join this event with a long running operation to make changes + * to the working copy before the operation starts. + */ + readonly onBeforeWorkingCopyFileOperation: Event; + + /** + * An event that is fired when a certain working copy IO operation is about to run. + * + * Participants can join this event with a long running operation to keep some state + * before the operation is started, but working copies should not be changed at this + * point in time. + */ + readonly onWillRunWorkingCopyFileOperation: Event; + + /** + * An event that is fired after a working copy IO operation has failed. + * + * Participants can join this event with a long running operation to clean up as needed. + */ + readonly onDidFailWorkingCopyFileOperation: Event; + + /** + * An event that is fired after a working copy IO operation has been performed. + * + * Participants can join this event with a long running operation to make changes + * after the operation has finished. + */ + readonly onDidRunWorkingCopyFileOperation: Event; + + //#endregion + + + //#region File operations + + /** + * Will move working copies matching the provided resource and children + * to the target resource using the associated file service for that resource. + * + * Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and + * `onDidRunWorkingCopyFileOperation` events to participate. + */ + move(source: URI, target: URI, overwrite?: boolean): Promise; + + /** + * Will copy working copies matching the provided resource and children + * to the target using the associated file service for that resource. + * + * Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and + * `onDidRunWorkingCopyFileOperation` events to participate. + */ + copy(source: URI, target: URI, overwrite?: boolean): Promise; + + /** + * Will delete working copies matching the provided resource and children + * using the associated file service for that resource. + * + * Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and + * `onDidRunWorkingCopyFileOperation` events to participate. + */ + delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; + + //#endregion +} + +export class WorkingCopyFileService extends Disposable implements IWorkingCopyFileService { + + _serviceBrand: undefined; + + //#region Events + + private readonly _onBeforeWorkingCopyFileOperation = this._register(new AsyncEmitter()); + readonly onBeforeWorkingCopyFileOperation = this._onBeforeWorkingCopyFileOperation.event; + + private readonly _onWillRunWorkingCopyFileOperation = this._register(new AsyncEmitter()); + readonly onWillRunWorkingCopyFileOperation = this._onWillRunWorkingCopyFileOperation.event; + + private readonly _onDidFailWorkingCopyFileOperation = this._register(new AsyncEmitter()); + readonly onDidFailWorkingCopyFileOperation = this._onDidFailWorkingCopyFileOperation.event; + + private readonly _onDidRunWorkingCopyFileOperation = this._register(new AsyncEmitter()); + readonly onDidRunWorkingCopyFileOperation = this._onDidRunWorkingCopyFileOperation.event; + + //#endregion + + private correlationIds = 0; + + constructor( + @IFileService private fileService: IFileService, + @IWorkingCopyService private workingCopyService: IWorkingCopyService + ) { + super(); + } + + async move(source: URI, target: URI, overwrite?: boolean): Promise { + return this.moveOrCopy(source, target, true, overwrite); + } + + async copy(source: URI, target: URI, overwrite?: boolean): Promise { + return this.moveOrCopy(source, target, false, overwrite); + } + + private async moveOrCopy(source: URI, target: URI, move: boolean, overwrite?: boolean): Promise { + const event = { correlationId: this.correlationIds++, operation: move ? FileOperation.MOVE : FileOperation.COPY, target, source }; + + // before events + await this._onBeforeWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + + // handle dirty working copies depending on the operation: + // - move: revert both source and target (if any) + // - copy: revert target (if any) + const dirtyWorkingCopies = (move ? [...this.getDirtyWorkingCopies(source), ...this.getDirtyWorkingCopies(target)] : this.getDirtyWorkingCopies(target)); + await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true }))); + + // now we can rename the source to target via file operation + let stat: IFileStatWithMetadata; + try { + if (move) { + stat = await this.fileService.move(source, target, overwrite); + } else { + stat = await this.fileService.copy(source, target, overwrite); + } + } catch (error) { + + // error event + await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + + throw error; + } + + // after event + await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + + return stat; + } + + async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise { + const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, target: resource }; + + // before events + await this._onBeforeWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + + // Check for any existing dirty working copies for the resource + // and do a soft revert before deleting to be able to close + // any opened editor with these working copies + const dirtyWorkingCopies = this.getDirtyWorkingCopies(resource); + await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true }))); + + // Now actually delete from disk + try { + await this.fileService.del(resource, options); + } catch (error) { + + // error event + await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + + throw error; + } + + // after event + await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + } + + private getDirtyWorkingCopies(resource: URI): IWorkingCopy[] { + return this.workingCopyService.dirtyWorkingCopies.filter(dirty => { + if (this.fileService.canHandleResource(resource)) { + // only check for parents if the resource can be handled + // by the file system where we then assume a folder like + // path structure + return isEqualOrParent(dirty.resource, resource); + } + + return isEqual(dirty.resource, resource); + }); + } +} + +registerSingleton(IWorkingCopyFileService, WorkingCopyFileService, true); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts new file mode 100644 index 0000000000..384f751f03 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { toResource } from 'vs/base/test/common/utils'; +import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { URI } from 'vs/base/common/uri'; +import { FileOperation } from 'vs/platform/files/common/files'; + +class ServiceAccessor { + constructor( + @ITextFileService public textFileService: TestTextFileService, + @IWorkingCopyFileService public workingCopyFileService: IWorkingCopyFileService, + @IWorkingCopyService public workingCopyService: IWorkingCopyService + ) { + } +} + +suite('WorkingCopyFileService', () => { + + let instantiationService: IInstantiationService; + let model: TextFileEditorModel; + let accessor: ServiceAccessor; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(ServiceAccessor); + }); + + teardown(() => { + model?.dispose(); + (accessor.textFileService.files).dispose(); + }); + + test('delete - dirty file', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + await model.load(); + model!.textEditorModel!.setValue('foo'); + assert.ok(accessor.workingCopyService.isDirty(model.resource)); + + let eventCounter = 0; + let correlationId: number | undefined = undefined; + + const listener0 = accessor.workingCopyFileService.onBeforeWorkingCopyFileOperation(e => { + assert.equal(e.target.toString(), model.resource.toString()); + assert.equal(e.operation, FileOperation.DELETE); + eventCounter++; + correlationId = e.correlationId; + }); + + const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => { + assert.equal(e.target.toString(), model.resource.toString()); + assert.equal(e.operation, FileOperation.DELETE); + assert.equal(e.correlationId, correlationId); + eventCounter++; + }); + + const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { + assert.equal(e.target.toString(), model.resource.toString()); + assert.equal(e.operation, FileOperation.DELETE); + assert.equal(e.correlationId, correlationId); + eventCounter++; + }); + + await accessor.workingCopyFileService.delete(model.resource); + assert.ok(!accessor.workingCopyService.isDirty(model.resource)); + + assert.equal(eventCounter, 3); + + listener0.dispose(); + listener1.dispose(); + listener2.dispose(); + }); + + test('move - dirty file', async function () { + await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), true); + }); + + test('move - dirty file (target exists and is dirty)', async function () { + await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), true, true); + }); + + test('copy - dirty file', async function () { + await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), false); + }); + + test('copy - dirty file (target exists and is dirty)', async function () { + await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), false, true); + }); + + async function testMoveOrCopy(source: URI, target: URI, move: boolean, targetDirty?: boolean): Promise { + let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined); + let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined); + (accessor.textFileService.files).add(sourceModel.resource, sourceModel); + (accessor.textFileService.files).add(targetModel.resource, targetModel); + + await sourceModel.load(); + sourceModel.textEditorModel!.setValue('foo'); + assert.ok(accessor.textFileService.isDirty(sourceModel.resource)); + + if (targetDirty) { + await targetModel.load(); + targetModel.textEditorModel!.setValue('bar'); + assert.ok(accessor.textFileService.isDirty(targetModel.resource)); + } + + let eventCounter = 0; + let correlationId: number | undefined = undefined; + + const listener0 = accessor.workingCopyFileService.onBeforeWorkingCopyFileOperation(e => { + assert.equal(e.target.toString(), targetModel.resource.toString()); + assert.equal(e.source?.toString(), sourceModel.resource.toString()); + assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY); + eventCounter++; + correlationId = e.correlationId; + }); + + const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => { + assert.equal(e.target.toString(), targetModel.resource.toString()); + assert.equal(e.source?.toString(), sourceModel.resource.toString()); + assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY); + eventCounter++; + assert.equal(e.correlationId, correlationId); + }); + + const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { + assert.equal(e.target.toString(), targetModel.resource.toString()); + assert.equal(e.source?.toString(), sourceModel.resource.toString()); + assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY); + eventCounter++; + assert.equal(e.correlationId, correlationId); + }); + + if (move) { + await accessor.workingCopyFileService.move(sourceModel.resource, targetModel.resource, true); + } else { + await accessor.workingCopyFileService.copy(sourceModel.resource, targetModel.resource, true); + } + + assert.equal(targetModel.textEditorModel!.getValue(), 'foo'); + + if (move) { + assert.ok(!accessor.textFileService.isDirty(sourceModel.resource)); + } else { + assert.ok(accessor.textFileService.isDirty(sourceModel.resource)); + } + assert.ok(accessor.textFileService.isDirty(targetModel.resource)); + + assert.equal(eventCounter, 3); + + sourceModel.dispose(); + targetModel.dispose(); + + listener0.dispose(); + listener1.dispose(); + listener2.dispose(); + } +}); diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts index 326e07e006..f8eaa4c8ea 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts @@ -58,7 +58,7 @@ suite('MainThreadDocumentsAndEditors', () => { const editorGroupService = new TestEditorGroupsService(); const fileService = new class extends mock() { - onAfterOperation = Event.None; + onDidRunOperation = Event.None; }; new MainThreadDocumentsAndEditors( diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index e7e9a770b3..5b78e6290f 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -40,6 +40,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; suite('MainThreadEditors', () => { @@ -79,14 +80,17 @@ suite('MainThreadEditors', () => { services.set(IEditorGroupsService, new TestEditorGroupsService()); services.set(ITextFileService, new class extends mock() { isDirty() { return false; } - create(uri: URI, contents?: string, options?: any) { - createdResources.add(uri); + create(resource: URI) { + createdResources.add(resource); return Promise.resolve(Object.create(null)); } - delete(resource: URI) { - deletedResources.add(resource); - return Promise.resolve(undefined); - } + files = { + onDidSave: Event.None, + onDidRevert: Event.None, + onDidChangeDirty: Event.None + }; + }); + services.set(IWorkingCopyFileService, new class extends mock() { move(source: URI, target: URI) { movedResources.set(source, target); return Promise.resolve(Object.create(null)); @@ -95,11 +99,10 @@ suite('MainThreadEditors', () => { copiedResources.set(source, target); return Promise.resolve(Object.create(null)); } - files = { - onDidSave: Event.None, - onDidRevert: Event.None, - onDidChangeDirty: Event.None - }; + delete(resource: URI) { + deletedResources.add(resource); + return Promise.resolve(undefined); + } }); services.set(ITextModelService, new class extends mock() { createModelReference(resource: URI): Promise> { diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts index 866bcbd7d4..147b0e3813 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts @@ -64,6 +64,9 @@ class MyInputFactory implements IEditorInputFactory { } class MyInput extends EditorInput { + + readonly resource = undefined; + getPreferredEditorId(ids: string[]) { return ids[1]; } @@ -78,6 +81,9 @@ class MyInput extends EditorInput { } class MyOtherInput extends EditorInput { + + readonly resource = undefined; + getTypeId(): string { return ''; } @@ -256,7 +262,7 @@ suite('Workbench base editor', () => { } class TestEditorInput extends EditorInput { - constructor(private resource: URI, private id = 'testEditorInputForMementoTest') { + constructor(public resource: URI, private id = 'testEditorInputForMementoTest') { super(); } getTypeId() { return 'testEditorInputForMementoTest'; } @@ -265,10 +271,6 @@ suite('Workbench base editor', () => { matches(other: TestEditorInput): boolean { return other && this.id === other.id && other instanceof TestEditorInput; } - - getResource(): URI { - return this.resource; - } } const rawMemento = Object.create(null); diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index 2ebf640e48..728e154877 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -20,7 +20,7 @@ class ServiceAccessor { class FileEditorInput extends EditorInput { - constructor(private resource: URI) { + constructor(public resource: URI) { super(); } @@ -28,10 +28,6 @@ class FileEditorInput extends EditorInput { return 'editorResourceFileTest'; } - getResource(): URI { - return this.resource; - } - resolve(): Promise { return Promise.resolve(null); } @@ -58,18 +54,18 @@ suite('Workbench editor', () => { const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - assert.equal(toResource(untitled)!.toString(), untitled.getResource().toString()); - assert.equal(toResource(untitled, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), untitled.getResource().toString()); - assert.equal(toResource(untitled, { filterByScheme: Schemas.untitled })!.toString(), untitled.getResource().toString()); - assert.equal(toResource(untitled, { filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), untitled.getResource().toString()); + assert.equal(toResource(untitled)!.toString(), untitled.resource.toString()); + assert.equal(toResource(untitled, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), untitled.resource.toString()); + assert.equal(toResource(untitled, { filterByScheme: Schemas.untitled })!.toString(), untitled.resource.toString()); + assert.equal(toResource(untitled, { filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), untitled.resource.toString()); assert.ok(!toResource(untitled, { filterByScheme: Schemas.file })); const file = new FileEditorInput(URI.file('/some/path.txt')); - assert.equal(toResource(file)!.toString(), file.getResource().toString()); - assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), file.getResource().toString()); - assert.equal(toResource(file, { filterByScheme: Schemas.file })!.toString(), file.getResource().toString()); - assert.equal(toResource(file, { filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), file.getResource().toString()); + assert.equal(toResource(file)!.toString(), file.resource.toString()); + assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), file.resource.toString()); + assert.equal(toResource(file, { filterByScheme: Schemas.file })!.toString(), file.resource.toString()); + assert.equal(toResource(file, { filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), file.resource.toString()); assert.ok(!toResource(file, { filterByScheme: Schemas.untitled })); const diffEditorInput = new DiffEditorInput('name', 'description', untitled, file); @@ -77,8 +73,8 @@ suite('Workbench editor', () => { assert.ok(!toResource(diffEditorInput)); assert.ok(!toResource(diffEditorInput, { filterByScheme: Schemas.file })); - assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), file.getResource().toString()); - assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: Schemas.file })!.toString(), file.getResource().toString()); - assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), file.getResource().toString()); + assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), file.resource.toString()); + assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: Schemas.file })!.toString(), file.resource.toString()); + assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), file.resource.toString()); }); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts index 3dde0a6fb7..62189b99fb 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -73,6 +73,9 @@ function groupListener(group: EditorGroup): GroupEvents { let index = 0; class TestEditorInput extends EditorInput { + + readonly resource = undefined; + constructor(public id: string) { super(); } @@ -93,6 +96,9 @@ class TestEditorInput extends EditorInput { } class NonSerializableTestEditorInput extends EditorInput { + + readonly resource = undefined; + constructor(public id: string) { super(); } @@ -106,7 +112,7 @@ class NonSerializableTestEditorInput extends EditorInput { class TestFileEditorInput extends EditorInput implements IFileEditorInput { - constructor(public id: string, private resource: URI) { + constructor(public id: string, public resource: URI) { super(); } getTypeId() { return 'testFileEditorInputForGroups'; } @@ -114,7 +120,6 @@ class TestFileEditorInput extends EditorInput implements IFileEditorInput { setEncoding(encoding: string) { } getEncoding() { return undefined; } setPreferredEncoding(encoding: string) { } - getResource(): URI { return this.resource; } setForceOpenAsBinary(): void { } setMode(mode: string) { } setPreferredMode(mode: string) { } diff --git a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts index c19f7b3686..3b66b13d65 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts @@ -8,6 +8,8 @@ import { EditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; class MyEditorInput extends EditorInput { + readonly resource = undefined; + getTypeId(): string { return ''; } resolve(): any { return null; } } diff --git a/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts b/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts index 1cd3e5cb79..592d0125e8 100644 --- a/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts @@ -40,7 +40,7 @@ suite('Editor - Range decorations', () => { model = aModel(URI.file('some_file')); codeEditor = createTestCodeEditor({ model: model }); - instantiationService.stub(IEditorService, 'activeEditor', { getResource: () => { return codeEditor.getModel()!.uri; } }); + instantiationService.stub(IEditorService, 'activeEditor', { get resource() { return codeEditor.getModel()!.uri; } }); instantiationService.stub(IEditorService, 'activeTextEditorWidget', codeEditor); testObject = instantiationService.createInstance(RangeHighlightDecorations); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 29e40f1583..5359a3d93a 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -89,6 +89,8 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { Direction } from 'vs/base/browser/ui/grid/grid'; +import { IProgressService, IProgressOptions, IProgressWindowOptions, IProgressNotificationOptions, IProgressCompositeOptions, IProgress, IProgressStep, emptyProgress } from 'vs/platform/progress/common/progress'; +import { IWorkingCopyFileService, WorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; export import TestTextResourcePropertiesService = CommonWorkbenchTestServices.TestTextResourcePropertiesService; export import TestContextService = CommonWorkbenchTestServices.TestContextService; @@ -120,7 +122,6 @@ export class TestTextFileService extends BrowserTextFileService { @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @ITextModelService textModelService: ITextModelService, @ICodeEditorService codeEditorService: ICodeEditorService, - @INotificationService notificationService: INotificationService, @IRemotePathService remotePathService: IRemotePathService ) { super( @@ -136,7 +137,6 @@ export class TestTextFileService extends BrowserTextFileService { filesConfigurationService, textModelService, codeEditorService, - notificationService, remotePathService ); } @@ -172,9 +172,11 @@ export const TestEnvironmentService = new BrowserWorkbenchEnvironmentService(Obj export function workbenchInstantiationService(overrides?: { textFileService?: (instantiationService: IInstantiationService) => ITextFileService }): ITestInstantiationService { const instantiationService = new TestInstantiationService(new ServiceCollection([ILifecycleService, new TestLifecycleService()])); + instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService()); instantiationService.stub(IEnvironmentService, TestEnvironmentService); const contextKeyService = instantiationService.createInstance(MockContextKeyService); instantiationService.stub(IContextKeyService, contextKeyService); + instantiationService.stub(IProgressService, new TestProgressService()); const workspaceContextService = new TestContextService(TestWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceContextService); const configService = new TestConfigurationService(); @@ -200,6 +202,7 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(IKeybindingService, new MockKeybindingService()); instantiationService.stub(IDecorationsService, new TestDecorationsService()); instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IWorkingCopyFileService, instantiationService.createInstance(WorkingCopyFileService)); instantiationService.stub(ITextFileService, overrides?.textFileService ? overrides.textFileService(instantiationService) : instantiationService.createInstance(TestTextFileService)); instantiationService.stub(IHostService, instantiationService.createInstance(TestHostService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); @@ -212,11 +215,23 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(IEditorService, editorService); instantiationService.stub(ICodeEditorService, new TestCodeEditorService()); instantiationService.stub(IViewletService, new TestViewletService()); - instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService()); return instantiationService; } +export class TestProgressService implements IProgressService { + + _serviceBrand: undefined; + + withProgress( + options: IProgressOptions | IProgressWindowOptions | IProgressNotificationOptions | IProgressCompositeOptions, + task: (progress: IProgress) => Promise, + onDidCancel?: ((choice?: number | undefined) => void) | undefined + ): Promise { + return task(emptyProgress); + } +} + export class TestAccessibilityService implements IAccessibilityService { _serviceBrand: undefined; @@ -588,8 +603,8 @@ export class TestFileService implements IFileService { _serviceBrand: undefined; - private readonly _onFileChanges: Emitter; - private readonly _onAfterOperation: Emitter; + private readonly _onDidFilesChange = new Emitter(); + private readonly _onDidRunOperation = new Emitter(); readonly onWillActivateFileSystemProvider = Event.None; readonly onDidChangeFileSystemProviderCapabilities = Event.None; @@ -598,18 +613,13 @@ export class TestFileService implements IFileService { private content = 'Hello Html'; private lastReadFileUri!: URI; - constructor() { - this._onFileChanges = new Emitter(); - this._onAfterOperation = new Emitter(); - } - setContent(content: string): void { this.content = content; } getContent(): string { return this.content; } getLastReadFileUri(): URI { return this.lastReadFileUri; } - get onFileChanges(): Event { return this._onFileChanges.event; } - fireFileChanges(event: FileChangesEvent): void { this._onFileChanges.fire(event); } - get onAfterOperation(): Event { return this._onAfterOperation.event; } - fireAfterOperation(event: FileOperationEvent): void { this._onAfterOperation.fire(event); } + get onDidFilesChange(): Event { return this._onDidFilesChange.event; } + fireFileChanges(event: FileChangesEvent): void { this._onDidFilesChange.fire(event); } + get onDidRunOperation(): Event { return this._onDidRunOperation.event; } + fireAfterOperation(event: FileOperationEvent): void { this._onDidRunOperation.fire(event); } resolve(resource: URI, _options?: IResolveFileOptions): Promise; resolve(resource: URI, _options: IResolveMetadataFileOptions): Promise; resolve(resource: URI, _options?: IResolveFileOptions): Promise { diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 121e03d41f..3f18fb3e9d 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -41,7 +41,7 @@ suite('Notifications', () => { // Events let called = 0; - item1.onDidExpansionChange(() => { + item1.onDidChangeExpansion(() => { called++; }); @@ -53,7 +53,7 @@ suite('Notifications', () => { assert.equal(called, 2); called = 0; - item1.onDidLabelChange(e => { + item1.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.PROGRESS) { called++; } @@ -65,7 +65,7 @@ suite('Notifications', () => { assert.equal(called, 2); called = 0; - item1.onDidLabelChange(e => { + item1.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.MESSAGE) { called++; } @@ -74,7 +74,7 @@ suite('Notifications', () => { item1.updateMessage('message update'); called = 0; - item1.onDidLabelChange(e => { + item1.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.SEVERITY) { called++; } @@ -83,7 +83,7 @@ suite('Notifications', () => { item1.updateSeverity(Severity.Error); called = 0; - item1.onDidLabelChange(e => { + item1.onDidChangeLabel(e => { if (e.kind === NotificationViewItemLabelKind.ACTIONS) { called++; } @@ -105,29 +105,6 @@ suite('Notifications', () => { let item6 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', { actions: [new Action('id', 'label')] }) })!; assert.equal(item6.actions!.primary!.length, 1); - // Links - let item7 = NotificationViewItem.create({ severity: Severity.Info, message: 'Unable to [Link 1](http://link1.com) open [Link 2](command:open.me "Open This") and [Link 3](command:without.title) and [Invalid Link4](ftp://link4.com)' })!; - - const links = item7.message.links; - assert.equal(links.length, 3); - assert.equal(links[0].name, 'Link 1'); - assert.equal(links[0].href, 'http://link1.com'); - assert.equal(links[0].title, 'http://link1.com'); - assert.equal(links[0].length, '[Link 1](http://link1.com)'.length); - assert.equal(links[0].offset, 'Unable to '.length); - - assert.equal(links[1].name, 'Link 2'); - assert.equal(links[1].href, 'command:open.me'); - assert.equal(links[1].title, 'Open This'); - assert.equal(links[1].length, '[Link 2](command:open.me "Open This")'.length); - assert.equal(links[1].offset, 'Unable to [Link 1](http://link1.com) open '.length); - - assert.equal(links[2].name, 'Link 3'); - assert.equal(links[2].href, 'command:without.title'); - assert.equal(links[2].title, 'Click to execute command \'without.title\''); - assert.equal(links[2].length, '[Link 3](command:without.title)'.length); - assert.equal(links[2].offset, 'Unable to [Link 1](http://link1.com) open [Link 2](command:open.me "Open This") and '.length); - // Filter let item8 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.SILENT)!; assert.equal(item8.silent, true); @@ -146,12 +123,12 @@ suite('Notifications', () => { const model = new NotificationsModel(); let lastNotificationEvent!: INotificationChangeEvent; - model.onDidNotificationChange(e => { + model.onDidChangeNotification(e => { lastNotificationEvent = e; }); let lastStatusMessageEvent!: IStatusMessageChangeEvent; - model.onDidStatusMessageChange(e => { + model.onDidChangeStatusMessage(e => { lastStatusMessageEvent = e; }); @@ -162,19 +139,19 @@ suite('Notifications', () => { let item1Handle = model.addNotification(item1); assert.equal(lastNotificationEvent.item.severity, item1.severity); - assert.equal(lastNotificationEvent.item.message.value, item1.message); + assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item1.message); assert.equal(lastNotificationEvent.index, 0); assert.equal(lastNotificationEvent.kind, NotificationChangeType.ADD); let item2Handle = model.addNotification(item2); assert.equal(lastNotificationEvent.item.severity, item2.severity); - assert.equal(lastNotificationEvent.item.message.value, item2.message); + assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item2.message); assert.equal(lastNotificationEvent.index, 0); assert.equal(lastNotificationEvent.kind, NotificationChangeType.ADD); model.addNotification(item3); assert.equal(lastNotificationEvent.item.severity, item3.severity); - assert.equal(lastNotificationEvent.item.message.value, item3.message); + assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item3.message); assert.equal(lastNotificationEvent.index, 0); assert.equal(lastNotificationEvent.kind, NotificationChangeType.ADD); @@ -189,27 +166,27 @@ suite('Notifications', () => { assert.equal(called, 1); assert.equal(model.notifications.length, 2); assert.equal(lastNotificationEvent.item.severity, item1.severity); - assert.equal(lastNotificationEvent.item.message.value, item1.message); + assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item1.message); assert.equal(lastNotificationEvent.index, 2); assert.equal(lastNotificationEvent.kind, NotificationChangeType.REMOVE); model.addNotification(item2Duplicate); assert.equal(model.notifications.length, 2); assert.equal(lastNotificationEvent.item.severity, item2Duplicate.severity); - assert.equal(lastNotificationEvent.item.message.value, item2Duplicate.message); + assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item2Duplicate.message); assert.equal(lastNotificationEvent.index, 0); assert.equal(lastNotificationEvent.kind, NotificationChangeType.ADD); item2Handle.close(); assert.equal(model.notifications.length, 1); assert.equal(lastNotificationEvent.item.severity, item2Duplicate.severity); - assert.equal(lastNotificationEvent.item.message.value, item2Duplicate.message); + assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item2Duplicate.message); assert.equal(lastNotificationEvent.index, 0); assert.equal(lastNotificationEvent.kind, NotificationChangeType.REMOVE); model.notifications[0].expand(); assert.equal(lastNotificationEvent.item.severity, item3.severity); - assert.equal(lastNotificationEvent.item.message.value, item3.message); + assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item3.message); assert.equal(lastNotificationEvent.index, 0); assert.equal(lastNotificationEvent.kind, NotificationChangeType.CHANGE); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 4cf11ed93f..a7e7de78be 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -22,7 +22,6 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { URI } from 'vs/base/common/uri'; import { IReadTextFileOptions, ITextFileStreamContent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; @@ -62,7 +61,6 @@ export class TestTextFileService extends NativeTextFileService { @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @ITextModelService textModelService: ITextModelService, @ICodeEditorService codeEditorService: ICodeEditorService, - @INotificationService notificationService: INotificationService, @IRemotePathService remotePathService: IRemotePathService ) { super( @@ -79,7 +77,6 @@ export class TestTextFileService extends NativeTextFileService { filesConfigurationService, textModelService, codeEditorService, - notificationService, remotePathService ); } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 875a90f804..b67633c89d 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -84,6 +84,7 @@ import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; import 'vs/workbench/services/path/common/remotePathService'; import 'vs/workbench/services/remote/common/remoteExplorerService'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; +import 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import 'vs/workbench/services/views/browser/viewDescriptorService'; @@ -376,6 +377,9 @@ import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; // Code Actions import 'vs/workbench/contrib/codeActions/common/codeActions.contribution'; +// Welcome +import 'vs/workbench/contrib/welcome/common/viewsWelcome.contribution'; + // Timeline import 'vs/workbench/contrib/timeline/browser/timeline.contribution';