From 8315dacda4c47d5ebd0b98913654c061b4e86faf Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Wed, 10 Apr 2019 16:29:23 -0700 Subject: [PATCH] Merge from vscode 31e03b8ffbb218a87e3941f2b63a249f061fe0e4 (#4986) --- build/azure-pipelines/common/publish.ts | 47 +- build/azure-pipelines/common/sync-mooncake.ts | 176 ++++ .../darwin/product-build-darwin.yml | 1 - build/azure-pipelines/distro-build.yml | 29 + .../linux/product-build-linux.yml | 3 - .../linux/snap-build-linux.yml | 1 - build/azure-pipelines/product-build.yml | 16 +- build/azure-pipelines/sync-mooncake.yml | 18 + .../win32/product-build-win32.yml | 1 - build/gulpfile.hygiene.js | 1 + build/gulpfile.vscode.js | 5 + build/lib/i18n.ts | 4 +- build/lib/task.js | 2 +- build/lib/task.ts | 2 +- build/npm/update-localization-extension.js | 4 +- build/tsconfig.json | 4 +- extensions/configuration-editing/package.json | 4 + .../client/src/jsonMain.ts | 1 + .../src/features/previewContentProvider.ts | 12 +- extensions/package.json | 2 +- extensions/yarn.lock | 8 +- scripts/test-integration.bat | 8 +- scripts/test-integration.sh | 13 +- src/main.js | 5 + .../workbench/api/common/sqlExtHostTypes.ts | 2 +- .../api/node/extHostAccountManagement.ts | 2 +- .../api/node/extHostCredentialManagement.ts | 2 +- .../workbench/api/node/extHostDataProtocol.ts | 2 +- .../api/node/extHostModelViewTree.ts | 4 +- src/sql/workbench/api/node/extHostNotebook.ts | 2 +- .../extHostNotebookDocumentsAndEditors.ts | 4 +- .../api/node/extHostResourceProvider.ts | 2 +- .../api/node/extHostSerializationProvider.ts | 2 +- src/sql/workbench/api/node/extHostTasks.ts | 2 +- .../workbench/api/node/sqlExtHost.api.impl.ts | 12 +- .../parts/insights/insightsUtils.test.ts | 143 ++- src/sqltest/stubs/editorGroupService.ts | 1 + src/vs/base/browser/mouseEvent.ts | 7 + src/vs/base/common/buffer.ts | 48 +- src/vs/base/common/event.ts | 27 - src/vs/base/common/network.ts | 2 + src/vs/base/common/resources.ts | 14 +- src/vs/base/test/common/buffer.test.ts | 20 + src/vs/base/test/common/event.test.ts | 61 -- .../issue/media/issueReporter.css | 4 + .../processExplorer/media/collapsed.svg | 1 + .../processExplorer/media/expanded.svg | 1 + .../processExplorer/media/processExplorer.css | 14 + .../processExplorer/processExplorerMain.ts | 260 +++-- .../sharedProcess/sharedProcessMain.ts | 4 +- src/vs/code/electron-main/app.ts | 4 +- src/vs/code/electron-main/main.ts | 2 +- src/vs/code/electron-main/window.ts | 3 + src/vs/code/node/cli.ts | 3 +- src/vs/code/node/cliProcessMain.ts | 25 +- .../editor/browser/controller/mouseHandler.ts | 6 + src/vs/editor/browser/editorBrowser.ts | 8 +- src/vs/editor/browser/view/viewController.ts | 5 + .../editor/browser/view/viewOutgoingEvents.ts | 8 + .../editor/browser/widget/codeEditorWidget.ts | 7 +- src/vs/editor/common/config/editorOptions.ts | 4 +- .../codeAction/codeActionContributions.ts | 3 +- .../editor/contrib/contextmenu/contextmenu.ts | 7 +- src/vs/editor/contrib/format/format.ts | 14 +- .../referenceSearch/referencesController.ts | 2 +- .../referenceSearch/referencesWidget.ts | 18 +- .../common/configurationModels.ts | 53 +- .../configuration/node/configuration.ts | 2 +- .../node/configurationService.ts | 5 +- .../test/common/configurationModels.test.ts | 30 +- .../test/node/configurationService.test.ts | 32 +- .../contextview/browser/contextMenuHandler.ts | 20 + .../diagnostics/common/diagnosticsService.ts | 9 + .../electron-main/diagnosticsService.ts | 79 +- .../driver/electron-browser/driver.ts | 4 +- .../environment/common/environment.ts | 6 +- src/vs/platform/environment/node/argv.ts | 1 + .../environment/node/environmentService.ts | 12 +- .../node/extensionGalleryService.ts | 12 +- src/vs/platform/files/common/files.ts | 91 +- .../electron-browser/sharedProcessService.ts | 2 +- .../issue/electron-main/issueService.ts | 31 + .../launch/electron-main/launchService.ts | 18 +- .../electron-browser/lifecycleService.ts | 2 +- src/vs/platform/list/browser/listService.ts | 404 +++----- .../remote/common/remoteAgentEnvironment.ts | 2 +- src/vs/platform/remote/common/remoteHosts.ts | 3 +- src/vs/platform/remote/common/tunnel.ts | 21 + .../remote/node/nodeWebSocketFactory.ts | 34 +- src/vs/platform/remote/node/tunnelService.ts | 21 + src/vs/platform/statusbar/common/statusbar.ts | 14 +- src/vs/platform/windows/common/windows.ts | 6 +- src/vs/vscode.proposed.d.ts | 16 +- .../api/browser/mainThreadDocuments.ts | 11 +- .../browser/mainThreadDocumentsAndEditors.ts | 7 +- .../api/browser/mainThreadFileSystem.ts | 13 +- .../api/browser/mainThreadSaveParticipant.ts | 4 +- .../api/browser/mainThreadStatusBar.ts | 50 +- .../workbench/api/browser/mainThreadWindow.ts | 52 +- .../api/{node => common}/apiCommands.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 15 +- .../{node => common}/extHostApiCommands.ts | 8 +- .../api/{node => common}/extHostClipboard.ts | 0 .../api/{node => common}/extHostCommands.ts | 8 +- .../api/{node => common}/extHostComments.ts | 8 +- .../{node => common}/extHostConfiguration.ts | 4 +- .../{node => common}/extHostDecorations.ts | 2 +- .../{node => common}/extHostDiagnostics.ts | 2 +- .../api/{node => common}/extHostDialogs.ts | 0 .../extHostDocumentContentProviders.ts | 4 +- .../{node => common}/extHostDocumentData.ts | 2 +- .../extHostDocumentSaveParticipant.ts | 6 +- .../api/{node => common}/extHostDocuments.ts | 6 +- .../extHostDocumentsAndEditors.ts | 8 +- .../extHostExtensionActivator.ts | 2 +- .../api/{node => common}/extHostFileSystem.ts | 29 +- .../extHostFileSystemEventService.ts | 4 +- .../{node => common}/extHostHeapService.ts | 2 +- .../extHostLanguageFeatures.ts | 16 +- .../api/{node => common}/extHostLanguages.ts | 4 +- .../api/{node => common}/extHostLogService.ts | 5 +- .../{node => common}/extHostMessageService.ts | 2 +- src/vs/workbench/api/common/extHostOutput.ts | 186 ++++ .../api/{node => common}/extHostProgress.ts | 2 +- .../api/{node => common}/extHostQuickOpen.ts | 8 +- .../api/{node => common}/extHostSCM.ts | 4 +- .../api/{node => common}/extHostStatusBar.ts | 2 +- .../api/{node => common}/extHostStorage.ts | 2 +- .../api/{node => common}/extHostTextEditor.ts | 6 +- .../{node => common}/extHostTextEditors.ts | 8 +- .../api/{node => common}/extHostTreeViews.ts | 6 +- .../{node => common}/extHostTypeConverters.ts | 2 +- .../api/{node => common}/extHostTypes.ts | 0 .../api/{node => common}/extHostUrls.ts | 2 +- .../api/{node => common}/extHostWebview.ts | 4 +- .../api/{node => common}/extHostWindow.ts | 21 +- .../api/{node => common}/extHostWorkspace.ts | 4 +- .../extensionHost.contribution.ts | 2 +- src/vs/workbench/api/node/extHost.api.impl.ts | 226 ++++- src/vs/workbench/api/node/extHostCLIServer.ts | 2 +- .../workbench/api/node/extHostDebugService.ts | 12 +- .../api/node/extHostExtensionService.ts | 41 +- .../api/node/extHostOutputService.ts | 193 +--- src/vs/workbench/api/node/extHostTask.ts | 8 +- .../api/node/extHostTerminalService.ts | 2 +- .../browser/actions/workspaceActions.ts | 6 +- src/vs/workbench/browser/composite.ts | 6 + src/vs/workbench/browser/contextkeys.ts | 12 +- src/vs/workbench/browser/labels.ts | 11 +- src/vs/workbench/browser/layout.ts | 14 +- src/vs/workbench/browser/nodeless.main.ts | 45 +- .../browser/nodeless.simpleservices.ts | 472 +-------- .../parts/editor/editor.contribution.ts | 9 +- .../browser/parts/editor/editorPart.ts | 4 + .../notifications/notificationsStatus.ts | 31 +- .../parts/statusbar/media/statusbarpart.css | 21 +- .../browser/parts/statusbar/statusbarPart.ts | 168 ++-- .../browser/parts/titlebar/menubarControl.ts | 4 +- .../browser/parts/titlebar/titlebarPart.ts | 6 +- .../browser/parts/views/panelViewlet.ts | 8 +- src/vs/workbench/browser/workbench.ts | 8 +- src/vs/workbench/common/contextkeys.ts | 4 +- .../contrib/debug/browser/debugActionItems.ts | 4 +- .../contrib/debug/browser/debugCommands.ts | 4 +- .../workbench/contrib/debug/common/debug.ts | 5 +- .../electron-browser/debug.contribution.ts | 5 + .../debugConfigurationManager.ts | 4 +- .../debug/electron-browser/debugService.ts | 25 +- .../debug/electron-browser/debugSession.ts | 10 + .../contrib/debug/test/common/mockDebug.ts | 6 + .../experimentalPrompts.test.ts | 41 +- .../contrib/extensions/common/extensions.ts | 67 +- .../electron-browser/extensionEditor.ts | 31 +- .../electron-browser/extensionsActions.ts | 444 ++++++--- .../electron-browser/extensionsList.ts | 31 +- .../electron-browser/extensionsViewlet.ts | 114 ++- .../electron-browser/extensionsViews.ts | 69 +- .../electron-browser/extensionsWidgets.ts | 11 +- .../media/extensionActions.css | 24 + .../media/status-warning-inverse.svg | 1 + .../electron-browser/media/status-warning.svg | 1 + .../runtimeExtensionsEditor.ts | 5 +- .../node/extensionsWorkbenchService.ts | 443 +++++---- .../extensionsActions.test.ts | 129 +-- .../electron-browser/extensionsViews.test.ts | 7 +- .../extensionsWorkbenchService.test.ts | 8 +- .../feedback/electron-browser/feedback.ts | 7 + .../files/browser/editors/textFileEditor.ts | 2 +- .../contrib/files/browser/fileActions.ts | 3 +- .../contrib/files/browser/fileCommands.ts | 25 +- .../contrib/files/browser/saveErrorHandler.ts | 9 +- .../files/common/editors/fileEditorInput.ts | 5 +- .../workbench/contrib/files/common/files.ts | 17 +- .../format/browser/formatActionsMultiple.ts | 17 +- .../issue/electron-browser/issueService.ts | 6 +- .../contrib/logs/common/logs.contribution.ts | 8 +- .../preferences.contribution.ts | 12 +- .../electron-browser/settingsEditor2.ts | 3 +- .../quickopen/browser/commandsHandler.ts | 13 +- .../relauncher.contribution.ts | 5 +- .../search/browser/openAnythingHandler.ts | 2 +- .../search/browser/search.contribution.ts | 11 +- .../contrib/search/browser/searchActions.ts | 40 +- .../contrib/search/browser/searchView.ts | 43 +- .../snippets/browser/configureSnippets.ts | 16 +- .../partsSplash.contribution.ts | 9 +- .../contrib/stats/node/workspaceStats.ts | 6 +- .../contrib/tasks/common/jsonSchema_v2.ts | 15 +- .../tasks/common/media/task.contribution.css | 1 + .../contrib/tasks/common/taskConfiguration.ts | 16 +- .../workbench/contrib/tasks/common/tasks.ts | 47 +- .../electron-browser/task.contribution.ts | 31 +- .../electron-browser/terminalTaskSystem.ts | 42 +- .../electron-browser/configuration.test.ts | 2 +- .../browser/telemetry.contribution.ts | 6 +- .../terminal/browser/terminal.contribution.ts | 5 + .../terminal/browser/terminalInstance.ts | 13 + .../browser/terminalProcessManager.ts | 18 +- .../terminal/browser/terminalService.ts | 8 +- .../browser/terminalTypeAheadAddon.ts | 136 +++ .../contrib/terminal/common/terminal.ts | 10 + .../terminalInstanceService.ts | 2 + .../electron-browser/terminalService.ts | 7 +- .../contrib/update/electron-browser/update.ts | 8 +- .../electron-browser/webviewElement.ts | 89 +- .../common/walkThroughContentProvider.ts | 4 +- .../actions/developerActions.ts | 2 +- .../electron-browser/actions/windowActions.ts | 2 +- .../electron-browser/main.contribution.ts | 2 +- src/vs/workbench/electron-browser/main.ts | 26 +- src/vs/workbench/electron-browser/window.ts | 8 +- .../node/accessibilityService.ts | 7 +- .../services/backup/common/backup.ts | 3 +- .../services/backup/node/backupFileService.ts | 33 +- .../backupFileService.test.ts | 14 +- .../electron-browser/broadcastService.ts | 2 +- .../configuration/browser/configuration.ts | 43 +- .../browser/configurationService.ts | 18 +- .../configuration/common/configuration.ts | 7 +- .../common/configurationEditingService.ts | 4 +- .../common/configurationModels.ts | 67 +- .../common/jsonEditingService.ts | 2 +- .../test/common/configurationModels.test.ts | 24 +- .../browser/configurationResolverService.ts | 41 +- .../configurationResolverService.ts | 31 + .../configurationResolverService.test.ts | 33 +- .../dialogs/browser/fileDialogService.ts | 8 +- .../dialogs/browser/remoteFileDialog.ts | 145 +-- .../editor/common/editorGroupsService.ts | 5 + .../environment/common/environmentService.ts | 16 + .../environment/node/environmentService.ts | 24 + .../node/extensionEnablementService.ts | 8 +- .../extensionEnablementService.test.ts | 22 +- .../extensionDescriptionRegistry.ts | 0 .../extensions/common/extensionDevOptions.ts | 8 +- .../extensions/common/extensionHostDebug.ts | 28 +- .../{node => common}/extensionHostProtocol.ts | 0 .../{node => common}/lazyPromise.ts | 0 .../{node => common}/rpcProtocol.ts | 11 +- .../cachedExtensionScanner.ts | 35 +- .../electron-browser/extensionHost.ts | 12 +- .../extensionHostDebugService.ts | 73 +- .../extensionHostProcessManager.ts | 5 +- .../extensionManagementServerService.ts | 2 +- .../electron-browser/extensionService.ts | 26 +- .../extensions/node/extensionHostMain.ts | 72 +- .../extensions/node/extensionHostProcess.ts | 52 +- .../services/extensions/node/proxyResolver.ts | 6 +- .../extensions/test/node/rpcProtocol.test.ts | 2 +- .../services/files/node/fileService.ts | 14 +- .../services/files/node/remoteFileService.ts | 16 +- .../workbench/services/files/node/streams.ts | 2 +- .../test/electron-browser/fileService.test.ts | 243 ++--- .../services/files2/browser/fileService2.ts | 78 ++ .../services/files2/common/fileService2.ts | 146 ++- .../files2/node/diskFileSystemProvider.ts | 55 +- .../files2/test/node/diskFileService.test.ts | 184 +++- .../keybinding/common/keybindingEditing.ts | 2 +- .../output/node/outputChannelModelService.ts | 8 +- .../preferences/browser/preferencesService.ts | 8 +- .../remote/browser/remoteAgentServiceImpl.ts | 25 + .../common/abstractRemoteAgentService.ts | 21 + .../common/remoteAgentEnvironmentChannel.ts | 11 +- .../remote/common/remoteAgentService.ts | 4 + .../services/remote/node/tunnelService.ts | 18 + .../electron-browser/telemetryService.ts | 11 +- .../textfile/common/textFileEditorModel.ts | 136 +-- .../common/textFileEditorModelManager.ts | 17 +- .../textfile/common/textFileService.ts | 932 +++++++++--------- .../services/textfile/common/textfiles.ts | 28 +- .../services/textfile/node/textFileService.ts | 86 ++ .../node/textResourcePropertiesService.ts | 11 +- .../themes/browser/workbenchThemeService.ts | 23 +- .../timer/electron-browser/timerService.ts | 8 +- .../untitled/common/untitledEditorService.ts | 11 +- .../window}/electron-browser/windowService.ts | 32 +- .../workspaceEditingService.ts | 16 +- .../api/extHostApiCommands.test.ts | 16 +- .../api/extHostCommands.test.ts | 2 +- .../api/extHostConfiguration.test.ts | 4 +- .../api/extHostDiagnostics.test.ts | 4 +- .../api/extHostDocumentData.test.ts | 4 +- .../extHostDocumentSaveParticipant.test.ts | 8 +- .../api/extHostDocumentsAndEditors.test.ts | 2 +- .../api/extHostFileSystemEventService.test.ts | 2 +- .../api/extHostLanguageFeatures.test.ts | 14 +- .../api/extHostSearch.test.ts | 2 +- .../api/extHostTextEditor.test.ts | 6 +- .../api/extHostTextEditors.test.ts | 6 +- .../api/extHostTreeViews.test.ts | 6 +- .../api/extHostTypeConverter.test.ts | 4 +- .../electron-browser/api/extHostTypes.test.ts | 2 +- .../api/extHostWebview.test.ts | 2 +- .../api/extHostWorkspace.test.ts | 4 +- .../api/mainThreadDocumentsAndEditors.test.ts | 5 +- .../api/mainThreadEditors.test.ts | 5 +- .../workbench/test/workbenchTestServices.ts | 37 +- src/vs/workbench/workbench.main.ts | 18 +- src/vs/workbench/workbench.nodeless.main.ts | 17 +- tslint.json | 2 +- 320 files changed, 5540 insertions(+), 3822 deletions(-) create mode 100644 build/azure-pipelines/common/sync-mooncake.ts create mode 100644 build/azure-pipelines/distro-build.yml create mode 100644 build/azure-pipelines/sync-mooncake.yml create mode 100644 src/vs/base/test/common/buffer.test.ts create mode 100755 src/vs/code/electron-browser/processExplorer/media/collapsed.svg create mode 100755 src/vs/code/electron-browser/processExplorer/media/expanded.svg create mode 100644 src/vs/platform/remote/common/tunnel.ts create mode 100644 src/vs/platform/remote/node/tunnelService.ts rename src/vs/workbench/api/{node => common}/apiCommands.ts (99%) rename src/vs/workbench/api/{node => common}/extHostApiCommands.ts (98%) rename src/vs/workbench/api/{node => common}/extHostClipboard.ts (100%) rename src/vs/workbench/api/{node => common}/extHostCommands.ts (96%) rename src/vs/workbench/api/{node => common}/extHostComments.ts (99%) rename src/vs/workbench/api/{node => common}/extHostConfiguration.ts (99%) rename src/vs/workbench/api/{node => common}/extHostDecorations.ts (97%) rename src/vs/workbench/api/{node => common}/extHostDiagnostics.ts (99%) rename src/vs/workbench/api/{node => common}/extHostDialogs.ts (100%) rename src/vs/workbench/api/{node => common}/extHostDocumentContentProviders.ts (95%) rename src/vs/workbench/api/{node => common}/extHostDocumentData.ts (99%) rename src/vs/workbench/api/{node => common}/extHostDocumentSaveParticipant.ts (97%) rename src/vs/workbench/api/{node => common}/extHostDocuments.ts (96%) rename src/vs/workbench/api/{node => common}/extHostDocumentsAndEditors.ts (94%) rename src/vs/workbench/api/{node => common}/extHostExtensionActivator.ts (99%) rename src/vs/workbench/api/{node => common}/extHostFileSystem.ts (91%) rename src/vs/workbench/api/{node => common}/extHostFileSystemEventService.ts (97%) rename src/vs/workbench/api/{node => common}/extHostHeapService.ts (92%) rename src/vs/workbench/api/{node => common}/extHostLanguageFeatures.ts (99%) rename src/vs/workbench/api/{node => common}/extHostLanguages.ts (91%) rename src/vs/workbench/api/{node => common}/extHostLogService.ts (88%) rename src/vs/workbench/api/{node => common}/extHostMessageService.ts (97%) create mode 100644 src/vs/workbench/api/common/extHostOutput.ts rename src/vs/workbench/api/{node => common}/extHostProgress.ts (99%) rename src/vs/workbench/api/{node => common}/extHostQuickOpen.ts (98%) rename src/vs/workbench/api/{node => common}/extHostSCM.ts (99%) rename src/vs/workbench/api/{node => common}/extHostStatusBar.ts (99%) rename src/vs/workbench/api/{node => common}/extHostStorage.ts (96%) rename src/vs/workbench/api/{node => common}/extHostTextEditor.ts (99%) rename src/vs/workbench/api/{node => common}/extHostTextEditors.ts (96%) rename src/vs/workbench/api/{node => common}/extHostTreeViews.ts (99%) rename src/vs/workbench/api/{node => common}/extHostTypeConverters.ts (99%) rename src/vs/workbench/api/{node => common}/extHostTypes.ts (100%) rename src/vs/workbench/api/{node => common}/extHostUrls.ts (97%) rename src/vs/workbench/api/{node => common}/extHostWebview.ts (98%) rename src/vs/workbench/api/{node => common}/extHostWindow.ts (76%) rename src/vs/workbench/api/{node => common}/extHostWorkspace.ts (99%) create mode 100644 src/vs/workbench/contrib/extensions/electron-browser/media/status-warning-inverse.svg create mode 100644 src/vs/workbench/contrib/extensions/electron-browser/media/status-warning.svg create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts rename src/vs/{platform => workbench/services}/accessibility/node/accessibilityService.ts (87%) create mode 100644 src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts create mode 100644 src/vs/workbench/services/environment/common/environmentService.ts create mode 100644 src/vs/workbench/services/environment/node/environmentService.ts rename src/vs/workbench/services/extensions/{node => common}/extensionDescriptionRegistry.ts (100%) rename src/vs/workbench/services/extensions/{node => common}/extensionHostProtocol.ts (100%) rename src/vs/workbench/services/extensions/{node => common}/lazyPromise.ts (100%) rename src/vs/workbench/services/extensions/{node => common}/rpcProtocol.ts (99%) create mode 100644 src/vs/workbench/services/files2/browser/fileService2.ts create mode 100644 src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts create mode 100644 src/vs/workbench/services/remote/node/tunnelService.ts rename src/vs/{platform => workbench/services}/telemetry/electron-browser/telemetryService.ts (85%) create mode 100644 src/vs/workbench/services/textfile/node/textFileService.ts rename src/vs/{platform/windows => workbench/services/window}/electron-browser/windowService.ts (84%) diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index e759a39636..c17615a8e6 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -65,8 +65,7 @@ interface Asset { platform: string; type: string; url: string; - // {{SQL CARBON EDIT}} - mooncakeUrl: string | undefined; + mooncakeUrl?: string; hash: string; sha256hash: string; size: number; @@ -189,56 +188,18 @@ async function publish(commit: string, quality: string, platform: string, type: const blobService = azure.createBlobService(storageAccount, process.env['AZURE_STORAGE_ACCESS_KEY_2']!) .withFilter(new azure.ExponentialRetryPolicyFilter(20)); - // {{SQL CARBON EDIT}} await assertContainer(blobService, quality); const blobExists = await doesAssetExist(blobService, quality, blobName); - const promises = []; - - if (!blobExists) { - promises.push(uploadBlob(blobService, quality, blobName, file)); - } - - // {{SQL CARBON EDIT}} - if (process.env['MOONCAKE_STORAGE_ACCESS_KEY']) { - const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY']!, `${storageAccount}.blob.core.chinacloudapi.cn`) - .withFilter(new azure.ExponentialRetryPolicyFilter(20)); - - // mooncake is fussy and far away, this is needed! - mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; - - await Promise.all([ - assertContainer(blobService, quality), - assertContainer(mooncakeBlobService, quality) - ]); - - const [blobExists, moooncakeBlobExists] = await Promise.all([ - doesAssetExist(blobService, quality, blobName), - doesAssetExist(mooncakeBlobService, quality, blobName) - ]); - - const promises: Array> = []; - - if (!blobExists) { - promises.push(uploadBlob(blobService, quality, blobName, file)); - } - - if (!moooncakeBlobExists) { - promises.push(uploadBlob(mooncakeBlobService, quality, blobName, file)); - } - } else { - console.log('Skipping Mooncake publishing.'); - } - - if (promises.length === 0) { + if (blobExists) { console.log(`Blob ${quality}, ${blobName} already exists, not publishing again.`); return; } console.log('Uploading blobs to Azure storage...'); - await Promise.all(promises); + await uploadBlob(blobService, quality, blobName, file); console.log('Blobs successfully uploaded.'); @@ -250,8 +211,6 @@ async function publish(commit: string, quality: string, platform: string, type: platform: platform, type: type, url: `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`, - // {{SQL CARBON EDIT}} - mooncakeUrl: process.env['MOONCAKE_CDN_URL'] ? `${process.env['MOONCAKE_CDN_URL']}/${quality}/${blobName}` : undefined, hash: sha1hash, sha256hash, size diff --git a/build/azure-pipelines/common/sync-mooncake.ts b/build/azure-pipelines/common/sync-mooncake.ts new file mode 100644 index 0000000000..af0db53068 --- /dev/null +++ b/build/azure-pipelines/common/sync-mooncake.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as url from 'url'; +import * as azure from 'azure-storage'; +import * as mime from 'mime'; +import { DocumentClient, RetrievedDocument } from 'documentdb'; + +function log(...args: any[]) { + console.log(...[`[${new Date().toISOString()}]`, ...args]); +} + +function error(...args: any[]) { + console.error(...[`[${new Date().toISOString()}]`, ...args]); +} + +if (process.argv.length < 3) { + error('Usage: node sync-mooncake.js '); + process.exit(-1); +} + +interface Build extends RetrievedDocument { + assets: Asset[]; +} + +interface Asset { + platform: string; + type: string; + url: string; + mooncakeUrl: string; + hash: string; + sha256hash: string; + size: number; + supportsFastUpdate?: boolean; +} + +function updateBuild(commit: string, quality: string, platform: string, type: string, asset: Asset): Promise { + const client = new DocumentClient(process.env['AZURE_DOCUMENTDB_ENDPOINT']!, { masterKey: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const collection = 'dbs/builds/colls/' + quality; + const updateQuery = { + query: 'SELECT TOP 1 * FROM c WHERE c.id = @id', + parameters: [{ name: '@id', value: commit }] + }; + + let updateTries = 0; + + function _update(): Promise { + updateTries++; + + return new Promise((c, e) => { + client.queryDocuments(collection, updateQuery).toArray((err, results) => { + if (err) { return e(err); } + if (results.length !== 1) { return e(new Error('No documents')); } + + const release = results[0]; + + release.assets = [ + ...release.assets.filter((a: any) => !(a.platform === platform && a.type === type)), + asset + ]; + + client.replaceDocument(release._self, release, err => { + if (err && err.code === 409 && updateTries < 5) { return c(_update()); } + if (err) { return e(err); } + + log('Build successfully updated.'); + c(); + }); + }); + }); + } + + return _update(); +} + +async function sync(commit: string, quality: string): Promise { + log(`Synchronizing Mooncake assets for ${quality}, ${commit}...`); + + const cosmosdb = new DocumentClient(process.env['AZURE_DOCUMENTDB_ENDPOINT']!, { masterKey: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const collection = `dbs/builds/colls/${quality}`; + const query = { + query: 'SELECT TOP 1 * FROM c WHERE c.id = @id', + parameters: [{ name: '@id', value: commit }] + }; + + const build = await new Promise((c, e) => { + cosmosdb.queryDocuments(collection, query).toArray((err, results) => { + if (err) { return e(err); } + if (results.length !== 1) { return e(new Error('No documents')); } + c(results[0] as Build); + }); + }); + + log(`Found build for ${commit}, with ${build.assets.length} assets`); + + const storageAccount = process.env['AZURE_STORAGE_ACCOUNT_2']!; + + const blobService = azure.createBlobService(storageAccount, process.env['AZURE_STORAGE_ACCESS_KEY_2']!) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + + const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY']!, `${storageAccount}.blob.core.chinacloudapi.cn`) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + + // mooncake is fussy and far away, this is needed! + blobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + + for (const asset of build.assets) { + try { + const blobPath = url.parse(asset.url).path; + + if (!blobPath) { + throw new Error(`Failed to parse URL: ${asset.url}`); + } + + const blobName = blobPath.replace(/^\/\w+\//, ''); + + log(`Found ${blobName}`); + + if (asset.mooncakeUrl) { + log(` Already in Mooncake ✔️`); + continue; + } + + const readStream = blobService.createReadStream(quality, blobName, undefined!); + const blobOptions: azure.BlobService.CreateBlockBlobRequestOptions = { + contentSettings: { + contentType: mime.lookup(blobPath), + cacheControl: 'max-age=31536000, public' + } + }; + + const writeStream = mooncakeBlobService.createWriteStreamToBlockBlob(quality, blobName, blobOptions, undefined); + + log(` Uploading to Mooncake...`); + await new Promise((c, e) => readStream.pipe(writeStream).on('finish', c).on('error', e)); + + log(` Updating build in DB...`); + asset.mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; + await updateBuild(commit, quality, asset.platform, asset.type, asset); + + log(` Done ✔️`); + } catch (err) { + error(err); + } + } + + log(`All done ✔️`); +} + +function main(): void { + if (process.env['VSCODE_BUILD_SKIP_PUBLISH']) { + error('Skipping publish due to VSCODE_BUILD_SKIP_PUBLISH'); + return; + } + + const commit = process.env['BUILD_SOURCEVERSION']; + + if (!commit) { + error('Skipping publish due to missing BUILD_SOURCEVERSION'); + return; + } + + const quality = process.argv[2]; + + sync(commit, quality).catch(err => { + error(err); + process.exit(1); + }); +} + +main(); diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index bf5afddde0..55ee286e85 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -78,7 +78,6 @@ steps: VERSION=`node -p "require(\"$PACKAGEJSON\").version"` AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ node build/azure-pipelines/common/publish.js \ "$(VSCODE_QUALITY)" \ darwin \ diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml new file mode 100644 index 0000000000..b8ef20162b --- /dev/null +++ b/build/azure-pipelines/distro-build.yml @@ -0,0 +1,29 @@ +trigger: + branches: + include: ['master', 'release/*'] +pr: + branches: + include: ['master', 'release/*'] + +steps: +- task: NodeTool@0 + inputs: + versionSpec: "10.15.1" + +- script: | + set -e + + cat << EOF > ~/.netrc + machine github.com + login vscode + password $(VSCODE_MIXIN_PASSWORD) + EOF + + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + + git remote add distro "https://github.com/$VSCODE_MIXIN_REPO.git" + git fetch distro + git merge $(node -p "require('./package.json').distro") + + displayName: Merge Distro \ No newline at end of file diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index f7bec85a97..3cea6670f0 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -68,7 +68,6 @@ steps: AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_LINUX" archive-unsigned "$TARBALL_FILENAME" "$VERSION" true "$TARBALL_PATH" # Publish hockeyapp symbols @@ -83,7 +82,6 @@ steps: AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_DEB" package "$DEB_FILENAME" "$VERSION" true "$DEB_PATH" # Publish RPM @@ -95,7 +93,6 @@ steps: AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "$PLATFORM_RPM" package "$RPM_FILENAME" "$VERSION" true "$RPM_PATH" # Publish Snap diff --git a/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index 7d3f5fefc1..9588ebcb36 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -46,5 +46,4 @@ steps: # Publish snap package AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ - MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ node build/azure-pipelines/common/publish.js "$VSCODE_QUALITY" "linux-snap-$ARCH" package "$SNAP_FILENAME" "$VERSION" true "$SNAP_PATH" \ No newline at end of file diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index afebc973cc..86868f6a2a 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -62,4 +62,18 @@ jobs: pool: vmImage: macOS 10.13 steps: - - template: darwin/product-build-darwin.yml \ No newline at end of file + - template: darwin/product-build-darwin.yml + +- job: Mooncake + pool: + vmImage: 'Ubuntu-16.04' + condition: true + dependsOn: + - Windows + - Windows32 + - Linux + - LinuxSnap + - Linux32 + - macOS + steps: + - template: sync-mooncake.yml \ No newline at end of file diff --git a/build/azure-pipelines/sync-mooncake.yml b/build/azure-pipelines/sync-mooncake.yml new file mode 100644 index 0000000000..c422839de1 --- /dev/null +++ b/build/azure-pipelines/sync-mooncake.yml @@ -0,0 +1,18 @@ +steps: +- task: NodeTool@0 + inputs: + versionSpec: "10.15.1" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.10.1" + +- script: | + set -e + + (cd build ; yarn) + + AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ + AZURE_STORAGE_ACCESS_KEY_2="$(AZURE_STORAGE_ACCESS_KEY_2)" \ + MOONCAKE_STORAGE_ACCESS_KEY="$(MOONCAKE_STORAGE_ACCESS_KEY)" \ + node build/azure-pipelines/common/sync-mooncake.js "$VSCODE_QUALITY" diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 6d680f0d9d..7c1b5cd9a5 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -133,7 +133,6 @@ steps: $Version = $PackageJson.version $Quality = "$env:VSCODE_QUALITY" $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(AZURE_STORAGE_ACCESS_KEY_2)" - $env:MOONCAKE_STORAGE_ACCESS_KEY = "$(MOONCAKE_STORAGE_ACCESS_KEY)" $env:AZURE_DOCUMENTDB_MASTERKEY = "$(AZURE_DOCUMENTDB_MASTERKEY)" $assetPlatform = if ("$(VSCODE_ARCH)" -eq "ia32") { "win32" } else { "win32-x64" } diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index f3fa69fedc..db97428736 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -87,6 +87,7 @@ const indentationFilter = [ '!build/azure-pipelines/**/*.js', '!build/azure-pipelines/**/*.config', '!**/Dockerfile', + '!**/Dockerfile.*', '!**/*.Dockerfile', '!**/*.dockerfile', '!extensions/markdown-language-features/media/*.js' diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 1ef4113c11..987ddd4115 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -89,6 +89,8 @@ const vscodeResources = [ 'out-build/vs/workbench/contrib/welcome/walkThrough/**/*.md', 'out-build/vs/workbench/services/files/**/*.exe', 'out-build/vs/workbench/services/files/**/*.md', + 'out-build/vs/workbench/services/files2/**/*.exe', + 'out-build/vs/workbench/services/files2/**/*.md', 'out-build/vs/code/electron-browser/workbench/**', 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js', 'out-build/vs/code/electron-browser/issue/issueReporter.js', @@ -425,6 +427,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op result = es.merge(result, gulp.src('resources/win32/bin/code.sh', { base: 'resources/win32' }) .pipe(replace('@@NAME@@', product.nameShort)) + .pipe(replace('@@PRODNAME@@', product.nameLong)) + .pipe(replace('@@VERSION@@', version)) .pipe(replace('@@COMMIT@@', commit)) .pipe(replace('@@APPNAME@@', product.applicationName)) .pipe(rename(function (f) { f.basename = product.applicationName; f.extname = ''; }))); @@ -433,6 +437,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); } else if (platform === 'linux') { result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) + .pipe(replace('@@PRODNAME@@', product.nameLong)) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(rename('bin/' + product.applicationName))); } diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 8d3f22f5ac..7bb98cbab7 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -24,7 +24,7 @@ function log(message: any, ...rest: any[]): void { } export interface Language { - id: string; // laguage id, e.g. zh-tw, de + id: string; // language id, e.g. zh-tw, de translationId?: string; // language id used in translation tools, e.g zh-hant, de (optional, if not set, the id is used) folderName?: string; // language specific folder name, e.g. cht, deu (optional, if not set, the id is used) } @@ -1378,4 +1378,4 @@ function decodeEntities(value: string): string { function pseudify(message: string) { return '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; -} \ No newline at end of file +} diff --git a/build/lib/task.js b/build/lib/task.js index 4c84bc7d6c..cf1dafbd34 100644 --- a/build/lib/task.js +++ b/build/lib/task.js @@ -32,7 +32,7 @@ async function _doExecute(task) { // Always invoke as if it were a callback task return new Promise((resolve, reject) => { if (task.length === 1) { - // this is a calback task + // this is a callback task task((err) => { if (err) { return reject(err); diff --git a/build/lib/task.ts b/build/lib/task.ts index 1ced5f3716..8b3d072386 100644 --- a/build/lib/task.ts +++ b/build/lib/task.ts @@ -54,7 +54,7 @@ async function _doExecute(task: Task): Promise { // Always invoke as if it were a callback task return new Promise((resolve, reject) => { if (task.length === 1) { - // this is a calback task + // this is a callback task task((err) => { if (err) { return reject(err); diff --git a/build/npm/update-localization-extension.js b/build/npm/update-localization-extension.js index 1273b40126..499ad918ec 100644 --- a/build/npm/update-localization-extension.js +++ b/build/npm/update-localization-extension.js @@ -74,7 +74,7 @@ function update(options) { let translationPaths = []; i18n.pullI18nPackFiles(server, userName, apiToken, { id: languageId }, translationPaths) .on('error', (error) => { - console.log(`Error occured while importing translations:`); + console.log(`Error occurred while importing translations:`); translationPaths = undefined; if (Array.isArray(error)) { error.forEach(console.log); @@ -100,7 +100,7 @@ function update(options) { gulp.src(path.join(location, languageId, '**', '*.xlf')) .pipe(i18n.prepareI18nPackFiles(i18n.externalExtensionsWithTranslations, translationPaths, languageId === 'ps')) .on('error', (error) => { - console.log(`Error occured while importing translations:`); + console.log(`Error occurred while importing translations:`); translationPaths = undefined; if (Array.isArray(error)) { error.forEach(console.log); diff --git a/build/tsconfig.json b/build/tsconfig.json index 46ad745bc2..df15ccdd1b 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -8,7 +8,7 @@ "resolveJsonModule": true, "experimentalDecorators": true, // enable JavaScript type checking for the language service - // use the tsconfig.build.json for compiling wich disable JavaScript + // use the tsconfig.build.json for compiling which disable JavaScript // type checking so that JavaScript file are not transpiled "allowJs": true, "checkJs": true, @@ -22,4 +22,4 @@ "exclude": [ "node_modules/**" ] -} \ No newline at end of file +} diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 9a1a95f5fb..bb9690a07d 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -57,6 +57,10 @@ "fileMatch": "%APP_SETTINGS_HOME%/settings.json", "url": "vscode://schemas/settings/user" }, + { + "fileMatch": "%MACHINE_SETTINGS_HOME%/settings.json", + "url": "vscode://schemas/settings/machine" + }, { "fileMatch": "%APP_WORKSPACES_HOME%/*/workspace.json", "url": "vscode://schemas/workspaceConfig" diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index 6a889d7cea..0a6d602886 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -265,6 +265,7 @@ function getSchemaAssociation(_context: ExtensionContext): ISchemaAssociations { } if (fileMatch[0] === '%') { fileMatch = fileMatch.replace(/%APP_SETTINGS_HOME%/, '/User'); + fileMatch = fileMatch.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine'); fileMatch = fileMatch.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces'); } else if (fileMatch.charAt(0) !== '/' && !fileMatch.match(/\w+:\/\//)) { fileMatch = '/' + fileMatch; diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 34a099c829..80f032b1b8 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -101,21 +101,19 @@ export class MarkdownContentProvider { return href; } - // Use href if it is already an URL - const hrefUri = vscode.Uri.parse(href); - if (['http', 'https'].indexOf(hrefUri.scheme) >= 0) { - return hrefUri.toString(true); + if (href.startsWith('http:') || href.startsWith('https:') || href.startsWith('file:')) { + return href; } - // Use href as file URI if it is absolute - if (path.isAbsolute(href) || hrefUri.scheme === 'file') { + // Assume it must be a local file + if (path.isAbsolute(href)) { return vscode.Uri.file(href) .with({ scheme: 'vscode-resource' }) .toString(); } // Use a workspace relative path if there is a workspace - let root = vscode.workspace.getWorkspaceFolder(resource); + const root = vscode.workspace.getWorkspaceFolder(resource); if (root) { return vscode.Uri.file(path.join(root.uri.fsPath, href)) .with({ scheme: 'vscode-resource' }) diff --git a/extensions/package.json b/extensions/package.json index dcce9525ef..aee1adf2d3 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.4.1" + "typescript": "3.4.3-insiders.20190408" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index a011714b22..9c39cd909a 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6" - integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q== +typescript@3.4.3-insiders.20190408: + version "3.4.3-insiders.20190408" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.3-insiders.20190408.tgz#18d98336c693a13dc8b2d5f39b70268c018c650b" + integrity sha512-5SI6EA+2u0ea/Uy0qCEczh8vBR0ByVaCFCyU0RdROROw8V5O4OIQHMFcnIdyg+nnfRGYp39PxvllGMDpsTFOOQ== diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 17d4519d8a..ff579e873d 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -11,18 +11,18 @@ set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% :: if %errorlevel% neq 0 exit /b %errorlevel% :: Tests in the extension host -call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% -call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testworkspace.code-workspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\workspace-tests --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testworkspace.code-workspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\workspace-tests --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% -call .\scripts\code.bat %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +call .\scripts\code.bat %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% call .\scripts\code.bat %~dp0\..\extensions\markdown-language-features\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% call .\scripts\code.bat %~dp0\..\extensions\azurecore\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% -:: call .\scripts\code.bat $%~dp0\..\extensions\emmet\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% . +:: call .\scripts\code.bat $%~dp0\..\extensions\emmet\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% . :: if %errorlevel% neq 0 exit /b %errorlevel% :: Integration & performance tests in AMD diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 20d400f18d..cab90665c4 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -17,14 +17,15 @@ cd $ROOT # Tests in the extension host # TODO port over an re-enable API tests -# ./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started -./scripts/code.sh $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started -./scripts/code.sh $ROOT/extensions/markdown-language-features/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +#./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests --disable-extensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +#./scripts/code.sh $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests --disable-extensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +./scripts/code.sh $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disable-extensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +./scripts/code.sh $ROOT/extensions/markdown-language-features/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disable-extensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started ./scripts/code.sh $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started -mkdir $ROOT/extensions/emmet/test-fixtures -./scripts/code.sh $ROOT/extensions/emmet/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started . -rm -r $ROOT/extensions/emmet/test-fixtures +mkdir -p $ROOT/extensions/emmet/test-fixtures +./scripts/code.sh $ROOT/extensions/emmet/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disable-extensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +rm -rf $ROOT/extensions/emmet/test-fixtures # Tests in commonJS cd $ROOT/extensions/css-language-features/server && $ROOT/scripts/node-electron.sh test/index.js diff --git a/src/main.js b/src/main.js index e5f1b02a72..52ece7e3ff 100644 --- a/src/main.js +++ b/src/main.js @@ -146,6 +146,11 @@ function configureCommandlineSwitches(cliArgs, nodeCachedDataDir) { if (jsFlags) { app.commandLine.appendSwitch('--js-flags', jsFlags); } + + // Disable smooth scrolling for Webviews + if (cliArgs['disable-smooth-scrolling']) { + app.commandLine.appendSwitch('disable-smooth-scrolling'); + } } /** diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 3ed11ae6c8..e7662b03cb 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -5,7 +5,7 @@ 'use strict'; import { nb, IConnectionProfile } from 'azdata'; -import * as vsExtTypes from 'vs/workbench/api/node/extHostTypes'; +import * as vsExtTypes from 'vs/workbench/api/common/extHostTypes'; // SQL added extension host types export enum ServiceOptionType { diff --git a/src/sql/workbench/api/node/extHostAccountManagement.ts b/src/sql/workbench/api/node/extHostAccountManagement.ts index 49dfe5b31d..0e1e5fb887 100644 --- a/src/sql/workbench/api/node/extHostAccountManagement.ts +++ b/src/sql/workbench/api/node/extHostAccountManagement.ts @@ -6,7 +6,7 @@ 'use strict'; import * as azdata from 'azdata'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { ExtHostAccountManagementShape, MainThreadAccountManagementShape, diff --git a/src/sql/workbench/api/node/extHostCredentialManagement.ts b/src/sql/workbench/api/node/extHostCredentialManagement.ts index 2d509a993a..a1d07a02e6 100644 --- a/src/sql/workbench/api/node/extHostCredentialManagement.ts +++ b/src/sql/workbench/api/node/extHostCredentialManagement.ts @@ -8,7 +8,7 @@ import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { SqlMainContext, MainThreadCredentialManagementShape, ExtHostCredentialManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; class CredentialAdapter { public provider: azdata.CredentialProvider; diff --git a/src/sql/workbench/api/node/extHostDataProtocol.ts b/src/sql/workbench/api/node/extHostDataProtocol.ts index 8909848a00..600b0c222f 100644 --- a/src/sql/workbench/api/node/extHostDataProtocol.ts +++ b/src/sql/workbench/api/node/extHostDataProtocol.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import { Event, Emitter } from 'vs/base/common/event'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { SqlMainContext, MainThreadDataProtocolShape, ExtHostDataProtocolShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { DataProviderType } from 'sql/workbench/api/common/sqlExtHostTypes'; diff --git a/src/sql/workbench/api/node/extHostModelViewTree.ts b/src/sql/workbench/api/node/extHostModelViewTree.ts index 88c0c3ee4c..89c66b907c 100644 --- a/src/sql/workbench/api/node/extHostModelViewTree.ts +++ b/src/sql/workbench/api/node/extHostModelViewTree.ts @@ -8,10 +8,10 @@ import { localize } from 'vs/nls'; import * as vscode from 'vscode'; import { SqlMainContext, ExtHostModelViewTreeViewsShape, MainThreadModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { ITreeComponentItem } from 'sql/workbench/common/views'; -import { CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; +import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import * as azdata from 'azdata'; -import * as vsTreeExt from 'vs/workbench/api/node/extHostTreeViews'; +import * as vsTreeExt from 'vs/workbench/api/common/extHostTreeViews'; import { Emitter } from 'vs/base/common/event'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/sql/workbench/api/node/extHostNotebook.ts b/src/sql/workbench/api/node/extHostNotebook.ts index 8896644fad..e74df3c799 100644 --- a/src/sql/workbench/api/node/extHostNotebook.ts +++ b/src/sql/workbench/api/node/extHostNotebook.ts @@ -8,7 +8,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { localize } from 'vs/nls'; import { URI, UriComponents } from 'vs/base/common/uri'; diff --git a/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts index 412695156b..15e0d34941 100644 --- a/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts @@ -10,8 +10,8 @@ import * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; -import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ok } from 'vs/base/common/assert'; diff --git a/src/sql/workbench/api/node/extHostResourceProvider.ts b/src/sql/workbench/api/node/extHostResourceProvider.ts index 0277e0c9a2..822e83142f 100644 --- a/src/sql/workbench/api/node/extHostResourceProvider.ts +++ b/src/sql/workbench/api/node/extHostResourceProvider.ts @@ -7,7 +7,7 @@ import * as azdata from 'azdata'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { ExtHostResourceProviderShape, MainThreadResourceProviderShape, diff --git a/src/sql/workbench/api/node/extHostSerializationProvider.ts b/src/sql/workbench/api/node/extHostSerializationProvider.ts index 2b80c381cb..c1971e2142 100644 --- a/src/sql/workbench/api/node/extHostSerializationProvider.ts +++ b/src/sql/workbench/api/node/extHostSerializationProvider.ts @@ -8,7 +8,7 @@ import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { SqlMainContext, MainThreadSerializationProviderShape, ExtHostSerializationProviderShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; class SerializationAdapter { private _provider: azdata.SerializationProvider; diff --git a/src/sql/workbench/api/node/extHostTasks.ts b/src/sql/workbench/api/node/extHostTasks.ts index 84db3c28dd..bbda511697 100644 --- a/src/sql/workbench/api/node/extHostTasks.ts +++ b/src/sql/workbench/api/node/extHostTasks.ts @@ -7,7 +7,7 @@ import { validateConstraint } from 'vs/base/common/types'; import { ILogService } from 'vs/platform/log/common/log'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; -import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import * as azdata from 'azdata'; diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 005cf76f46..9c0e2a4fff 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -7,7 +7,6 @@ import * as extHostApi from 'vs/workbench/api/node/extHost.api.impl'; import { IInitData, IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; -import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; import { URI } from 'vs/base/common/uri'; import * as azdata from 'azdata'; @@ -20,8 +19,6 @@ import { ExtHostDataProtocol } from 'sql/workbench/api/node/extHostDataProtocol' import { ExtHostSerializationProvider } from 'sql/workbench/api/node/extHostSerializationProvider'; import { ExtHostResourceProvider } from 'sql/workbench/api/node/extHostResourceProvider'; import * as sqlExtHostTypes from 'sql/workbench/api/common/sqlExtHostTypes'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; -import { ExtHostConfiguration, ExtHostConfigProvider } from 'vs/workbench/api/node/extHostConfiguration'; import { ExtHostModalDialogs } from 'sql/workbench/api/node/extHostModalDialog'; import { ExtHostTasks } from 'sql/workbench/api/node/extHostTasks'; import { ExtHostDashboardWebviews } from 'sql/workbench/api/node/extHostDashboardWebview'; @@ -29,18 +26,21 @@ import { ExtHostModelView } from 'sql/workbench/api/node/extHostModelView'; import { ExtHostConnectionManagement } from 'sql/workbench/api/node/extHostConnectionManagement'; import { ExtHostDashboard } from 'sql/workbench/api/node/extHostDashboard'; import { ExtHostObjectExplorer } from 'sql/workbench/api/node/extHostObjectExplorer'; -import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; +import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog'; import { ExtHostModelViewTreeViews } from 'sql/workbench/api/node/extHostModelViewTree'; import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor'; import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement'; import { ExtHostNotebook } from 'sql/workbench/api/node/extHostNotebook'; import { ExtHostNotebookDocumentsAndEditors } from 'sql/workbench/api/node/extHostNotebookDocumentsAndEditors'; -import { ExtHostStorage } from 'vs/workbench/api/node/extHostStorage'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ExtHostExtensionManagement } from 'sql/workbench/api/node/extHostExtensionManagement'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { TernarySearchTree } from 'vs/base/common/map'; +import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { ExtHostConfiguration, ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; +import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; export interface ISqlExtensionApiFactory { vsCodeFactory(extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; diff --git a/src/sqltest/parts/insights/insightsUtils.test.ts b/src/sqltest/parts/insights/insightsUtils.test.ts index 27b47545c7..ec9eaf4175 100644 --- a/src/sqltest/parts/insights/insightsUtils.test.ts +++ b/src/sqltest/parts/insights/insightsUtils.test.ts @@ -3,11 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equal } from 'assert'; +import { equal, fail } from 'assert'; import * as os from 'os'; import { resolveQueryFilePath } from 'sql/workbench/services/insights/common/insightsUtils'; -import { TestWindowService } from 'sqltest/stubs/windowTestService'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; @@ -16,69 +15,129 @@ import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { TestContextService } from 'vs/workbench/test/workbenchTestServices'; +import { IExtensionHostDebugParams, IDebugParams, ParsedArgs } from 'vs/platform/environment/common/environment'; +import { URI } from 'vs/base/common/uri'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; + +class TestEnvironmentService implements IWorkbenchEnvironmentService { + machineSettingsHome: string; + machineSettingsPath: string; + extensionDevelopmentLocationURI?: URI[]; + + constructor(private userEnv: { [key: string]: any }) { + + } + + get configuration(): IWindowConfiguration { + return { + userEnv: this.userEnv + } as IWindowConfiguration; + } + + _serviceBrand: any; + args: ParsedArgs; + execPath: string; + cliPath: string; + appRoot: string; + userHome: string; + userDataPath: string; + appNameLong: string; + appQuality?: string; + appSettingsHome: string; + appSettingsPath: string; + appKeybindingsPath: string; + settingsSearchBuildId?: number; + settingsSearchUrl?: string; + globalStorageHome: string; + workspaceStorageHome: string; + backupHome: string; + backupWorkspacesPath: string; + untitledWorkspacesHome: URI; + isExtensionDevelopment: boolean; + disableExtensions: boolean | string[]; + builtinExtensionsPath: string; + extensionsPath: string; + extensionTestsLocationURI?: URI; + debugExtensionHost: IExtensionHostDebugParams; + debugSearch: IDebugParams; + logExtensionHostCommunication: boolean; + isBuilt: boolean; + wait: boolean; + status: boolean; + log?: string; + logsPath: string; + verbose: boolean; + skipGettingStarted: boolean; + skipReleaseNotes: boolean; + skipAddToRecentlyOpened: boolean; + mainIPCHandle: string; + sharedIPCHandle: string; + nodeCachedDataDir?: string; + installSourcePath: string; + disableUpdates: boolean; + disableCrashReporter: boolean; + driverHandle?: string; + driverVerbose: boolean; +} suite('Insights Utils tests', function () { let testRootPath: string; let queryFileDir: string; let queryFilePath: string; - suiteSetup(done => { + suiteSetup(async () => { // Create test file - just needs to exist for verifying the path resolution worked correctly testRootPath = path.join(os.tmpdir(), 'adstests'); queryFileDir = getRandomTestPath(testRootPath, 'insightsutils'); - pfs.mkdirp(queryFileDir).then(() => { - queryFilePath = path.join(queryFileDir, 'test.sql'); - pfs.writeFile(queryFilePath, '').then(done()); - }); - + await pfs.mkdirp(queryFileDir); + queryFilePath = path.join(queryFileDir, 'test.sql'); + await pfs.writeFile(queryFilePath, ''); }); test('resolveQueryFilePath resolves path correctly with fully qualified path', async () => { - let configurationResolverService = new ConfigurationResolverService( - new TestWindowService({}), - undefined, + const configurationResolverService = new ConfigurationResolverService( undefined, + new TestEnvironmentService({}), undefined, undefined, new TestContextService(), undefined); - let resolvedPath = await resolveQueryFilePath(queryFilePath, new TestContextService(), configurationResolverService); + const resolvedPath = await resolveQueryFilePath(queryFilePath, new TestContextService(), configurationResolverService); equal(resolvedPath, queryFilePath); }); test('resolveQueryFilePath resolves path correctly with workspaceRoot var and non-empty workspace containing file', async () => { // Create mock context service with our test folder added as a workspace folder for resolution - let contextService = new TestContextService( + const contextService = new TestContextService( new Workspace( 'TestWorkspace', toWorkspaceFolders([{ path: queryFileDir }]) )); - let configurationResolverService = new ConfigurationResolverService( - new TestWindowService({}), - undefined, + const configurationResolverService = new ConfigurationResolverService( undefined, + new TestEnvironmentService({}), undefined, undefined, contextService, undefined); - let resolvedPath = await resolveQueryFilePath(path.join('${workspaceRoot}', 'test.sql'), contextService, configurationResolverService); + const resolvedPath = await resolveQueryFilePath(path.join('${workspaceRoot}', 'test.sql'), contextService, configurationResolverService); equal(resolvedPath, queryFilePath); }); test('resolveQueryFilePath throws with workspaceRoot var and non-empty workspace not containing file', async (done) => { - let tokenizedPath = path.join('${workspaceRoot}', 'test.sql'); + const tokenizedPath = path.join('${workspaceRoot}', 'test.sql'); // Create mock context service with a folder NOT containing our test file to verify it returns original path - let contextService = new TestContextService( + const contextService = new TestContextService( new Workspace( 'TestWorkspace', toWorkspaceFolders([{ path: os.tmpdir() }]) )); - let configurationResolverService = new ConfigurationResolverService( - new TestWindowService({}), - undefined, + const configurationResolverService = new ConfigurationResolverService( undefined, + new TestEnvironmentService({}), undefined, undefined, contextService, @@ -86,6 +145,7 @@ suite('Insights Utils tests', function () { try { await resolveQueryFilePath(tokenizedPath, contextService, configurationResolverService); + fail('Should have thrown'); } catch (e) { done(); @@ -93,15 +153,14 @@ suite('Insights Utils tests', function () { }); test('resolveQueryFilePath throws with workspaceRoot var and empty workspace', async (done) => { - let tokenizedPath = path.join('${workspaceRoot}', 'test.sql'); + const tokenizedPath = path.join('${workspaceRoot}', 'test.sql'); // Create mock context service with an empty workspace - let contextService = new TestContextService( + const contextService = new TestContextService( new Workspace( 'TestWorkspace')); - let configurationResolverService = new ConfigurationResolverService( - new TestWindowService({}), - undefined, + const configurationResolverService = new ConfigurationResolverService( undefined, + new TestEnvironmentService({}), undefined, undefined, contextService, @@ -109,6 +168,7 @@ suite('Insights Utils tests', function () { try { await resolveQueryFilePath(tokenizedPath, contextService, configurationResolverService); + fail('Should have thrown'); } catch (e) { done(); @@ -116,47 +176,44 @@ suite('Insights Utils tests', function () { }); test('resolveQueryFilePath resolves path correctly with env var and empty workspace', async () => { - let contextService = new TestContextService( + const contextService = new TestContextService( new Workspace('TestWorkspace')); // Create mock window service with env variable containing test folder for resolution - let configurationResolverService = new ConfigurationResolverService( - new TestWindowService({ TEST_PATH: queryFileDir }), - undefined, + const configurationResolverService = new ConfigurationResolverService( undefined, + new TestEnvironmentService({ TEST_PATH: queryFileDir }), undefined, undefined, undefined, undefined); - let resolvedPath = await resolveQueryFilePath(path.join('${env:TEST_PATH}', 'test.sql'), contextService, configurationResolverService); + const resolvedPath = await resolveQueryFilePath(path.join('${env:TEST_PATH}', 'test.sql'), contextService, configurationResolverService); equal(resolvedPath, queryFilePath); }); test('resolveQueryFilePath resolves path correctly with env var and non-empty workspace', async () => { - let contextService = new TestContextService( + const contextService = new TestContextService( new Workspace('TestWorkspace', toWorkspaceFolders([{ path: os.tmpdir() }]))); // Create mock window service with env variable containing test folder for resolution - let configurationResolverService = new ConfigurationResolverService( - new TestWindowService({ TEST_PATH: queryFileDir }), - undefined, + const configurationResolverService = new ConfigurationResolverService( undefined, + new TestEnvironmentService({ TEST_PATH: queryFileDir }), undefined, undefined, undefined, undefined); - let resolvedPath = await resolveQueryFilePath(path.join('${env:TEST_PATH}', 'test.sql'), contextService, configurationResolverService); + const resolvedPath = await resolveQueryFilePath(path.join('${env:TEST_PATH}', 'test.sql'), contextService, configurationResolverService); equal(resolvedPath, queryFilePath); }); test('resolveQueryFilePath throws if invalid param var specified', async (done) => { - let invalidPath = path.join('${INVALID}', 'test.sql'); - let configurationResolverService = new ConfigurationResolverService( - new TestWindowService({}), - undefined, + const invalidPath = path.join('${INVALID}', 'test.sql'); + const configurationResolverService = new ConfigurationResolverService( undefined, + new TestEnvironmentService({}), undefined, undefined, undefined, @@ -164,8 +221,8 @@ suite('Insights Utils tests', function () { try { await resolveQueryFilePath(invalidPath, new TestContextService(), configurationResolverService); - } - catch (e) { + fail('Should have thrown'); + } catch (e) { done(); } diff --git a/src/sqltest/stubs/editorGroupService.ts b/src/sqltest/stubs/editorGroupService.ts index f19e81e1ee..4302a320ee 100644 --- a/src/sqltest/stubs/editorGroupService.ts +++ b/src/sqltest/stubs/editorGroupService.ts @@ -15,6 +15,7 @@ import { IDimension } from 'vs/editor/common/editorCommon'; import { IDisposable } from 'vs/base/common/lifecycle'; export class EditorGroupTestService implements IEditorGroupsService { + willRestoreEditors: boolean; dimension: IDimension; whenRestored: Promise; centerLayout(active: boolean): void { diff --git a/src/vs/base/browser/mouseEvent.ts b/src/vs/base/browser/mouseEvent.ts index bdc02ac20e..f4040c6300 100644 --- a/src/vs/base/browser/mouseEvent.ts +++ b/src/vs/base/browser/mouseEvent.ts @@ -115,6 +115,13 @@ export class DragMouseEvent extends StandardMouseEvent { export interface IMouseWheelEvent extends MouseEvent { readonly wheelDelta: number; + readonly wheelDeltaX: number; + readonly wheelDeltaY: number; + + readonly deltaX: number; + readonly deltaY: number; + readonly deltaZ: number; + readonly deltaMode: number; } interface IWebKitMouseWheelEvent { diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index ce5bb1d562..fc14fd4782 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ declare var Buffer: any; -const hasBuffer = (typeof Buffer !== 'undefined'); +export const hasBuffer = (typeof Buffer !== 'undefined'); let textEncoder: TextEncoder | null; let textDecoder: TextDecoder | null; @@ -20,6 +20,11 @@ export class VSBuffer { } public static wrap(actual: Uint8Array): VSBuffer { + if (hasBuffer && !(Buffer.isBuffer(actual))) { + // https://nodejs.org/dist/latest-v10.x/docs/api/buffer.html#buffer_class_method_buffer_from_arraybuffer_byteoffset_length + // Create a zero-copy Buffer wrapper around the ArrayBuffer pointed to by the Uint8Array + actual = Buffer.from(actual.buffer, actual.byteOffset, actual.byteLength); + } return new VSBuffer(actual); } @@ -124,3 +129,44 @@ function readUint8(source: Uint8Array, offset: number): number { function writeUint8(destination: Uint8Array, value: number, offset: number): void { destination[offset] = value; } + +export interface VSBufferReadable { + + /** + * Read data from the underlying source. Will return + * null to indicate that no more data can be read. + */ + read(): VSBuffer | null; +} + +/** + * Helper to fully read a VSBuffer readable into a single buffer. + */ +export function readableToBuffer(readable: VSBufferReadable): VSBuffer { + const chunks: VSBuffer[] = []; + + let chunk: VSBuffer | null; + while (chunk = readable.read()) { + chunks.push(chunk); + } + + return VSBuffer.concat(chunks); +} + +/** + * Helper to convert a buffer into a readable buffer. + */ +export function bufferToReadable(buffer: VSBuffer): VSBufferReadable { + let done = false; + return { + read: () => { + if (done) { + return null; + } + + done = true; + + return buffer; + } + }; +} \ No newline at end of file diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 9a1390cec0..c34b499e89 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -265,33 +265,6 @@ export namespace Event { return emitter.event; } - /** - * Similar to `buffer` but it buffers indefinitely and repeats - * the buffered events to every new listener. - */ - export function echo(event: Event, nextTick = false, buffer: T[] = []): Event { - buffer = buffer.slice(); - - event(e => { - buffer.push(e); - emitter.fire(e); - }); - - const flush = (listener: (e: T) => any, thisArgs?: any) => buffer.forEach(e => listener.call(thisArgs, e)); - - const emitter = new Emitter({ - onListenerDidAdd(emitter: Emitter, listener: (e: T) => any, thisArgs?: any) { - if (nextTick) { - setTimeout(() => flush(listener, thisArgs)); - } else { - flush(listener, thisArgs); - } - } - }); - - return emitter.event; - } - export interface IChainableEvent { event: Event; map(fn: (i: T) => O): IChainableEvent; diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 7a0a7071d1..926ad30a8d 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -44,4 +44,6 @@ export namespace Schemas { export const data: string = 'data'; export const command: string = 'command'; + + export const vscodeRemote: string = 'vscode-remote'; } diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 09ecf79499..dde4a358fd 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -284,7 +284,6 @@ export namespace DataUri { } } - export class ResourceGlobMatcher { private readonly globalExpression: ParsedExpression; @@ -311,3 +310,16 @@ export class ResourceGlobMatcher { return !!this.globalExpression(resource.path); } } + +export function toLocalResource(resource: URI, authority: string | undefined): URI { + if (authority) { + let path = resource.path; + if (path && path[0] !== paths.posix.sep) { + path = paths.posix.sep + path; + } + + return resource.with({ scheme: Schemas.vscodeRemote, authority, path }); + } + + return resource.with({ scheme: Schemas.file }); +} \ No newline at end of file diff --git a/src/vs/base/test/common/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts new file mode 100644 index 0000000000..a158aeeb51 --- /dev/null +++ b/src/vs/base/test/common/buffer.test.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as assert from 'assert'; +import { hasBuffer, VSBuffer } from 'vs/base/common/buffer'; + +suite('Buffer', () => { + + if (hasBuffer) { + test('issue #71993 - VSBuffer#toString returns numbers', () => { + const data = new Uint8Array([1, 2, 3, 'h'.charCodeAt(0), 'i'.charCodeAt(0), 4, 5]).buffer; + const buffer = VSBuffer.wrap(new Uint8Array(data, 3, 2)); + assert.deepEqual(buffer.toString(), 'hi'); + }); + } + +}); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index a475715729..3c40821802 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -645,67 +645,6 @@ suite('Event utils', () => { }); }); - suite('echo', () => { - - test('should echo events', () => { - const result: number[] = []; - const emitter = new Emitter(); - const event = emitter.event; - const echoEvent = Event.echo(event); - - emitter.fire(1); - emitter.fire(2); - emitter.fire(3); - assert.deepEqual(result, []); - - const listener = echoEvent(num => result.push(num)); - assert.deepEqual(result, [1, 2, 3]); - - emitter.fire(4); - assert.deepEqual(result, [1, 2, 3, 4]); - - listener.dispose(); - emitter.fire(5); - assert.deepEqual(result, [1, 2, 3, 4]); - }); - - test('should echo events for every listener', () => { - const result1: number[] = []; - const result2: number[] = []; - const emitter = new Emitter(); - const event = emitter.event; - const echoEvent = Event.echo(event); - - emitter.fire(1); - emitter.fire(2); - emitter.fire(3); - assert.deepEqual(result1, []); - assert.deepEqual(result2, []); - - const listener1 = echoEvent(num => result1.push(num)); - assert.deepEqual(result1, [1, 2, 3]); - assert.deepEqual(result2, []); - - emitter.fire(4); - assert.deepEqual(result1, [1, 2, 3, 4]); - assert.deepEqual(result2, []); - - const listener2 = echoEvent(num => result2.push(num)); - assert.deepEqual(result1, [1, 2, 3, 4]); - assert.deepEqual(result2, [1, 2, 3, 4]); - - emitter.fire(5); - assert.deepEqual(result1, [1, 2, 3, 4, 5]); - assert.deepEqual(result2, [1, 2, 3, 4, 5]); - - listener1.dispose(); - listener2.dispose(); - emitter.fire(6); - assert.deepEqual(result1, [1, 2, 3, 4, 5]); - assert.deepEqual(result2, [1, 2, 3, 4, 5]); - }); - }); - suite('EventMultiplexer', () => { test('works', () => { diff --git a/src/vs/code/electron-browser/issue/media/issueReporter.css b/src/vs/code/electron-browser/issue/media/issueReporter.css index de93d8bd3c..433cc91fdc 100644 --- a/src/vs/code/electron-browser/issue/media/issueReporter.css +++ b/src/vs/code/electron-browser/issue/media/issueReporter.css @@ -24,6 +24,10 @@ td { vertical-align: top; } +tr td:first-child { + width: 30%; +} + label { user-select: none; } diff --git a/src/vs/code/electron-browser/processExplorer/media/collapsed.svg b/src/vs/code/electron-browser/processExplorer/media/collapsed.svg new file mode 100755 index 0000000000..3a63808c35 --- /dev/null +++ b/src/vs/code/electron-browser/processExplorer/media/collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/code/electron-browser/processExplorer/media/expanded.svg b/src/vs/code/electron-browser/processExplorer/media/expanded.svg new file mode 100755 index 0000000000..75f73adbb0 --- /dev/null +++ b/src/vs/code/electron-browser/processExplorer/media/expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css index a253de40ec..b9b703bdef 100644 --- a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css @@ -85,6 +85,20 @@ td { height: 22px; } +.error { + padding-left: 20px; + white-space: nowrap; +} + tbody > tr:hover { background-color: #2A2D2E; } + +.hidden { + display: none; +} + +img { + width: 16px; + margin-right: 4px; +} \ No newline at end of file diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index 8e4b913dcb..f3e4ef527b 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/processExplorer'; -import { listProcesses } from 'vs/base/node/ps'; import { webFrame, ipcRenderer, clipboard } from 'electron'; import { repeat } from 'vs/base/common/strings'; import { totalmem } from 'os'; @@ -16,31 +15,46 @@ import * as platform from 'vs/base/common/platform'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-browser/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; +import { addDisposableListener } from 'vs/base/browser/dom'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; + -let processList: any[]; let mapPidToWindowTitle = new Map(); const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; +const listeners: IDisposable[] = []; +const collapsedStateCache: Map = new Map(); +let lastRequestTime: number; -function getProcessList(rootProcess: ProcessItem) { - const processes: any[] = []; +interface FormattedProcessItem { + cpu: string; + memory: string; + pid: string; + name: string; + formattedName: string; + cmd: string; +} + +function getProcessList(rootProcess: ProcessItem, isLocal: boolean): FormattedProcessItem[] { + const processes: FormattedProcessItem[] = []; if (rootProcess) { - getProcessItem(processes, rootProcess, 0); + getProcessItem(processes, rootProcess, 0, isLocal); } return processes; } -function getProcessItem(processes: any[], item: ProcessItem, indent: number): void { +function getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean): void { const isRoot = (indent === 0); const MB = 1024 * 1024; let name = item.name; if (isRoot) { - name = `${product.applicationName} main`; + name = isLocal ? `${product.applicationName} main` : 'remote agent'; } if (name === 'window') { @@ -52,9 +66,9 @@ function getProcessItem(processes: any[], item: ProcessItem, indent: number): vo const formattedName = isRoot ? name : `${repeat(' ', indent)} ${name}`; const memory = process.platform === 'win32' ? item.mem : (totalmem() * (item.mem / 100)); processes.push({ - cpu: Number(item.load.toFixed(0)), - memory: Number((memory / MB).toFixed(0)), - pid: Number((item.pid).toFixed(0)), + cpu: item.load.toFixed(0), + memory: (memory / MB).toFixed(0), + pid: item.pid.toFixed(0), name, formattedName, cmd: item.cmd @@ -62,7 +76,7 @@ function getProcessItem(processes: any[], item: ProcessItem, indent: number): vo // Recurse into children if any if (Array.isArray(item.children)) { - item.children.forEach(child => getProcessItem(processes, child, indent + 1)); + item.children.forEach(child => getProcessItem(processes, child, indent + 1, isLocal)); } } @@ -71,7 +85,7 @@ function isDebuggable(cmd: string): boolean { return (matches && matches.length >= 2) || cmd.indexOf('node ') >= 0 || cmd.indexOf('node.exe') >= 0; } -function attachTo(item: ProcessItem) { +function attachTo(item: FormattedProcessItem) { const config: any = { type: 'node', request: 'attach', @@ -113,35 +127,82 @@ function getProcessIdWithHighestProperty(processList: any[], propertyName: strin return maxProcessId; } -function updateProcessInfo(processList: any[]): void { +function updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: HTMLImageElement, sectionName: string) { + if (shouldExpand) { + body.classList.remove('hidden'); + collapsedStateCache.set(sectionName, false); + twistie.src = './media/expanded.svg'; + } else { + body.classList.add('hidden'); + collapsedStateCache.set(sectionName, true); + twistie.src = './media/collapsed.svg'; + } +} + +function renderProcessFetchError(sectionName: string, errorMessage: string) { + const container = document.getElementById('process-list'); + if (!container) { + return; + } + + const body = document.createElement('tbody'); + + renderProcessGroupHeader(sectionName, body, container); + + const errorRow = document.createElement('tr'); + const data = document.createElement('td'); + data.textContent = errorMessage; + data.className = 'error'; + data.colSpan = 4; + errorRow.appendChild(data); + + body.appendChild(errorRow); + container.appendChild(body); +} + +function renderProcessGroupHeader(sectionName: string, body: HTMLElement, container: HTMLElement) { + const headerRow = document.createElement('tr'); + const data = document.createElement('td'); + data.textContent = sectionName; + data.colSpan = 4; + headerRow.appendChild(data); + + const twistie = document.createElement('img'); + updateSectionCollapsedState(!collapsedStateCache.get(sectionName), body, twistie, sectionName); + data.prepend(twistie); + + listeners.push(addDisposableListener(data, 'click', (e) => { + const isHidden = body.classList.contains('hidden'); + updateSectionCollapsedState(isHidden, body, twistie, sectionName); + })); + + container.appendChild(headerRow); +} + +function renderTableSection(sectionName: string, processList: FormattedProcessItem[], renderManySections: boolean, sectionIsLocal: boolean): void { const container = document.getElementById('process-list'); if (!container) { return; } - container.innerHTML = ''; const highestCPUProcess = getProcessIdWithHighestProperty(processList, 'cpu'); const highestMemoryProcess = getProcessIdWithHighestProperty(processList, 'memory'); - const tableHead = document.createElement('thead'); - tableHead.innerHTML = ` - ${localize('cpu', "CPU %")} - ${localize('memory', "Memory (MB)")} - ${localize('pid', "pid")} - ${localize('name', "Name")} - `; + const body = document.createElement('tbody'); - const tableBody = document.createElement('tbody'); + if (renderManySections) { + renderProcessGroupHeader(sectionName, body, container); + } processList.forEach(p => { const row = document.createElement('tr'); - row.id = p.pid; + row.id = p.pid.toString(); const cpu = document.createElement('td'); p.pid === highestCPUProcess ? cpu.classList.add('centered', 'highest') : cpu.classList.add('centered'); - cpu.textContent = p.cpu; + cpu.textContent = p.cpu.toString(); const memory = document.createElement('td'); p.pid === highestMemoryProcess @@ -160,10 +221,45 @@ function updateProcessInfo(processList: any[]): void { name.textContent = p.formattedName; row.append(cpu, memory, pid, name); - tableBody.appendChild(row); + + listeners.push(addDisposableListener(row, 'contextmenu', (e) => { + showContextMenu(e, p, sectionIsLocal); + })); + + body.appendChild(row); }); - container.append(tableHead, tableBody); + container.appendChild(body); +} + +function updateProcessInfo(processLists: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]): void { + const container = document.getElementById('process-list'); + if (!container) { + return; + } + + container.innerHTML = ''; + listeners.forEach(l => l.dispose()); + + const tableHead = document.createElement('thead'); + tableHead.innerHTML = ` + ${localize('cpu', "CPU %")} + ${localize('memory', "Memory (MB)")} + ${localize('pid', "pid")} + ${localize('name', "Name")} + `; + + container.append(tableHead); + + const hasMultipleMachines = Object.keys(processLists).length > 1; + processLists.forEach((remote, i) => { + const isLocal = i === 0; + if (isRemoteDiagnosticError(remote.rootProcess)) { + renderProcessFetchError(remote.name, remote.rootProcess.errorMessage); + } else { + renderTableSection(remote.name, getProcessList(remote.rootProcess, isLocal), hasMultipleMachines, isLocal); + } + }); } function applyStyles(styles: ProcessExplorerStyles): void { @@ -171,11 +267,11 @@ function applyStyles(styles: ProcessExplorerStyles): void { const content: string[] = []; if (styles.hoverBackground) { - content.push(`tbody > tr:hover { background-color: ${styles.hoverBackground}; }`); + content.push(`tbody > tr:hover, table > tr:hover { background-color: ${styles.hoverBackground}; }`); } if (styles.hoverForeground) { - content.push(`tbody > tr:hover{ color: ${styles.hoverForeground}; }`); + content.push(`tbody > tr:hover, table > tr:hover { color: ${styles.hoverForeground}; }`); } if (styles.highlightForeground) { @@ -200,13 +296,13 @@ function applyZoom(zoomLevel: number): void { browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false); } -function showContextMenu(e: MouseEvent) { +function showContextMenu(e: MouseEvent, item: FormattedProcessItem, isLocal: boolean) { e.preventDefault(); const items: IContextMenuItem[] = []; + const pid = Number(item.pid); - const pid = parseInt((e.currentTarget as HTMLElement).id); - if (pid && typeof pid === 'number') { + if (isLocal) { items.push({ label: localize('killProcess', "Kill Process"), click() { @@ -224,48 +320,37 @@ function showContextMenu(e: MouseEvent) { items.push({ type: 'separator' }); + } - items.push({ - label: localize('copy', "Copy"), - click() { - const row = document.getElementById(pid.toString()); - if (row) { - clipboard.writeText(row.innerText); - } + items.push({ + label: localize('copy', "Copy"), + click() { + const row = document.getElementById(pid.toString()); + if (row) { + clipboard.writeText(row.innerText); } - }); - - items.push({ - label: localize('copyAll', "Copy All"), - click() { - const processList = document.getElementById('process-list'); - if (processList) { - clipboard.writeText(processList.innerText); - } - } - }); - - const item = processList.filter(process => process.pid === pid)[0]; - if (item && isDebuggable(item.cmd)) { - items.push({ - type: 'separator' - }); - - items.push({ - label: localize('debug', "Debug"), - click() { - attachTo(item); - } - }); } - } else { + }); + + items.push({ + label: localize('copyAll', "Copy All"), + click() { + const processList = document.getElementById('process-list'); + if (processList) { + clipboard.writeText(processList.innerText); + } + } + }); + + if (item && isLocal && isDebuggable(item.cmd)) { items.push({ - label: localize('copyAll', "Copy All"), + type: 'separator' + }); + + items.push({ + label: localize('debug', "Debug"), click() { - const processList = document.getElementById('process-list'); - if (processList) { - clipboard.writeText(processList.innerText); - } + attachTo(item); } }); } @@ -273,6 +358,22 @@ function showContextMenu(e: MouseEvent) { popup(items); } +function requestProcessList(totalWaitTime: number): void { + setTimeout(() => { + const nextRequestTime = Date.now(); + const waited = totalWaitTime + nextRequestTime - lastRequestTime; + lastRequestTime = nextRequestTime; + + // Wait at least a second between requests. + if (waited > 1000) { + ipcRenderer.send('windowsInfoRequest'); + ipcRenderer.send('vscode:listProcesses'); + } else { + requestProcessList(waited); + } + }, 200); +} + export function startup(data: ProcessExplorerData): void { applyStyles(data.styles); applyZoom(data.zoomLevel); @@ -283,23 +384,14 @@ export function startup(data: ProcessExplorerData): void { windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title)); }); - setInterval(() => { - ipcRenderer.send('windowsInfoRequest'); - - listProcesses(data.pid).then(processes => { - processList = getProcessList(processes); - updateProcessInfo(processList); - - const tableRows = document.getElementsByTagName('tr'); - for (let i = 0; i < tableRows.length; i++) { - const tableRow = tableRows[i]; - tableRow.addEventListener('contextmenu', (e) => { - showContextMenu(e); - }); - } - }); - }, 1200); + ipcRenderer.on('vscode:listProcessesResponse', (_event: Event, processRoots: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]) => { + updateProcessInfo(processRoots); + requestProcessList(0); + }); + lastRequestTime = Date.now(); + ipcRenderer.send('windowsInfoRequest'); + ipcRenderer.send('vscode:listProcesses'); document.onkeydown = (e: KeyboardEvent) => { const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index a01c89b315..2f1c55e3c9 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -101,7 +101,7 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I services.set(IEnvironmentService, environmentService); services.set(ILogService, logService); - services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); + services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.appSettingsPath])); services.set(IRequestService, new SyncDescriptor(RequestService)); services.set(IDownloadService, new SyncDescriptor(DownloadService)); @@ -121,7 +121,7 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I instantiationService.invokeFunction(accessor => { const services = new ServiceCollection(); const environmentService = accessor.get(IEnvironmentService); - const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService; + const { appRoot, extensionsPath, extensionDevelopmentLocationURI: extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService; const telemetryLogService = new FollowerLogService(logLevelClient, createSpdLogService('telemetry', initData.logLevel, environmentService.logsPath)); telemetryLogService.info('The below are logs for every telemetry event sent from VS Code once the log level is set to trace.'); telemetryLogService.info('==========================================================='); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 81e3adba08..2073b305eb 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -67,7 +67,7 @@ import { storeBackgroundColor } from 'vs/code/electron-main/theme'; import { homedir } from 'os'; import { join, sep } from 'vs/base/common/path'; import { localize } from 'vs/nls'; -import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; +import { Schemas } from 'vs/base/common/network'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/remote/common/remoteAgentFileSystemChannel'; import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap'; @@ -748,7 +748,7 @@ export class CodeApplication extends Disposable { } }; - protocol.registerBufferProtocol(REMOTE_HOST_SCHEME, async (request, callback) => { + protocol.registerBufferProtocol(Schemas.vscodeRemote, async (request, callback) => { if (request.method !== 'GET') { return callback(undefined); } diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index b8caac5c9f..b4d897b858 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -307,7 +307,7 @@ function createServices(args: ParsedArgs, bufferLogService: BufferLogService): I services.set(ILogService, logService); services.set(ILifecycleService, new SyncDescriptor(LifecycleService)); services.set(IStateService, new SyncDescriptor(StateService)); - services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); + services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.appSettingsPath])); services.set(IRequestService, new SyncDescriptor(RequestService)); services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 4d0ca9a71f..f2ae8517da 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -320,6 +320,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => { this.marketplaceHeadersPromise.then(headers => { const requestHeaders = objects.assign(details.requestHeaders, headers); + if (!this.configurationService.getValue('extensions.disableExperimentalAzureSearch')) { + requestHeaders['Cookie'] = `${requestHeaders['Cookie'] ? requestHeaders['Cookie'] + ';' : ''}EnableExternalSearchForVSCode=true`; + } cb({ cancel: false, requestHeaders }); }); }); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 0c049c37ff..bf2c6afc32 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -24,7 +24,8 @@ function shouldSpawnCliProcess(argv: ParsedArgs): boolean { return !!argv['install-source'] || !!argv['list-extensions'] || !!argv['install-extension'] - || !!argv['uninstall-extension']; + || !!argv['uninstall-extension'] + || !!argv['locate-extension']; } interface IMainCli { diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 8e3d0cb7e2..a3a1971787 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -41,6 +41,7 @@ import { IExtensionManifest, ExtensionType, isLanguagePackExtension } from 'vs/p import { isUIExtension } from 'vs/platform/extensions/node/extensionsUtil'; import { CancellationToken } from 'vs/base/common/cancellation'; import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { Schemas } from 'vs/base/common/network'; const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); @@ -92,6 +93,10 @@ export class Main { const arg = argv['uninstall-extension']; const ids: string[] = typeof arg === 'string' ? [arg] : arg; await this.uninstallExtension(ids); + } else if (argv['locate-extension']) { + const arg = argv['locate-extension']; + const ids: string[] = typeof arg === 'string' ? [arg] : arg; + await this.locateExtension(ids); } } @@ -257,6 +262,20 @@ export class Main { } } + private async locateExtension(extensions: string[]): Promise { + const installed = await this.extensionManagementService.getInstalled(); + extensions.forEach(e => { + installed.forEach(i => { + if (i.identifier.id === e) { + if (i.location.scheme === Schemas.file) { + console.log(i.location.fsPath); + return; + } + } + }); + }); + } + private async updateLocalizationsCache(): Promise { const localizationService = this.instantiationService.createInstance(LocalizationsService); await localizationService.update(); @@ -286,10 +305,10 @@ export function main(argv: ParsedArgs): Promise { const stateService = accessor.get(IStateService); return Promise.all([envService.appSettingsHome, envService.extensionsPath].map(p => mkdirp(p))).then(() => { - const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = envService; + const { appRoot, extensionsPath, extensionDevelopmentLocationURI: extensionDevelopmentLocationURI, isBuilt, installSourcePath } = envService; const services = new ServiceCollection(); - services.set(IConfigurationService, new SyncDescriptor(ConfigurationService)); + services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.appSettingsPath])); services.set(IRequestService, new SyncDescriptor(RequestService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService, [false])); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); @@ -321,4 +340,4 @@ export function main(argv: ParsedArgs): Promise { }); }); }); -} +} \ No newline at end of file diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index b0adac76e5..480440d5c3 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -109,6 +109,8 @@ export class MouseHandler extends ViewEventHandler { this._register(mouseEvents.onMouseDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e))); const onMouseWheel = (browserEvent: IMouseWheelEvent) => { + this.viewController.emitMouseWheel(browserEvent); + if (!this._context.configuration.editor.viewInfo.mouseWheelZoom) { return; } @@ -259,6 +261,10 @@ export class MouseHandler extends ViewEventHandler { target: t }); } + + public _onMouseWheel(e: IMouseWheelEvent): void { + this.viewController.emitMouseWheel(e); + } } class MouseDownOperation extends Disposable { diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 73d3430c5b..cba6e57afd 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IMouseEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { ICursors } from 'vs/editor/common/controller/cursorCommon'; @@ -461,6 +461,12 @@ export interface ICodeEditor extends editorCommon.IEditor { * @event */ onMouseLeave(listener: (e: IPartialEditorMouseEvent) => void): IDisposable; + /** + * An event emitted on a "mousewheel" + * @event + * @internal + */ + onMouseWheel(listener: (e: IMouseWheelEvent) => void): IDisposable; /** * An event emitted on a "keyup". * @event diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index f6a7aad032..7d4b2ceaed 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -11,6 +11,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { IConfiguration } from 'vs/editor/common/editorCommon'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; export interface IMouseDispatchData { position: Position; @@ -316,4 +317,8 @@ export class ViewController { public emitMouseDrop(e: IPartialEditorMouseEvent): void { this.outgoingEvents.emitMouseDrop(e); } + + public emitMouseWheel(e: IMouseWheelEvent): void { + this.outgoingEvents.emitMouseWheel(e); + } } diff --git a/src/vs/editor/browser/view/viewOutgoingEvents.ts b/src/vs/editor/browser/view/viewOutgoingEvents.ts index d1ed8e2e2a..16bdfbcdf7 100644 --- a/src/vs/editor/browser/view/viewOutgoingEvents.ts +++ b/src/vs/editor/browser/view/viewOutgoingEvents.ts @@ -12,6 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IScrollEvent } from 'vs/editor/common/editorCommon'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; export interface EventCallback { (event: T): void; @@ -31,6 +32,7 @@ export class ViewOutgoingEvents extends Disposable { public onMouseDown: EventCallback | null = null; public onMouseDrag: EventCallback | null = null; public onMouseDrop: EventCallback | null = null; + public onMouseWheel: EventCallback | null = null; private readonly _viewModel: IViewModel; @@ -111,6 +113,12 @@ export class ViewOutgoingEvents extends Disposable { } } + public emitMouseWheel(e: IMouseWheelEvent): void { + if (this.onMouseWheel) { + this.onMouseWheel(e); + } + } + private _convertViewToModelMouseEvent(e: IEditorMouseEvent): IEditorMouseEvent; private _convertViewToModelMouseEvent(e: IPartialEditorMouseEvent): IPartialEditorMouseEvent; private _convertViewToModelMouseEvent(e: IEditorMouseEvent | IPartialEditorMouseEvent): IEditorMouseEvent | IPartialEditorMouseEvent { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index ff182783df..01f34523a2 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -8,7 +8,7 @@ import 'vs/css!./media/tokens'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IMouseEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -186,6 +186,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onMouseLeave: Emitter = this._register(new Emitter()); public readonly onMouseLeave: Event = this._onMouseLeave.event; + private readonly _onMouseWheel: Emitter = this._register(new Emitter()); + public readonly onMouseWheel: Event = this._onMouseWheel.event; + private readonly _onKeyUp: Emitter = this._register(new Emitter()); public readonly onKeyUp: Event = this._onKeyUp.event; @@ -1442,6 +1445,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE // In IE, the focus is not synchronous, so we give it a little help this._editorWidgetFocus.setValue(true); }; + viewOutgoingEvents.onDidScroll = (e) => this._onDidScrollChange.fire(e); viewOutgoingEvents.onDidLoseFocus = () => this._editorTextFocus.setValue(false); viewOutgoingEvents.onContextMenu = (e) => this._onContextMenu.fire(e); @@ -1452,6 +1456,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE viewOutgoingEvents.onKeyUp = (e) => this._onKeyUp.fire(e); viewOutgoingEvents.onMouseMove = (e) => this._onMouseMove.fire(e); viewOutgoingEvents.onMouseLeave = (e) => this._onMouseLeave.fire(e); + viewOutgoingEvents.onMouseWheel = (e) => this._onMouseWheel.fire(e); viewOutgoingEvents.onKeyDown = (e) => this._onKeyDown.fire(e); const view = new View( diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 0569682f4a..43c76fe6cf 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1958,7 +1958,7 @@ export class EditorOptionsValidator { }; } - private static _santizeGotoLocationOpts(opts: IEditorOptions, defaults: InternalGoToLocationOptions): InternalGoToLocationOptions { + private static _sanitizeGotoLocationOpts(opts: IEditorOptions, defaults: InternalGoToLocationOptions): InternalGoToLocationOptions { const gotoOpts = opts.gotoLocation || {}; return { multiple: _stringSet<'peek' | 'gotoAndPeek' | 'goto'>(gotoOpts.multiple, defaults.multiple, ['peek', 'gotoAndPeek', 'goto']) @@ -2117,7 +2117,7 @@ export class EditorOptionsValidator { suggestLineHeight: _clampedInt(opts.suggestLineHeight, defaults.suggestLineHeight, 0, 1000), tabCompletion: this._sanitizeTabCompletionOpts(opts.tabCompletion, defaults.tabCompletion), suggest: this._sanitizeSuggestOpts(opts, defaults.suggest), - gotoLocation: this._santizeGotoLocationOpts(opts, defaults.gotoLocation), + gotoLocation: this._sanitizeGotoLocationOpts(opts, defaults.gotoLocation), selectionHighlight: _boolean(opts.selectionHighlight, defaults.selectionHighlight), occurrencesHighlight: _boolean(opts.occurrencesHighlight, defaults.occurrencesHighlight), codeLens: _boolean(opts.codeLens, defaults.codeLens), diff --git a/src/vs/editor/contrib/codeAction/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/codeActionContributions.ts index c00bd90f11..b7b5c3562c 100644 --- a/src/vs/editor/contrib/codeAction/codeActionContributions.ts +++ b/src/vs/editor/contrib/codeAction/codeActionContributions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { CodeActionCommand, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, SourceAction, AutoFixAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { CodeActionCommand, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, SourceAction, AutoFixAction, FixAllAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; registerEditorContribution(QuickFixController); @@ -13,4 +13,5 @@ registerEditorAction(RefactorAction); registerEditorAction(SourceAction); registerEditorAction(OrganizeImportsAction); registerEditorAction(AutoFixAction); +registerEditorAction(FixAllAction); registerEditorCommand(new CodeActionCommand()); diff --git a/src/vs/editor/contrib/contextmenu/contextmenu.ts b/src/vs/editor/contrib/contextmenu/contextmenu.ts index 9ce70f55fa..d6c2cb1595 100644 --- a/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -13,7 +13,7 @@ import { KeyCode, KeyMod, ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { IEditorContribution, IScrollEvent, ScrollType } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -21,6 +21,7 @@ import { IContextMenuService, IContextViewService } from 'vs/platform/contextvie import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITextModel } from 'vs/editor/common/model'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; export class ContextMenuController implements IEditorContribution { @@ -45,8 +46,8 @@ export class ContextMenuController implements IEditorContribution { this._editor = editor; this._toDispose.push(this._editor.onContextMenu((e: IEditorMouseEvent) => this._onContextMenu(e))); - this._toDispose.push(this._editor.onDidScrollChange((e: IScrollEvent) => { - if (this._contextMenuIsBeingShownCount > 0 && e.scrollTopChanged) { + this._toDispose.push(this._editor.onMouseWheel((e: IMouseWheelEvent) => { + if (this._contextMenuIsBeingShownCount > 0) { this._contextViewService.hideContextView(); } })); diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 76ec9c7513..f414915ce0 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -214,7 +214,7 @@ export async function formatDocumentWithSelectedProvider( const provider = getRealAndSyntheticDocumentFormattersOrdered(model); const selected = await FormattingConflicts.select(provider, model, mode); if (selected) { - await instaService.invokeFunction(formatDocumentWithProvider, selected, editorOrModel, token); + await instaService.invokeFunction(formatDocumentWithProvider, selected, editorOrModel, mode, token); } } @@ -222,6 +222,7 @@ export async function formatDocumentWithProvider( accessor: ServicesAccessor, provider: DocumentFormattingEditProvider, editorOrModel: ITextModel | IActiveCodeEditor, + mode: FormattingMode, token: CancellationToken ): Promise { const workerService = accessor.get(IEditorWorkerService); @@ -255,10 +256,13 @@ export async function formatDocumentWithProvider( if (isCodeEditor(editorOrModel)) { // use editor to apply edits FormattingEdit.execute(editorOrModel, edits); - alertFormattingEdits(edits); - editorOrModel.pushUndoStop(); - editorOrModel.focus(); - editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), editorCommon.ScrollType.Immediate); + + if (mode !== FormattingMode.Silent) { + alertFormattingEdits(edits); + editorOrModel.pushUndoStop(); + editorOrModel.focus(); + editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), editorCommon.ScrollType.Immediate); + } } else { // use model to apply edits diff --git a/src/vs/editor/contrib/referenceSearch/referencesController.ts b/src/vs/editor/contrib/referenceSearch/referencesController.ts index 88029247b3..2f714b86bb 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesController.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesController.ts @@ -98,7 +98,7 @@ export abstract class ReferencesController implements editorCommon.IEditorContri } })); const storageKey = 'peekViewLayout'; - const data = JSON.parse(this._storageService.get(storageKey, StorageScope.GLOBAL, '{}')); + const data = LayoutData.fromJSON(this._storageService.get(storageKey, StorageScope.GLOBAL, '{}')); this._widget = this._instantiationService.createInstance(ReferenceWidget, this._editor, this._defaultTreeKeyboardSupport, data); this._widget.setTitle(nls.localize('labelLoading', "Loading...")); this._widget.show(range); diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index 4220e0ece2..73cd2904ca 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -157,9 +157,25 @@ class DecorationsManager implements IDisposable { } } -export interface LayoutData { +export class LayoutData { ratio: number; heightInLines: number; + + static fromJSON(raw: string): LayoutData { + let ratio: number | undefined; + let heightInLines: number | undefined; + try { + const data = JSON.parse(raw); + ratio = data.ratio; + heightInLines = data.heightInLines; + } catch { + // + } + return { + ratio: ratio || 0.7, + heightInLines: heightInLines || 18 + }; + } } export interface SelectionEvent { diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index cb1b65084a..dfd2205135 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -9,9 +9,10 @@ import * as arrays from 'vs/base/common/arrays'; import * as types from 'vs/base/common/types'; import * as objects from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; -import { OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; +import { OVERRIDE_PROPERTY_PATTERN, ConfigurationScope, IConfigurationRegistry, Extensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IOverrides, overrideIdentifierFromKey, addToValueTree, toValuesTree, IConfigurationModel, getConfigurationValue, IConfigurationOverrides, IConfigurationData, getDefaultValues, getConfigurationKeys, IConfigurationChangeEvent, ConfigurationTarget, removeFromValueTree, toOverrides } from 'vs/platform/configuration/common/configuration'; import { Workspace } from 'vs/platform/workspace/common/workspace'; +import { Registry } from 'vs/platform/registry/common/platform'; export class ConfigurationModel implements IConfigurationModel { @@ -195,10 +196,11 @@ export class DefaultConfigurationModel extends ConfigurationModel { export class ConfigurationModelParser { + private _raw: any = null; private _configurationModel: ConfigurationModel | null = null; private _parseErrors: any[] = []; - constructor(protected readonly _name: string) { } + constructor(protected readonly _name: string, private _scopes?: ConfigurationScope[]) { } get configurationModel(): ConfigurationModel { return this._configurationModel || new ConfigurationModel(); @@ -208,15 +210,26 @@ export class ConfigurationModelParser { return this._parseErrors; } - public parse(content: string | null | undefined): void { + public parseContent(content: string | null | undefined): void { if (content) { - const raw = this.parseContent(content); - const configurationModel = this.parseRaw(raw); - this._configurationModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides); + const raw = this.doParseContent(content); + this.parseRaw(raw); } } - protected parseContent(content: string): any { + public parseRaw(raw: any): void { + this._raw = raw; + const configurationModel = this.doParseRaw(raw); + this._configurationModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides); + } + + public parse(): void { + if (this._raw) { + this.parseRaw(this._raw); + } + } + + protected doParseContent(content: string): any { let raw: any = {}; let currentProperty: string | null = null; let currentParent: any = []; @@ -273,12 +286,36 @@ export class ConfigurationModelParser { return raw; } - protected parseRaw(raw: any): IConfigurationModel { + protected doParseRaw(raw: any): IConfigurationModel { + if (this._scopes) { + const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); + raw = this.filterByScope(raw, configurationProperties, true, this._scopes); + } const contents = toValuesTree(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`)); const keys = Object.keys(raw); const overrides: IOverrides[] = toOverrides(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`)); return { contents, keys, overrides }; } + + private filterByScope(properties: {}, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }, filterOverriddenProperties: boolean, scopes: ConfigurationScope[]): {} { + const result = {}; + for (let key in properties) { + if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) { + result[key] = this.filterByScope(properties[key], configurationProperties, false, scopes); + } else { + const scope = this.getScope(key, configurationProperties); + if (scopes.indexOf(scope) !== -1) { + result[key] = properties[key]; + } + } + } + return result; + } + + private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope { + const propertySchema = configurationProperties[key]; + return propertySchema && typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW; + } } export class Configuration { diff --git a/src/vs/platform/configuration/node/configuration.ts b/src/vs/platform/configuration/node/configuration.ts index 3336528611..53d7d1da6d 100644 --- a/src/vs/platform/configuration/node/configuration.ts +++ b/src/vs/platform/configuration/node/configuration.ts @@ -27,7 +27,7 @@ export class NodeBasedUserConfiguration extends Disposable { this.userConfigModelWatcher = new ConfigWatcher(this.settingsPath, { changeBufferDelay: 300, onError: error => onUnexpectedError(error), defaultConfig: new ConfigurationModelParser(this.settingsPath), parse: (content: string, parseErrors: any[]) => { const userConfigModelParser = new ConfigurationModelParser(this.settingsPath); - userConfigModelParser.parse(content); + userConfigModelParser.parseContent(content); parseErrors = [...userConfigModelParser.errors]; return userConfigModelParser; }, initCallback: () => c(undefined) diff --git a/src/vs/platform/configuration/node/configurationService.ts b/src/vs/platform/configuration/node/configurationService.ts index cccbdefcde..eb55d0e780 100644 --- a/src/vs/platform/configuration/node/configurationService.ts +++ b/src/vs/platform/configuration/node/configurationService.ts @@ -9,7 +9,6 @@ import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService, IConfigurationChangeEvent, IConfigurationOverrides, ConfigurationTarget, compare, isConfigurationOverrides, IConfigurationData } from 'vs/platform/configuration/common/configuration'; import { DefaultConfigurationModel, Configuration, ConfigurationChangeEvent, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { Event, Emitter } from 'vs/base/common/event'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { NodeBasedUserConfiguration } from 'vs/platform/configuration/node/configuration'; @@ -24,11 +23,11 @@ export class ConfigurationService extends Disposable implements IConfigurationSe readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; constructor( - @IEnvironmentService environmentService: IEnvironmentService + configurationPath: string ) { super(); - this.userConfiguration = this._register(new NodeBasedUserConfiguration(environmentService.appSettingsPath)); + this.userConfiguration = this._register(new NodeBasedUserConfiguration(configurationPath)); // Initialize const defaults = new DefaultConfigurationModel(); diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index 2abed07642..adb8538d5c 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -250,10 +250,10 @@ suite('CustomConfigurationModel', () => { test('simple merge using models', () => { let base = new ConfigurationModelParser('base'); - base.parse(JSON.stringify({ 'a': 1, 'b': 2 })); + base.parseContent(JSON.stringify({ 'a': 1, 'b': 2 })); let add = new ConfigurationModelParser('add'); - add.parse(JSON.stringify({ 'a': 3, 'c': 4 })); + add.parseContent(JSON.stringify({ 'a': 3, 'c': 4 })); let result = base.configurationModel.merge(add.configurationModel); assert.deepEqual(result.contents, { 'a': 3, 'b': 2, 'c': 4 }); @@ -261,14 +261,14 @@ suite('CustomConfigurationModel', () => { test('simple merge with an undefined contents', () => { let base = new ConfigurationModelParser('base'); - base.parse(JSON.stringify({ 'a': 1, 'b': 2 })); + base.parseContent(JSON.stringify({ 'a': 1, 'b': 2 })); let add = new ConfigurationModelParser('add'); let result = base.configurationModel.merge(add.configurationModel); assert.deepEqual(result.contents, { 'a': 1, 'b': 2 }); base = new ConfigurationModelParser('base'); add = new ConfigurationModelParser('add'); - add.parse(JSON.stringify({ 'a': 1, 'b': 2 })); + add.parseContent(JSON.stringify({ 'a': 1, 'b': 2 })); result = base.configurationModel.merge(add.configurationModel); assert.deepEqual(result.contents, { 'a': 1, 'b': 2 }); @@ -280,25 +280,25 @@ suite('CustomConfigurationModel', () => { test('Recursive merge using config models', () => { let base = new ConfigurationModelParser('base'); - base.parse(JSON.stringify({ 'a': { 'b': 1 } })); + base.parseContent(JSON.stringify({ 'a': { 'b': 1 } })); let add = new ConfigurationModelParser('add'); - add.parse(JSON.stringify({ 'a': { 'b': 2 } })); + add.parseContent(JSON.stringify({ 'a': { 'b': 2 } })); let result = base.configurationModel.merge(add.configurationModel); assert.deepEqual(result.contents, { 'a': { 'b': 2 } }); }); test('Test contents while getting an existing property', () => { let testObject = new ConfigurationModelParser('test'); - testObject.parse(JSON.stringify({ 'a': 1 })); + testObject.parseContent(JSON.stringify({ 'a': 1 })); assert.deepEqual(testObject.configurationModel.getValue('a'), 1); - testObject.parse(JSON.stringify({ 'a': { 'b': 1 } })); + testObject.parseContent(JSON.stringify({ 'a': { 'b': 1 } })); assert.deepEqual(testObject.configurationModel.getValue('a'), { 'b': 1 }); }); test('Test contents are undefined for non existing properties', () => { const testObject = new ConfigurationModelParser('test'); - testObject.parse(JSON.stringify({ + testObject.parseContent(JSON.stringify({ awesome: true })); @@ -313,7 +313,7 @@ suite('CustomConfigurationModel', () => { test('Test configWithOverrides gives all content merged with overrides', () => { const testObject = new ConfigurationModelParser('test'); - testObject.parse(JSON.stringify({ 'a': 1, 'c': 1, '[b]': { 'a': 2 } })); + testObject.parseContent(JSON.stringify({ 'a': 1, 'c': 1, '[b]': { 'a': 2 } })); assert.deepEqual(testObject.configurationModel.override('b').contents, { 'a': 2, 'c': 1, '[b]': { 'a': 2 } }); }); @@ -326,17 +326,17 @@ suite('CustomConfigurationModel', () => { test('Test update with empty data', () => { const testObject = new ConfigurationModelParser('test'); - testObject.parse(''); + testObject.parseContent(''); assert.deepEqual(testObject.configurationModel.contents, {}); assert.deepEqual(testObject.configurationModel.keys, []); - testObject.parse(null!); + testObject.parseContent(null!); assert.deepEqual(testObject.configurationModel.contents, {}); assert.deepEqual(testObject.configurationModel.keys, []); - testObject.parse(undefined!); + testObject.parseContent(undefined!); assert.deepEqual(testObject.configurationModel.contents, {}); assert.deepEqual(testObject.configurationModel.keys, []); @@ -472,7 +472,7 @@ suite('Configuration', () => { test('Test update value', () => { const parser = new ConfigurationModelParser('test'); - parser.parse(JSON.stringify({ 'a': 1 })); + parser.parseContent(JSON.stringify({ 'a': 1 })); const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel()); testObject.updateValue('a', 2); @@ -482,7 +482,7 @@ suite('Configuration', () => { test('Test update value after inspect', () => { const parser = new ConfigurationModelParser('test'); - parser.parse(JSON.stringify({ 'a': 1 })); + parser.parseContent(JSON.stringify({ 'a': 1 })); const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel()); testObject.inspect('a', {}, undefined); diff --git a/src/vs/platform/configuration/test/node/configurationService.test.ts b/src/vs/platform/configuration/test/node/configurationService.test.ts index 85a5089024..90fad2fafc 100644 --- a/src/vs/platform/configuration/test/node/configurationService.test.ts +++ b/src/vs/platform/configuration/test/node/configurationService.test.ts @@ -10,29 +10,17 @@ import * as fs from 'fs'; import { Registry } from 'vs/platform/registry/common/platform'; import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; -import { ParsedArgs } from 'vs/platform/environment/common/environment'; -import { parseArgs } from 'vs/platform/environment/node/argv'; -import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; 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'; -class SettingsTestEnvironmentService extends EnvironmentService { - - constructor(args: ParsedArgs, _execPath: string, private customAppSettingsHome: string) { - super(args, _execPath); - } - - get appSettingsPath(): string { return this.customAppSettingsHome; } -} - suite('ConfigurationService - Node', () => { test('simple', async () => { const res = await testFile('config', 'config.json'); fs.writeFileSync(res.testFile, '{ "foo": "bar" }'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, res.testFile)); + const service = new ConfigurationService(res.testFile); const config = service.getValue<{ foo: string; }>(); @@ -49,7 +37,7 @@ suite('ConfigurationService - Node', () => { fs.writeFileSync(res.testFile, '{ "testworkbench.editor.tabs": true }'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, res.testFile)); + const service = new ConfigurationService(res.testFile); const config = service.getValue<{ testworkbench: { editor: { @@ -71,7 +59,7 @@ suite('ConfigurationService - Node', () => { fs.writeFileSync(res.testFile, ',,,,'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, res.testFile)); + const service = new ConfigurationService(res.testFile); const config = service.getValue<{ foo: string; }>(); @@ -87,7 +75,7 @@ suite('ConfigurationService - Node', () => { const newDir = path.join(parentDir, 'config', id); const testFile = path.join(newDir, 'config.json'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, testFile)); + const service = new ConfigurationService(testFile); const config = service.getValue<{ foo: string }>(); assert.ok(config); @@ -98,7 +86,7 @@ suite('ConfigurationService - Node', () => { test('trigger configuration change event', async () => { const res = await testFile('config', 'config.json'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, res.testFile)); + const service = new ConfigurationService(res.testFile); return new Promise((c, e) => { service.onDidChangeConfiguration(() => { assert.equal(service.getValue('foo'), 'bar'); @@ -115,7 +103,7 @@ suite('ConfigurationService - Node', () => { fs.writeFileSync(res.testFile, '{ "foo": "bar" }'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, res.testFile)); + const service = new ConfigurationService(res.testFile); let config = service.getValue<{ foo: string; }>(); @@ -163,7 +151,7 @@ suite('ConfigurationService - Node', () => { } }); - let serviceWithoutFile = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, '__testFile')); + let serviceWithoutFile = new ConfigurationService('__testFile'); let setting = serviceWithoutFile.getValue(); assert.ok(setting); @@ -172,7 +160,7 @@ suite('ConfigurationService - Node', () => { return testFile('config', 'config.json').then(async res => { fs.writeFileSync(res.testFile, '{ "testworkbench.editor.tabs": true }'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, res.testFile)); + const service = new ConfigurationService(res.testFile); let setting = service.getValue(); @@ -205,7 +193,7 @@ suite('ConfigurationService - Node', () => { }); const r = await testFile('config', 'config.json'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, r.testFile)); + const service = new ConfigurationService(r.testFile); let res = service.inspect('something.missing'); assert.strictEqual(res.value, undefined); assert.strictEqual(res.default, undefined); @@ -241,7 +229,7 @@ suite('ConfigurationService - Node', () => { }); const r = await testFile('config', 'config.json'); - const service = new ConfigurationService(new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, r.testFile)); + const service = new ConfigurationService(r.testFile); let res = service.inspect('lookup.service.testNullSetting'); assert.strictEqual(res.default, null); assert.strictEqual(res.value, null); diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index c3eb560e05..c89e7d5559 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -17,6 +17,7 @@ import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { EventType, $, removeNode } from 'vs/base/browser/dom'; import { attachMenuStyler } from 'vs/platform/theme/common/styler'; import { domEvent } from 'vs/base/browser/event'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; export interface IContextMenuHandlerOptions { blockMouse: boolean; @@ -83,6 +84,25 @@ export class ContextMenuHandler { menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables); menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables); domEvent(window, EventType.BLUR)(() => { this.contextViewService.hideContextView(true); }, null, menuDisposables); + domEvent(window, EventType.MOUSE_DOWN)((e: MouseEvent) => { + let event = new StandardMouseEvent(e); + let element: HTMLElement | null = event.target; + + // Don't do anything as we are likely creating a context menu + if (event.rightButton) { + return; + } + + while (element) { + if (element === container) { + return; + } + + element = element.parentElement; + } + + this.contextViewService.hideContextView(true); + }, null, menuDisposables); return combinedDisposable([...menuDisposables, menu]); }, diff --git a/src/vs/platform/diagnostics/common/diagnosticsService.ts b/src/vs/platform/diagnostics/common/diagnosticsService.ts index bf5fa38f38..d8d2ed7042 100644 --- a/src/vs/platform/diagnostics/common/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/common/diagnosticsService.ts @@ -30,6 +30,11 @@ export interface IRemoteDiagnosticInfo extends IDiagnosticInfo { hostName: string; } +export interface IRemoteDiagnosticError { + hostName: string; + errorMessage: string; +} + export interface IDiagnosticInfoOptions { includeProcesses?: boolean; folders?: UriComponents[]; @@ -46,4 +51,8 @@ export interface WorkspaceStats { configFiles: WorkspaceStatItem[]; fileCount: number; maxFilesReached: boolean; +} + +export function isRemoteDiagnosticError(x: any): x is IRemoteDiagnosticError { + return !!x.hostName && !!x.errorMessage; } \ No newline at end of file diff --git a/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts index 56c33a2288..ea522186aa 100644 --- a/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts @@ -15,7 +15,7 @@ import { app } from 'electron'; import { basename } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IMachineInfo, WorkspaceStats, SystemInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IMachineInfo, WorkspaceStats, SystemInfo, IRemoteDiagnosticInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; import { collectWorkspaceStats, getMachineInfo } from 'vs/platform/diagnostics/node/diagnosticsService'; import { ProcessItem } from 'vs/base/common/processes'; @@ -92,23 +92,28 @@ export class DiagnosticsService implements IDiagnosticsService { try { const remoteData = await launchService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true }); remoteData.forEach(diagnostics => { - processInfo += `\n\nRemote: ${diagnostics.hostName}`; - if (diagnostics.processes) { - processInfo += `\n${this.formatProcessList(info, diagnostics.processes)}`; - } + if (isRemoteDiagnosticError(diagnostics)) { + processInfo += `\n${diagnostics.errorMessage}`; + workspaceInfo += `\n${diagnostics.errorMessage}`; + } else { + processInfo += `\n\nRemote: ${diagnostics.hostName}`; + if (diagnostics.processes) { + processInfo += `\n${this.formatProcessList(info, diagnostics.processes)}`; + } - if (diagnostics.workspaceMetadata) { - workspaceInfo += `\n| Remote: ${diagnostics.hostName}`; - for (const folder of Object.keys(diagnostics.workspaceMetadata)) { - const metadata = diagnostics.workspaceMetadata[folder]; + if (diagnostics.workspaceMetadata) { + workspaceInfo += `\n| Remote: ${diagnostics.hostName}`; + for (const folder of Object.keys(diagnostics.workspaceMetadata)) { + const metadata = diagnostics.workspaceMetadata[folder]; - let countMessage = `${metadata.fileCount} files`; - if (metadata.maxFilesReached) { - countMessage = `more than ${countMessage}`; + let countMessage = `${metadata.fileCount} files`; + if (metadata.maxFilesReached) { + countMessage = `more than ${countMessage}`; + } + + workspaceInfo += `| Folder (${folder}): ${countMessage}`; + workspaceInfo += this.formatWorkspaceStats(metadata); } - - workspaceInfo += `| Folder (${folder}): ${countMessage}`; - workspaceInfo += this.formatWorkspaceStats(metadata); } } }); @@ -135,7 +140,7 @@ export class DiagnosticsService implements IDiagnosticsService { processArgs: `${info.mainArguments.join(' ')}`, gpuStatus: app.getGPUFeatureStatus(), screenReader: `${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`, - remoteData: await launchService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false }) + remoteData: (await launchService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })).filter((x): x is IRemoteDiagnosticInfo => !(x instanceof Error)) }; @@ -169,25 +174,29 @@ export class DiagnosticsService implements IDiagnosticsService { try { const data = await launchService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true }); data.forEach(diagnostics => { - output.push('\n\n'); - output.push(`Remote: ${diagnostics.hostName}`); - output.push(this.formatMachineInfo(diagnostics.machineInfo)); + if (isRemoteDiagnosticError(diagnostics)) { + output.push(`\n${diagnostics.errorMessage}`); + } else { + output.push('\n\n'); + output.push(`Remote: ${diagnostics.hostName}`); + output.push(this.formatMachineInfo(diagnostics.machineInfo)); - if (diagnostics.processes) { - output.push(this.formatProcessList(info, diagnostics.processes)); - } + if (diagnostics.processes) { + output.push(this.formatProcessList(info, diagnostics.processes)); + } - if (diagnostics.workspaceMetadata) { - for (const folder of Object.keys(diagnostics.workspaceMetadata)) { - const metadata = diagnostics.workspaceMetadata[folder]; + if (diagnostics.workspaceMetadata) { + for (const folder of Object.keys(diagnostics.workspaceMetadata)) { + const metadata = diagnostics.workspaceMetadata[folder]; - let countMessage = `${metadata.fileCount} files`; - if (metadata.maxFilesReached) { - countMessage = `more than ${countMessage}`; + let countMessage = `${metadata.fileCount} files`; + if (metadata.maxFilesReached) { + countMessage = `more than ${countMessage}`; + } + + output.push(`Folder (${folder}): ${countMessage}`); + output.push(this.formatWorkspaceStats(metadata)); } - - output.push(`Folder (${folder}): ${countMessage}`); - output.push(this.formatWorkspaceStats(metadata)); } } }); @@ -306,13 +315,13 @@ export class DiagnosticsService implements IDiagnosticsService { output.push('CPU %\tMem MB\t PID\tProcess'); if (rootProcess) { - this.formatProcessItem(mapPidToWindowTitle, output, rootProcess, 0); + this.formatProcessItem(info.mainPID, mapPidToWindowTitle, output, rootProcess, 0); } return output.join('\n'); } - private formatProcessItem(mapPidToWindowTitle: Map, output: string[], item: ProcessItem, indent: number): void { + private formatProcessItem(mainPid: number, mapPidToWindowTitle: Map, output: string[], item: ProcessItem, indent: number): void { const isRoot = (indent === 0); const MB = 1024 * 1024; @@ -320,7 +329,7 @@ export class DiagnosticsService implements IDiagnosticsService { // Format name with indent let name: string; if (isRoot) { - name = `${product.applicationName} main`; + name = item.pid === mainPid ? `${product.applicationName} main` : 'remote agent'; } else { name = `${repeat(' ', indent)} ${item.name}`; @@ -333,7 +342,7 @@ export class DiagnosticsService implements IDiagnosticsService { // Recurse into children if any if (Array.isArray(item.children)) { - item.children.forEach(child => this.formatProcessItem(mapPidToWindowTitle, output, child, indent + 1)); + item.children.forEach(child => this.formatProcessItem(mainPid, mapPidToWindowTitle, output, child, indent + 1)); } } } diff --git a/src/vs/platform/driver/electron-browser/driver.ts b/src/vs/platform/driver/electron-browser/driver.ts index ae2c035408..476d02f4d4 100644 --- a/src/vs/platform/driver/electron-browser/driver.ts +++ b/src/vs/platform/driver/electron-browser/driver.ts @@ -226,12 +226,12 @@ export async function registerWindowDriver(accessor: ServicesAccessor): Promise< const windowDriverRegistryChannel = mainProcessService.getChannel('windowDriverRegistry'); const windowDriverRegistry = new WindowDriverRegistryChannelClient(windowDriverRegistryChannel); - await windowDriverRegistry.registerWindowDriver(windowService.getCurrentWindowId()); + await windowDriverRegistry.registerWindowDriver(windowService.windowId); // const options = await windowDriverRegistry.registerWindowDriver(windowId); // if (options.verbose) { // windowDriver.openDevTools(); // } - return toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowService.getCurrentWindowId())); + return toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowService.windowId)); } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 8cbda0e31f..17d468fb39 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -50,6 +50,7 @@ export interface ParsedArgs { 'show-versions'?: boolean; 'install-extension'?: string | string[]; 'uninstall-extension'?: string | string[]; + 'locate-extension'?: string | string[]; 'enable-proposed-api'?: string | string[]; 'open-url'?: boolean; 'skip-getting-started'?: boolean; @@ -109,6 +110,9 @@ export interface IEnvironmentService { appSettingsPath: string; appKeybindingsPath: string; + machineSettingsHome: string; + machineSettingsPath: string; + settingsSearchBuildId?: number; settingsSearchUrl?: string; @@ -124,7 +128,7 @@ export interface IEnvironmentService { disableExtensions: boolean | string[]; builtinExtensionsPath: string; extensionsPath: string; - extensionDevelopmentLocationURI?: URI | URI[]; + extensionDevelopmentLocationURI?: URI[]; extensionTestsLocationURI?: URI; debugExtensionHost: IExtensionHostDebugParams; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index a067596688..0d7230f46c 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -66,6 +66,7 @@ export const options: Option[] = [ { id: 'max-memory', type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes).") }, { id: 'remote', type: 'string' }, + { id: 'locate-extension', type: 'string' }, { id: 'extensionDevelopmentPath', type: 'string' }, { id: 'extensionTestsPath', type: 'string' }, { id: 'debugId', type: 'string' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index aeb54a484a..45ccb6fab5 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -110,6 +110,12 @@ export class EnvironmentService implements IEnvironmentService { @memoize get appSettingsPath(): string { return path.join(this.appSettingsHome, 'settings.json'); } + @memoize + get machineSettingsHome(): string { return path.join(this.userDataPath, 'Machine'); } + + @memoize + get machineSettingsPath(): string { return path.join(this.machineSettingsHome, 'settings.json'); } + @memoize get globalStorageHome(): string { return path.join(this.appSettingsHome, 'globalStorage'); } @@ -172,7 +178,7 @@ export class EnvironmentService implements IEnvironmentService { } @memoize - get extensionDevelopmentLocationURI(): URI | URI[] | undefined { + get extensionDevelopmentLocationURI(): URI[] | undefined { const s = this._args.extensionDevelopmentPath; if (Array.isArray(s)) { return s.map(p => { @@ -183,9 +189,9 @@ export class EnvironmentService implements IEnvironmentService { }); } else if (s) { if (/^[^:/?#]+?:\/\//.test(s)) { - return URI.parse(s); + return [URI.parse(s)]; } - return URI.file(path.normalize(s)); + return [URI.file(path.normalize(s))]; } return undefined; } diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index 518cdb2959..0d75128e36 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -753,7 +753,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } loadAllDependencies(extensions: IExtensionIdentifier[], token: CancellationToken): Promise { - return this.getDependenciesReccursively(extensions.map(e => e.id), [], token); + return this.getDependenciesRecursively(extensions.map(e => e.id), [], token); } getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise { @@ -812,7 +812,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { }); } - private getDependenciesReccursively(toGet: string[], result: IGalleryExtension[], token: CancellationToken): Promise { + private getDependenciesRecursively(toGet: string[], result: IGalleryExtension[], token: CancellationToken): Promise { if (!toGet || !toGet.length) { return Promise.resolve(result); } @@ -832,7 +832,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { result = distinct(result.concat(loadedDependencies), d => d.identifier.uuid); const dependencies: string[] = []; dependenciesSet.forEach(d => !ExtensionGalleryService.hasExtensionByName(result, d) && dependencies.push(d)); - return this.getDependenciesReccursively(dependencies, result, token); + return this.getDependenciesRecursively(dependencies, result, token); }); } @@ -903,7 +903,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (version) { return version; } - return this.getLastValidExtensionVersionReccursively(extension, versions); + return this.getLastValidExtensionVersionRecursively(extension, versions); } private getLastValidExtensionVersionFromProperties(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise | null { @@ -936,7 +936,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { .then(manifest => manifest ? manifest.engines.vscode : Promise.reject('Error while reading manifest')); } - private getLastValidExtensionVersionReccursively(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise { + private getLastValidExtensionVersionRecursively(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise { if (!versions.length) { return Promise.resolve(null); } @@ -945,7 +945,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return this.getEngine(version) .then(engine => { if (!isEngineValid(engine)) { - return this.getLastValidExtensionVersionReccursively(extension, versions.slice(1)); + return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1)); } version.properties = version.properties || []; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 5ee365a046..3c8f4923e8 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -13,6 +13,7 @@ import { startsWithIgnoreCase } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; import { isEqualOrParent, isEqual } from 'vs/base/common/resources'; import { isUndefinedOrNull } from 'vs/base/common/types'; +import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; export const IFileService = createDecorator('fileService'); @@ -123,10 +124,15 @@ export interface IFileService { */ resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise; + /** + * @deprecated use writeFile instead + */ + updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise; + /** * Updates the content replacing its previous value. */ - updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise; + writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise; /** * Moves the file/folder to a new path identified by the resource. @@ -143,12 +149,17 @@ export interface IFileService { copy(source: URI, target: URI, overwrite?: boolean): Promise; /** - * Creates a new file with the given path. The returned promise + * @deprecated use createFile2 instead + */ + createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; + + /** + * Creates a new file with the given path and optional contents. The returned promise * will have the stat model object as a result. * * The optional parameter content can be used as value to fill into the new file. */ - createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; + createFile2(resource: URI, bufferOrReadable?: VSBuffer | VSBufferReadable, options?: ICreateFileOptions): Promise; /** * Creates a new folder with the given path. The returned promise @@ -650,17 +661,6 @@ export interface ITextSnapshot { read(): string | null; } -export class StringSnapshot implements ITextSnapshot { - private _value: string | null; - constructor(value: string) { - this._value = value; - } - read(): string | null { - let ret = this._value; - this._value = null; - return ret; - } -} /** * Helper method to convert a snapshot into its full string form. */ @@ -674,6 +674,35 @@ export function snapshotToString(snapshot: ITextSnapshot): string { return chunks.join(''); } +export class TextSnapshotReadable implements VSBufferReadable { + private preambleHandled: boolean; + + constructor(private snapshot: ITextSnapshot, private preamble?: string) { } + + read(): VSBuffer | null { + let value = this.snapshot.read(); + + // Handle preamble if provided + if (!this.preambleHandled) { + this.preambleHandled = true; + + if (typeof this.preamble === 'string') { + if (typeof value === 'string') { + value = this.preamble + value; + } else { + value = this.preamble; + } + } + } + + if (typeof value === 'string') { + return VSBuffer.fromString(value); + } + + return null; + } +} + /** * Streamable content and meta information of a file. */ @@ -724,7 +753,20 @@ export interface IResolveContentOptions { position?: number; } -export interface IUpdateContentOptions { +export interface IWriteFileOptions { + + /** + * The last known modification time of the file. This can be used to prevent dirty writes. + */ + mtime?: number; + + /** + * The etag of the file. This can be used to prevent dirty writes. + */ + etag?: string; +} + +export interface IWriteTextFileOptions extends IWriteFileOptions { /** * The encoding to use when updating a file. @@ -746,21 +788,6 @@ export interface IUpdateContentOptions { * ask the user to authenticate as super user. */ writeElevated?: boolean; - - /** - * The last known modification time of the file. This can be used to prevent dirty writes. - */ - mtime?: number; - - /** - * The etag of the file. This can be used to prevent dirty writes. - */ - etag?: string; - - /** - * Run mkdirp before saving. - */ - mkdirp?: boolean; } export interface IResolveFileOptions { @@ -797,7 +824,7 @@ export interface ICreateFileOptions { } export class FileOperationError extends Error { - constructor(message: string, public fileOperationResult: FileOperationResult, public options?: IResolveContentOptions & IUpdateContentOptions & ICreateFileOptions) { + constructor(message: string, public fileOperationResult: FileOperationResult, public options?: IResolveContentOptions & IWriteTextFileOptions & ICreateFileOptions) { super(message); } @@ -1132,7 +1159,7 @@ export interface ILegacyFileService extends IDisposable { resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise; - updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise; + updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise; createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; } \ No newline at end of file diff --git a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts index c1849e7091..dc3e99552e 100644 --- a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts +++ b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts @@ -33,7 +33,7 @@ export class SharedProcessService implements ISharedProcessService { @IEnvironmentService environmentService: IEnvironmentService ) { this.withSharedProcessConnection = windowsService.whenSharedProcessReady() - .then(() => connect(environmentService.sharedIPCHandle, `window:${windowService.getCurrentWindowId()}`)); + .then(() => connect(environmentService.sharedIPCHandle, `window:${windowService.windowId}`)); } getChannel(channelName: string): IChannel { diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index f22d2af05d..f867b27de4 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -15,6 +15,8 @@ import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { IWindowState } from 'vs/platform/windows/electron-main/windows'; +import { listProcesses } from 'vs/base/node/ps'; +import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; const DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; @@ -44,6 +46,35 @@ export class IssueService implements IIssueService { }); }); + ipcMain.on('vscode:listProcesses', async (event: Event) => { + const processes = []; + + try { + const mainPid = await this.launchService.getMainProcessId(); + processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(mainPid) }); + (await this.launchService.getRemoteDiagnostics({ includeProcesses: true })) + .forEach(data => { + if (isRemoteDiagnosticError(data)) { + processes.push({ + name: data.hostName, + rootProcess: data + }); + } else { + if (data.processes) { + processes.push({ + name: data.hostName, + rootProcess: data.processes + }); + } + } + }); + } catch (e) { + this.logService.error(`Listing processes failed: ${e}`); + } + + event.sender.send('vscode:listProcessesResponse', processes); + }); + ipcMain.on('vscode:issuePerformanceInfoRequest', (event: Event) => { this.getPerformanceInfo().then(msg => { event.sender.send('vscode:issuePerformanceInfoResponse', msg); diff --git a/src/vs/platform/launch/electron-main/launchService.ts b/src/vs/platform/launch/electron-main/launchService.ts index 5d6eb8c52c..e7d4c1b71d 100644 --- a/src/vs/platform/launch/electron-main/launchService.ts +++ b/src/vs/platform/launch/electron-main/launchService.ts @@ -19,7 +19,7 @@ import { BrowserWindow, ipcMain, Event as IpcEvent } from 'electron'; import { Event } from 'vs/base/common/event'; import { hasArgs } from 'vs/platform/environment/node/argv'; import { coalesce } from 'vs/base/common/arrays'; -import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; export const ID = 'launchService'; export const ILaunchService = createDecorator(ID); @@ -71,7 +71,7 @@ export interface ILaunchService { getMainProcessId(): Promise; getMainProcessInfo(): Promise; getLogsPath(): Promise; - getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise; + getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]>; } export class LaunchChannel implements IServerChannel { @@ -299,9 +299,9 @@ export class LaunchService implements ILaunchService { return Promise.resolve(this.environmentService.logsPath); } - getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise { + getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); - const promises: Promise[] = windows.map(window => { + const promises: Promise[] = windows.map(window => { return new Promise((resolve, reject) => { if (window.remoteAuthority) { const replyChannel = `vscode:getDiagnosticInfoResponse${window.id}`; @@ -315,18 +315,14 @@ export class LaunchService implements ILaunchService { ipcMain.once(replyChannel, (_: IpcEvent, data: IRemoteDiagnosticInfo) => { // No data is returned if getting the connection fails. if (!data) { - resolve(); - } - - if (typeof (data) === 'string') { - reject(new Error(data)); + resolve({ hostName: window.remoteAuthority!, errorMessage: `Unable to resolve connection to '${window.remoteAuthority}'.` }); } resolve(data); }); setTimeout(() => { - resolve(); + resolve({ hostName: window.remoteAuthority!, errorMessage: `Fetching remote diagnostics for '${window.remoteAuthority}' timed out.` }); }, 5000); } else { resolve(); @@ -334,7 +330,7 @@ export class LaunchService implements ILaunchService { }); }); - return Promise.all(promises).then(diagnostics => diagnostics.filter((x): x is IRemoteDiagnosticInfo => !!x)); + return Promise.all(promises).then(diagnostics => diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x)); } private getFolderURIs(window: ICodeWindow): URI[] { diff --git a/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts b/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts index a317d25fde..6c6d033adb 100644 --- a/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts +++ b/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts @@ -53,7 +53,7 @@ export class LifecycleService extends AbstractLifecycleService { } private registerListeners(): void { - const windowId = this.windowService.getCurrentWindowId(); + const windowId = this.windowService.windowId; // Main side indicates that window is about to unload, check for vetos ipc.on('vscode:onBeforeUnload', (_event: unknown, reply: { okChannel: string, cancelChannel: string, reason: ShutdownReason }) => { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index f4afb80840..01b56cdf92 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -29,7 +29,7 @@ import { ObjectTree, IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTr import { ITreeEvent, ITreeRenderer, IAsyncDataSource, IDataSource, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; import { AsyncDataTree, IAsyncDataTreeOptions } from 'vs/base/browser/ui/tree/asyncDataTree'; import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree'; -import { IKeyboardNavigationEventFilter } from 'vs/base/browser/ui/tree/abstractTree'; +import { IKeyboardNavigationEventFilter, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; export type ListWidget = List | PagedList | ITree | ObjectTree | DataTree | AsyncDataTree; @@ -659,8 +659,9 @@ export interface IOpenEvent { browserEvent?: UIEvent; } -export interface IResourceResultsNavigationOptions { - openOnFocus: boolean; +export interface IResourceResultsNavigationOptions2 { + openOnFocus?: boolean; + openOnSelection?: boolean; } export interface SelectionKeyboardEvent extends KeyboardEvent { @@ -676,14 +677,24 @@ export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: b export class TreeResourceNavigator2 extends Disposable { + private options: IResourceResultsNavigationOptions2; + private readonly _onDidOpenResource = new Emitter>(); readonly onDidOpenResource: Event> = this._onDidOpenResource.event; constructor( private tree: WorkbenchObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree, - private options?: IResourceResultsNavigationOptions + options?: IResourceResultsNavigationOptions2 ) { super(); + + this.options = { + ...{ + openOnSelection: true + }, + ...(options || {}) + }; + this.registerListeners(); } @@ -692,7 +703,10 @@ export class TreeResourceNavigator2 extends Disposable { this._register(this.tree.onDidChangeFocus(e => this.onFocus(e))); } - this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); + if (this.options && this.options.openOnSelection) { + this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); + } + this._register(this.tree.onDidOpen(e => this.onSelection(e))); } @@ -766,15 +780,9 @@ function createKeyboardNavigationEventFilter(container: HTMLElement, keybindingS export class WorkbenchObjectTree, TFilterData = void> extends ObjectTree { - readonly contextKeyService: IContextKeyService; - - protected disposables: IDisposable[]; - - private hasSelectionOrFocus: IContextKey; - private hasDoubleSelection: IContextKey; - private hasMultiSelection: IContextKey; - - private _useAltAsMultipleSelectionModifier: boolean; + private internals: WorkbenchTreeInternals; + get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } + get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } constructor( container: HTMLElement, @@ -788,132 +796,19 @@ export class WorkbenchObjectTree, TFilterData = void> @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - WorkbenchListSupportsKeyboardNavigation.bindTo(contextKeyService); - - if (!didBindWorkbenchListAutomaticKeyboardNavigation) { - WorkbenchListAutomaticKeyboardNavigation.bindTo(contextKeyService); - didBindWorkbenchListAutomaticKeyboardNavigation = true; - } - - const getAutomaticKeyboardNavigation = () => { - // give priority to the context key value to disable this completely - let automaticKeyboardNavigation = contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); - - if (automaticKeyboardNavigation) { - automaticKeyboardNavigation = configurationService.getValue(automaticKeyboardNavigationSettingKey); - } - - return automaticKeyboardNavigation; - }; - - const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled; - const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : getHorizontalScrollingSetting(configurationService); - const openOnSingleClick = useSingleClickToOpen(configurationService); - const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); - - super(container, delegate, renderers, { - keyboardSupport: false, - styleController: new DefaultStyleController(getSharedListStyleSheet()), - ...computeStyles(themeService.getTheme(), defaultListStyles), - ...workbenchListOptions, - indent: configurationService.getValue(treeIndentKey), - automaticKeyboardNavigation: getAutomaticKeyboardNavigation(), - simpleKeyboardNavigation: keyboardNavigation === 'simple', - filterOnType: keyboardNavigation === 'filter', - horizontalScrolling, - openOnSingleClick, - keyboardNavigationEventFilter: createKeyboardNavigationEventFilter(container, keybindingService) - }); - - this.disposables.push(workbenchListOptionsDisposable); - - this.contextKeyService = createScopedContextKeyService(contextKeyService, this); - - const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); - listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); - - this.hasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); - this.hasDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); - this.hasMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); - - this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); - - const interestingContextKeys = new Set(); - interestingContextKeys.add(WorkbenchListAutomaticKeyboardNavigationKey); - const updateKeyboardNavigation = () => { - const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled; - const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); - this.updateOptions({ - simpleKeyboardNavigation: keyboardNavigation === 'simple', - filterOnType: keyboardNavigation === 'filter' - }); - }; - - this.disposables.push( - this.contextKeyService, - (listService as ListService).register(this), - attachListStyler(this, themeService), - this.onDidChangeSelection(() => { - const selection = this.getSelection(); - const focus = this.getFocus(); - - this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); - this.hasMultiSelection.set(selection.length > 1); - this.hasDoubleSelection.set(selection.length === 2); - }), - this.onDidChangeFocus(() => { - const selection = this.getSelection(); - const focus = this.getFocus(); - - this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); - }), - configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(openModeSettingKey)) { - this.updateOptions({ openOnSingleClick: useSingleClickToOpen(configurationService) }); - } - if (e.affectsConfiguration(multiSelectModifierSettingKey)) { - this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); - } - if (e.affectsConfiguration(treeIndentKey)) { - const indent = configurationService.getValue(treeIndentKey); - this.updateOptions({ indent }); - } - if (e.affectsConfiguration(keyboardNavigationSettingKey)) { - updateKeyboardNavigation(); - } - if (e.affectsConfiguration(automaticKeyboardNavigationSettingKey)) { - this.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); - } - }), - this.contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(interestingContextKeys)) { - this.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); - } - }), - accessibilityService.onDidChangeAccessibilitySupport(() => updateKeyboardNavigation()) - ); - } - - get useAltAsMultipleSelectionModifier(): boolean { - return this._useAltAsMultipleSelectionModifier; - } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + super(container, delegate, renderers, treeOptions); + this.disposables.push(disposable); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.disposables.push(this.internals); } } export class WorkbenchDataTree extends DataTree { - readonly contextKeyService: IContextKeyService; - - private hasSelectionOrFocus: IContextKey; - private hasDoubleSelection: IContextKey; - private hasMultiSelection: IContextKey; - - private _useAltAsMultipleSelectionModifier: boolean; + private internals: WorkbenchTreeInternals; + get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } + get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } constructor( container: HTMLElement, @@ -928,127 +823,19 @@ export class WorkbenchDataTree extends DataTree { - // give priority to the context key value to disable this completely - let automaticKeyboardNavigation = contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); - - if (automaticKeyboardNavigation) { - automaticKeyboardNavigation = configurationService.getValue(automaticKeyboardNavigationSettingKey); - } - - return automaticKeyboardNavigation; - }; - - const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled; - const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : getHorizontalScrollingSetting(configurationService); - const openOnSingleClick = useSingleClickToOpen(configurationService); - const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); - - super(container, delegate, renderers, dataSource, { - keyboardSupport: false, - styleController: new DefaultStyleController(getSharedListStyleSheet()), - ...computeStyles(themeService.getTheme(), defaultListStyles), - ...workbenchListOptions, - indent: configurationService.getValue(treeIndentKey), - automaticKeyboardNavigation: getAutomaticKeyboardNavigation(), - simpleKeyboardNavigation: keyboardNavigation === 'simple', - filterOnType: keyboardNavigation === 'filter', - horizontalScrolling, - openOnSingleClick, - keyboardNavigationEventFilter: createKeyboardNavigationEventFilter(container, keybindingService) - }); - - this.disposables.push(workbenchListOptionsDisposable); - - this.contextKeyService = createScopedContextKeyService(contextKeyService, this); - - const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); - listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); - - this.hasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); - this.hasDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); - this.hasMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); - - this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); - - const interestingContextKeys = new Set(); - interestingContextKeys.add(WorkbenchListAutomaticKeyboardNavigationKey); - const updateKeyboardNavigation = () => { - const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled; - const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); - this.updateOptions({ - simpleKeyboardNavigation: keyboardNavigation === 'simple', - filterOnType: keyboardNavigation === 'filter' - }); - }; - - this.disposables.push( - this.contextKeyService, - (listService as ListService).register(this), - attachListStyler(this, themeService), - this.onDidChangeSelection(() => { - const selection = this.getSelection(); - const focus = this.getFocus(); - - this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); - this.hasMultiSelection.set(selection.length > 1); - this.hasDoubleSelection.set(selection.length === 2); - }), - this.onDidChangeFocus(() => { - const selection = this.getSelection(); - const focus = this.getFocus(); - - this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); - }), - configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(openModeSettingKey)) { - this.updateOptions({ openOnSingleClick: useSingleClickToOpen(configurationService) }); - } - if (e.affectsConfiguration(multiSelectModifierSettingKey)) { - this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); - } - if (e.affectsConfiguration(treeIndentKey)) { - const indent = configurationService.getValue(treeIndentKey); - this.updateOptions({ indent }); - } - if (e.affectsConfiguration(keyboardNavigationSettingKey)) { - updateKeyboardNavigation(); - } - if (e.affectsConfiguration(automaticKeyboardNavigationSettingKey)) { - this.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); - } - }), - this.contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(interestingContextKeys)) { - this.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); - } - }), - accessibilityService.onDidChangeAccessibilitySupport(() => updateKeyboardNavigation()) - ); - } - - get useAltAsMultipleSelectionModifier(): boolean { - return this._useAltAsMultipleSelectionModifier; + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + super(container, delegate, renderers, dataSource, treeOptions); + this.disposables.push(disposable); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.disposables.push(this.internals); } } export class WorkbenchAsyncDataTree extends AsyncDataTree { - readonly contextKeyService: IContextKeyService; - - private hasSelectionOrFocus: IContextKey; - private hasDoubleSelection: IContextKey; - private hasMultiSelection: IContextKey; - - private _useAltAsMultipleSelectionModifier: boolean; + private internals: WorkbenchTreeInternals; + get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } + get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } constructor( container: HTMLElement, @@ -1063,31 +850,51 @@ export class WorkbenchAsyncDataTree extends Async @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - WorkbenchListSupportsKeyboardNavigation.bindTo(contextKeyService); + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + super(container, delegate, renderers, dataSource, treeOptions); + this.disposables.push(disposable); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.disposables.push(this.internals); + } +} - if (!didBindWorkbenchListAutomaticKeyboardNavigation) { - WorkbenchListAutomaticKeyboardNavigation.bindTo(contextKeyService); - didBindWorkbenchListAutomaticKeyboardNavigation = true; +function workbenchTreeDataPreamble( + container: HTMLElement, + options: IAbstractTreeOptions, + contextKeyService: IContextKeyService, + themeService: IThemeService, + configurationService: IConfigurationService, + keybindingService: IKeybindingService, + accessibilityService: IAccessibilityService, +): { options: IAbstractTreeOptions, getAutomaticKeyboardNavigation: () => boolean | undefined, disposable: IDisposable } { + WorkbenchListSupportsKeyboardNavigation.bindTo(contextKeyService); + + if (!didBindWorkbenchListAutomaticKeyboardNavigation) { + WorkbenchListAutomaticKeyboardNavigation.bindTo(contextKeyService); + didBindWorkbenchListAutomaticKeyboardNavigation = true; + } + + const getAutomaticKeyboardNavigation = () => { + // give priority to the context key value to disable this completely + let automaticKeyboardNavigation = contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); + + if (automaticKeyboardNavigation) { + automaticKeyboardNavigation = configurationService.getValue(automaticKeyboardNavigationSettingKey); } - const getAutomaticKeyboardNavigation = () => { - // give priority to the context key value to disable this completely - let automaticKeyboardNavigation = contextKeyService.getContextKeyValue(WorkbenchListAutomaticKeyboardNavigationKey); + return automaticKeyboardNavigation; + }; - if (automaticKeyboardNavigation) { - automaticKeyboardNavigation = configurationService.getValue(automaticKeyboardNavigationSettingKey); - } + const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled; + const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); + const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : getHorizontalScrollingSetting(configurationService); + const openOnSingleClick = useSingleClickToOpen(configurationService); + const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService); - return automaticKeyboardNavigation; - }; - - const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled; - const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); - const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : getHorizontalScrollingSetting(configurationService); - const openOnSingleClick = useSingleClickToOpen(configurationService); - const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); - - super(container, delegate, renderers, dataSource, { + return { + getAutomaticKeyboardNavigation, + disposable, + options: { keyboardSupport: false, styleController: new DefaultStyleController(getSharedListStyleSheet()), ...computeStyles(themeService.getTheme(), defaultListStyles), @@ -1099,11 +906,30 @@ export class WorkbenchAsyncDataTree extends Async horizontalScrolling, openOnSingleClick, keyboardNavigationEventFilter: createKeyboardNavigationEventFilter(container, keybindingService) - }); + } + }; +} - this.disposables.push(workbenchListOptionsDisposable); +class WorkbenchTreeInternals { - this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + readonly contextKeyService: IContextKeyService; + private hasSelectionOrFocus: IContextKey; + private hasDoubleSelection: IContextKey; + private hasMultiSelection: IContextKey; + private _useAltAsMultipleSelectionModifier: boolean; + private disposables: IDisposable[] = []; + + constructor( + tree: WorkbenchObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree, + options: IAbstractTreeOptions, + getAutomaticKeyboardNavigation: () => boolean | undefined, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IAccessibilityService accessibilityService: IAccessibilityService, + ) { + this.contextKeyService = createScopedContextKeyService(contextKeyService, tree); const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); @@ -1119,7 +945,7 @@ export class WorkbenchAsyncDataTree extends Async const updateKeyboardNavigation = () => { const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled; const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); - this.updateOptions({ + tree.updateOptions({ simpleKeyboardNavigation: keyboardNavigation === 'simple', filterOnType: keyboardNavigation === 'filter' }); @@ -1127,43 +953,43 @@ export class WorkbenchAsyncDataTree extends Async this.disposables.push( this.contextKeyService, - (listService as ListService).register(this), - attachListStyler(this, themeService), - this.onDidChangeSelection(() => { - const selection = this.getSelection(); - const focus = this.getFocus(); + (listService as ListService).register(tree), + attachListStyler(tree, themeService), + tree.onDidChangeSelection(() => { + const selection = tree.getSelection(); + const focus = tree.getFocus(); this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); this.hasMultiSelection.set(selection.length > 1); this.hasDoubleSelection.set(selection.length === 2); }), - this.onDidChangeFocus(() => { - const selection = this.getSelection(); - const focus = this.getFocus(); + tree.onDidChangeFocus(() => { + const selection = tree.getSelection(); + const focus = tree.getFocus(); this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); }), configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(openModeSettingKey)) { - this.updateOptions({ openOnSingleClick: useSingleClickToOpen(configurationService) }); + tree.updateOptions({ openOnSingleClick: useSingleClickToOpen(configurationService) }); } if (e.affectsConfiguration(multiSelectModifierSettingKey)) { this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); } if (e.affectsConfiguration(treeIndentKey)) { const indent = configurationService.getValue(treeIndentKey); - this.updateOptions({ indent }); + tree.updateOptions({ indent }); } if (e.affectsConfiguration(keyboardNavigationSettingKey)) { updateKeyboardNavigation(); } if (e.affectsConfiguration(automaticKeyboardNavigationSettingKey)) { - this.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); + tree.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); } }), this.contextKeyService.onDidChangeContext(e => { if (e.affectsSome(interestingContextKeys)) { - this.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); + tree.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); } }), accessibilityService.onDidChangeAccessibilitySupport(() => updateKeyboardNavigation()) @@ -1173,6 +999,10 @@ export class WorkbenchAsyncDataTree extends Async get useAltAsMultipleSelectionModifier(): boolean { return this._useAltAsMultipleSelectionModifier; } + + dispose(): void { + this.disposables = dispose(this.disposables); + } } const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index 814a11e67d..f960f9b804 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -11,7 +11,7 @@ export interface IRemoteAgentEnvironment { pid: number; appRoot: URI; appSettingsHome: URI; - appSettingsPath: URI; + settingsPath: URI; logsPath: URI; extensionsPath: URI; extensionHostLogsPath: URI; diff --git a/src/vs/platform/remote/common/remoteHosts.ts b/src/vs/platform/remote/common/remoteHosts.ts index 83960bf381..4c5b419780 100644 --- a/src/vs/platform/remote/common/remoteHosts.ts +++ b/src/vs/platform/remote/common/remoteHosts.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; -export const REMOTE_HOST_SCHEME = 'vscode-remote'; +export const REMOTE_HOST_SCHEME = Schemas.vscodeRemote; export function getRemoteAuthority(uri: URI): string | undefined { return uri.scheme === REMOTE_HOST_SCHEME ? uri.authority : undefined; diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts new file mode 100644 index 0000000000..d92f3e45d9 --- /dev/null +++ b/src/vs/platform/remote/common/tunnel.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ITunnelService = createDecorator('tunnelService'); + +export interface RemoteTunnel { + readonly tunnelRemotePort: number; + readonly tunnelLocalPort: number; + + dispose(): void; +} + +export interface ITunnelService { + _serviceBrand: any; + + openTunnel(remotePort: number): Promise | undefined; +} diff --git a/src/vs/platform/remote/node/nodeWebSocketFactory.ts b/src/vs/platform/remote/node/nodeWebSocketFactory.ts index dd69ea1a5e..9139f3e999 100644 --- a/src/vs/platform/remote/node/nodeWebSocketFactory.ts +++ b/src/vs/platform/remote/node/nodeWebSocketFactory.ts @@ -3,10 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as net from 'net'; +import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { IWebSocketFactory, IConnectCallback } from 'vs/platform/remote/common/remoteAgentConnection'; export const nodeWebSocketFactory = new class implements IWebSocketFactory { connect(host: string, port: number, query: string, callback: IConnectCallback): void { - throw new Error(`Not implemented`); + const errorListener = (err: any) => callback(err, undefined); + + const socket = net.createConnection({ host: host, port: port }, () => { + socket.removeListener('error', errorListener); + + // https://tools.ietf.org/html/rfc6455#section-4 + const buffer = Buffer.alloc(16); + for (let i = 0; i < 16; i++) { + buffer[i] = Math.round(Math.random() * 256); + } + const nonce = buffer.toString('base64'); + + let headers = [ + `GET ws://${host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`, + `Connection: Upgrade`, + `Upgrade: websocket`, + `Sec-WebSocket-Key: ${nonce}` + ]; + socket.write(headers.join('\r\n') + '\r\n\r\n'); + + const onData = (data: Buffer) => { + const strData = data.toString(); + if (strData.indexOf('\r\n\r\n') >= 0) { + // headers received OK + socket.off('data', onData); + callback(undefined, new NodeSocket(socket)); + } + }; + socket.on('data', onData); + }); + socket.once('error', errorListener); } }; diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts new file mode 100644 index 0000000000..2b1e8a1a04 --- /dev/null +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class TunnelService implements ITunnelService { + _serviceBrand: any; + + public constructor( + ) { + } + + openTunnel(remotePort: number): Promise | undefined { + return undefined; + } +} + +registerSingleton(ITunnelService, TunnelService); diff --git a/src/vs/platform/statusbar/common/statusbar.ts b/src/vs/platform/statusbar/common/statusbar.ts index f860fee04c..cd7f01e10a 100644 --- a/src/vs/platform/statusbar/common/statusbar.ts +++ b/src/vs/platform/statusbar/common/statusbar.ts @@ -67,13 +67,21 @@ export interface IStatusbarService { _serviceBrand: any; /** - * Adds an entry to the statusbar with the given alignment and priority. Use the returned IDisposable - * to remove the statusbar entry. + * Adds an entry to the statusbar with the given alignment and priority. Use the returned accessor + * to update or remove the statusbar entry. */ - addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority?: number): IDisposable; + addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority?: number): IStatusbarEntryAccessor; /** * Prints something to the status bar area with optional auto dispose and delay. */ setStatusMessage(message: string, autoDisposeAfter?: number, delayBy?: number): IDisposable; +} + +export interface IStatusbarEntryAccessor extends IDisposable { + + /** + * Allows to update an existing status bar entry. + */ + update(properties: IStatusbarEntry): void; } \ No newline at end of file diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 47948337ed..328530a4e0 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -226,8 +226,8 @@ export interface IWindowService { readonly hasFocus: boolean; - getConfiguration(): IWindowConfiguration; - getCurrentWindowId(): number; + readonly windowId: number; + pickFileFolderAndOpen(options: INativeOpenDialogOptions): Promise; pickFileAndOpen(options: INativeOpenDialogOptions): Promise; pickFolderAndOpen(options: INativeOpenDialogOptions): Promise; @@ -459,7 +459,7 @@ export class ActiveWindowManager implements IDisposable { this.firstActiveWindowIdPromise = createCancelablePromise(_ => windowsService.getActiveWindowId()); this.firstActiveWindowIdPromise - .then(id => this.activeWindowId = id) + .then(id => this.activeWindowId = typeof this.activeWindowId === 'number' ? this.activeWindowId : id) .finally(this.firstActiveWindowIdPromise = undefined); } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 312b905339..57975a05be 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -812,28 +812,28 @@ declare module 'vscode' { /** * The id of the comment */ - commentId: string; + readonly commentId: string; /** * The text of the comment */ - body: MarkdownString; + readonly body: MarkdownString; /** * Optional label describing the [Comment](#Comment) * Label will be rendered next to userName if exists. */ - label?: string; + readonly label?: string; /** * The display name of the user who created the comment */ - userName: string; + readonly userName: string; /** * The icon path for the user who created the comment */ - userIconPath?: Uri; + readonly userIconPath?: Uri; /** * @deprecated Use userIconPath instead. The avatar src of the user who created the comment @@ -869,17 +869,17 @@ declare module 'vscode' { /** * The command to be executed if the comment is selected in the Comments Panel */ - selectCommand?: Command; + readonly selectCommand?: Command; /** * The command to be executed when users try to save the edits to the comment */ - editCommand?: Command; + readonly editCommand?: Command; /** * The command to be executed when users try to delete the comment */ - deleteCommand?: Command; + readonly deleteCommand?: Command; /** * Deprecated diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index a95f8c32f4..eed35c1a81 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -17,6 +17,8 @@ import { ExtHostContext, ExtHostDocumentsShape, IExtHostContext, MainThreadDocum import { ITextEditorModel } from 'vs/workbench/common/editor'; import { ITextFileService, TextFileModelChangeEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { toLocalResource } from 'vs/base/common/resources'; export class BoundModelReferenceCollection { @@ -69,6 +71,7 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { private readonly _textFileService: ITextFileService; private readonly _fileService: IFileService; private readonly _untitledEditorService: IUntitledEditorService; + private readonly _environmentService: IWorkbenchEnvironmentService; private _toDispose: IDisposable[]; private _modelToDisposeMap: { [modelUrl: string]: IDisposable; }; @@ -85,12 +88,14 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { @IFileService fileService: IFileService, @ITextModelService textModelResolverService: ITextModelService, @IUntitledEditorService untitledEditorService: IUntitledEditorService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { this._modelService = modelService; this._textModelResolverService = textModelResolverService; this._textFileService = textFileService; this._fileService = fileService; this._untitledEditorService = untitledEditorService; + this._environmentService = environmentService; this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments); this._modelIsSynced = {}; @@ -214,10 +219,10 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { } private _handleUntitledScheme(uri: URI): Promise { - const asFileUri = uri.with({ scheme: Schemas.file }); - return this._fileService.resolve(asFileUri).then(stats => { + const asLocalUri = toLocalResource(uri, this._environmentService.configuration.remoteAuthority); + return this._fileService.resolve(asLocalUri).then(stats => { // don't create a new file ontop of an existing file - return Promise.reject(new Error('file already exists on disk')); + return Promise.reject(new Error('file already exists')); }, err => { return this._doCreateUntitled(uri).then(resource => !!resource); }); diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index b24e33b6fe..db5ee1e41e 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -29,6 +29,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; namespace delta { @@ -330,12 +331,12 @@ export class MainThreadDocumentsAndEditors { @IUntitledEditorService untitledEditorService: IUntitledEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IBulkEditService bulkEditService: IBulkEditService, - @IPanelService panelService: IPanelService - + @IPanelService panelService: IPanelService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); - const mainThreadDocuments = new MainThreadDocuments(this, extHostContext, this._modelService, modeService, this._textFileService, fileService, textModelResolverService, untitledEditorService); + const mainThreadDocuments = new MainThreadDocuments(this, extHostContext, this._modelService, modeService, this._textFileService, fileService, textModelResolverService, untitledEditorService, environmentService); extHostContext.set(MainContext.MainThreadDocuments, mainThreadDocuments); const mainThreadTextEditors = new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._editorService, this._editorGroupService); diff --git a/src/vs/workbench/api/browser/mainThreadFileSystem.ts b/src/vs/workbench/api/browser/mainThreadFileSystem.ts index e0090d0a44..277eb653f3 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystem.ts @@ -10,6 +10,7 @@ import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileSer import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../common/extHost.protocol'; import { ResourceLabelFormatter, ILabelService } from 'vs/platform/label/common/label'; +import { VSBuffer } from 'vs/base/common/buffer'; @extHostNamedCustomer(MainContext.MainThreadFileSystem) export class MainThreadFileSystem implements MainThreadFileSystemShape { @@ -105,10 +106,6 @@ class RemoteFileSystemProvider implements IFileSystemProvider { // --- forwarding calls - private static _asBuffer(data: Uint8Array): Buffer { - return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength); - } - stat(resource: URI): Promise { return this._proxy.$stat(this._handle, resource).then(undefined, err => { throw err; @@ -116,11 +113,11 @@ class RemoteFileSystemProvider implements IFileSystemProvider { } readFile(resource: URI): Promise { - return this._proxy.$readFile(this._handle, resource); + return this._proxy.$readFile(this._handle, resource).then(buffer => buffer.buffer); } writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - return this._proxy.$writeFile(this._handle, resource, RemoteFileSystemProvider._asBuffer(content), opts); + return this._proxy.$writeFile(this._handle, resource, VSBuffer.wrap(content), opts); } delete(resource: URI, opts: FileDeleteOptions): Promise { @@ -153,12 +150,12 @@ class RemoteFileSystemProvider implements IFileSystemProvider { read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return this._proxy.$read(this._handle, fd, pos, length).then(readData => { - data.set(readData, offset); + data.set(readData.buffer, offset); return readData.byteLength; }); } write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - return this._proxy.$write(this._handle, fd, pos, Buffer.from(data, offset, length)); + return this._proxy.$write(this._handle, fd, pos, VSBuffer.wrap(data).slice(offset, offset + length)); } } diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 246856cba2..c53f566596 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -240,6 +240,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { // Nothing @@ -256,8 +257,9 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { return new Promise((resolve, reject) => { const source = new CancellationTokenSource(); + const editorOrModel = findEditor(model, this._codeEditorService) || model; const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', overrides); - const request = this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, model, FormattingMode.Silent, source.token); + const request = this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, source.token); setTimeout(() => { reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)); diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index ef73e60c2c..167fb9e5dd 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -3,47 +3,55 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; import { MainThreadStatusBarShape, MainContext, IExtHostContext } from '../common/extHost.protocol'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { dispose } from 'vs/base/common/lifecycle'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { - private readonly _entries: { [id: number]: IDisposable }; + private readonly entries: Map = new Map(); constructor( - extHostContext: IExtHostContext, - @IStatusbarService private readonly _statusbarService: IStatusbarService - ) { - this._entries = Object.create(null); - } + _extHostContext: IExtHostContext, + @IStatusbarService private readonly statusbarService: IStatusbarService + ) { } dispose(): void { - for (const key in this._entries) { - this._entries[key].dispose(); - } + this.entries.forEach(entry => entry.accessor.dispose()); + this.entries.clear(); } $setEntry(id: number, extensionId: ExtensionIdentifier, text: string, tooltip: string, command: string, color: string | ThemeColor, alignment: MainThreadStatusBarAlignment, priority: number): void { + const entry = { text, tooltip, command, color, extensionId }; - // Dispose any old - this.$dispose(id); + // Reset existing entry if alignment or priority changed + let existingEntry = this.entries.get(id); + if (existingEntry && (existingEntry.alignment !== alignment || existingEntry.priority !== priority)) { + dispose(existingEntry.accessor); + this.entries.delete(id); + existingEntry = undefined; + } - // Add new - const entry = this._statusbarService.addEntry({ text, tooltip, command, color, extensionId }, alignment, priority); - this._entries[id] = entry; + // Create new entry if not existing + if (!existingEntry) { + this.entries.set(id, { accessor: this.statusbarService.addEntry(entry, alignment, priority), alignment, priority }); + } + + // Otherwise update + else { + existingEntry.accessor.update(entry); + } } $dispose(id: number) { - const disposeable = this._entries[id]; - if (disposeable) { - disposeable.dispose(); + const entry = this.entries.get(id); + if (entry) { + dispose(entry.accessor); + this.entries.delete(id); } - - delete this._entries[id]; } } diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index e7905e4875..79c117267f 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -9,17 +9,22 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostWindowShape, IExtHostContext, MainContext, MainThreadWindowShape } from '../common/extHost.protocol'; +import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @extHostNamedCustomer(MainContext.MainThreadWindow) export class MainThreadWindow implements MainThreadWindowShape { private readonly proxy: ExtHostWindowShape; private disposables: IDisposable[] = []; + private readonly _tunnels = new Map>(); constructor( extHostContext: IExtHostContext, @IWindowService private readonly windowService: IWindowService, - @IWindowsService private readonly windowsService: IWindowsService + @IWindowsService private readonly windowsService: IWindowsService, + @ITunnelService private readonly tunnelService: ITunnelService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostWindow); @@ -29,13 +34,54 @@ export class MainThreadWindow implements MainThreadWindowShape { dispose(): void { this.disposables = dispose(this.disposables); + + for (const tunnel of this._tunnels.values()) { + tunnel.then(tunnel => tunnel.dispose()); + } + this._tunnels.clear(); } $getWindowVisibility(): Promise { return this.windowService.isFocused(); } - $openUri(uri: UriComponents): Promise { - return this.windowsService.openExternal(URI.revive(uri).toString(true)); + async $openUri(uriComponent: UriComponents): Promise { + const uri = URI.revive(uriComponent); + if (!!this.environmentService.configuration.remoteAuthority) { + if (uri.scheme === 'http' || uri.scheme === 'https') { + const port = this.getLocalhostPort(uri); + if (typeof port === 'number') { + const tunnel = await this.getOrCreateTunnel(port); + if (tunnel) { + const tunneledUrl = uri.toString().replace( + new RegExp(`^${uri.scheme}://localhost:${port}/`), + `${uri.scheme}://localhost:${tunnel.tunnelLocalPort}/`); + return this.windowsService.openExternal(tunneledUrl); + } + } + } + } + + return this.windowsService.openExternal(uri.toString(true)); + } + + private getLocalhostPort(uri: URI): number | undefined { + const match = /^localhost:(\d+)$/.exec(uri.authority); + if (match) { + return +match[1]; + } + return undefined; + } + + private getOrCreateTunnel(remotePort: number): Promise | undefined { + const existing = this._tunnels.get(remotePort); + if (existing) { + return existing; + } + const tunnel = this.tunnelService.openTunnel(remotePort); + if (tunnel) { + this._tunnels.set(remotePort, tunnel); + } + return tunnel; } } diff --git a/src/vs/workbench/api/node/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts similarity index 99% rename from src/vs/workbench/api/node/apiCommands.ts rename to src/vs/workbench/api/common/apiCommands.ts index 2ee3ac852d..908d69cf1d 100644 --- a/src/vs/workbench/api/node/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; -import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 01f1e498e3..9790b65d78 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -46,6 +46,7 @@ import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset'; import * as callHierarchy from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { IRelativePattern } from 'vs/base/common/glob'; import { IRemoteConsoleLog } from 'vs/base/common/console'; +import { VSBuffer } from 'vs/base/common/buffer'; // {{SQL CARBON EDIT}} import { ITreeItem as sqlITreeItem } from 'sql/workbench/common/views'; @@ -57,7 +58,7 @@ export interface IEnvironment { appLanguage: string; appUriScheme: string; appSettingsHome?: URI; - extensionDevelopmentLocationURI?: URI | URI[]; + extensionDevelopmentLocationURI?: URI[]; extensionTestsLocationURI?: URI; globalStorageHome: URI; userHome: URI; @@ -791,8 +792,8 @@ export interface ExtHostWorkspaceShape { export interface ExtHostFileSystemShape { $stat(handle: number, resource: UriComponents): Promise; $readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]>; - $readFile(handle: number, resource: UriComponents): Promise; - $writeFile(handle: number, resource: UriComponents, content: Buffer, opts: files.FileWriteOptions): Promise; + $readFile(handle: number, resource: UriComponents): Promise; + $writeFile(handle: number, resource: UriComponents, content: VSBuffer, opts: files.FileWriteOptions): Promise; $rename(handle: number, resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise; $copy(handle: number, resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise; $mkdir(handle: number, resource: UriComponents): Promise; @@ -801,8 +802,8 @@ export interface ExtHostFileSystemShape { $unwatch(handle: number, session: number): void; $open(handle: number, resource: UriComponents, opts: files.FileOpenOptions): Promise; $close(handle: number, fd: number): Promise; - $read(handle: number, fd: number, pos: number, length: number): Promise; - $write(handle: number, fd: number, pos: number, data: Buffer): Promise; + $read(handle: number, fd: number, pos: number, length: number): Promise; + $write(handle: number, fd: number, pos: number, data: VSBuffer): Promise; } export interface ExtHostSearchShape { @@ -820,8 +821,8 @@ export interface ExtHostExtensionServiceShape { $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise; $test_latency(n: number): Promise; - $test_up(b: Buffer): Promise; - $test_down(size: number): Promise; + $test_up(b: VSBuffer): Promise; + $test_down(size: number): Promise; } export interface FileSystemEvents { diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts similarity index 98% rename from src/vs/workbench/api/node/extHostApiCommands.ts rename to src/vs/workbench/api/common/extHostApiCommands.ts index 453686d594..3cbc553caf 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -6,15 +6,15 @@ import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as vscode from 'vscode'; -import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import * as types from 'vs/workbench/api/node/extHostTypes'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import * as types from 'vs/workbench/api/common/extHostTypes'; import { IRawColorInfo, WorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import * as search from 'vs/workbench/contrib/search/common/search'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; -import { CustomCodeAction } from 'vs/workbench/api/node/extHostLanguageFeatures'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { CustomCodeAction } from 'vs/workbench/api/common/extHostLanguageFeatures'; import { ICommandsExecutor, OpenFolderAPICommand, DiffAPICommand, OpenAPICommand, RemoveFromRecentlyOpenedAPICommand, SetEditorLayoutAPICommand } from './apiCommands'; import { EditorGroupLayout } from 'vs/workbench/services/editor/common/editorGroupsService'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; diff --git a/src/vs/workbench/api/node/extHostClipboard.ts b/src/vs/workbench/api/common/extHostClipboard.ts similarity index 100% rename from src/vs/workbench/api/node/extHostClipboard.ts rename to src/vs/workbench/api/common/extHostClipboard.ts diff --git a/src/vs/workbench/api/node/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts similarity index 96% rename from src/vs/workbench/api/node/extHostCommands.ts rename to src/vs/workbench/api/common/extHostCommands.ts index 36583389cd..a5c2f0f738 100644 --- a/src/vs/workbench/api/node/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -5,11 +5,11 @@ import { validateConstraint } from 'vs/base/common/types'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; -import * as extHostTypeConverter from 'vs/workbench/api/node/extHostTypeConverters'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; +import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters'; import { cloneAndChange } from 'vs/base/common/objects'; -import { MainContext, MainThreadCommandsShape, ExtHostCommandsShape, ObjectIdentifier, IMainContext, CommandDto } from '../common/extHost.protocol'; -import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; +import { MainContext, MainThreadCommandsShape, ExtHostCommandsShape, ObjectIdentifier, IMainContext, CommandDto } from './extHost.protocol'; +import { ExtHostHeapService } from 'vs/workbench/api/common/extHostHeapService'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import * as modes from 'vs/editor/common/modes'; import * as vscode from 'vscode'; diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts similarity index 99% rename from src/vs/workbench/api/node/extHostComments.ts rename to src/vs/workbench/api/common/extHostComments.ts index d710cc1b2b..4a77d2fe78 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -6,11 +6,11 @@ import { asPromise } from 'vs/base/common/async'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; -import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; -import * as extHostTypeConverter from 'vs/workbench/api/node/extHostTypeConverters'; -import * as types from 'vs/workbench/api/node/extHostTypes'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters'; +import * as types from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; -import { ExtHostCommentsShape, IMainContext, MainContext, MainThreadCommentsShape } from '../common/extHost.protocol'; +import { ExtHostCommentsShape, IMainContext, MainContext, MainThreadCommentsShape } from './extHost.protocol'; import { CommandsConverter, ExtHostCommands } from './extHostCommands'; import { IRange } from 'vs/editor/common/core/range'; import { CancellationToken } from 'vs/base/common/cancellation'; diff --git a/src/vs/workbench/api/node/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts similarity index 99% rename from src/vs/workbench/api/node/extHostConfiguration.ts rename to src/vs/workbench/api/common/extHostConfiguration.ts index 5c77d994eb..3cf2d5a628 100644 --- a/src/vs/workbench/api/node/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -7,8 +7,8 @@ import { mixin, deepClone } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import * as vscode from 'vscode'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; -import { ExtHostConfigurationShape, MainThreadConfigurationShape, IWorkspaceConfigurationChangeEventData, IConfigurationInitData } from '../common/extHost.protocol'; +import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { ExtHostConfigurationShape, MainThreadConfigurationShape, IWorkspaceConfigurationChangeEventData, IConfigurationInitData } from './extHost.protocol'; import { ConfigurationTarget as ExtHostConfigurationTarget } from './extHostTypes'; import { IConfigurationData, ConfigurationTarget, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; import { Configuration, ConfigurationChangeEvent, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; diff --git a/src/vs/workbench/api/node/extHostDecorations.ts b/src/vs/workbench/api/common/extHostDecorations.ts similarity index 97% rename from src/vs/workbench/api/node/extHostDecorations.ts rename to src/vs/workbench/api/common/extHostDecorations.ts index a90f8e42ad..9cb2528cf6 100644 --- a/src/vs/workbench/api/node/extHostDecorations.ts +++ b/src/vs/workbench/api/common/extHostDecorations.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { MainContext, IMainContext, ExtHostDecorationsShape, MainThreadDecorationsShape, DecorationData, DecorationRequest, DecorationReply } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { asArray } from 'vs/base/common/arrays'; diff --git a/src/vs/workbench/api/node/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts similarity index 99% rename from src/vs/workbench/api/node/extHostDiagnostics.ts rename to src/vs/workbench/api/common/extHostDiagnostics.ts index 623f8ffcb4..d193742306 100644 --- a/src/vs/workbench/api/node/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { URI } from 'vs/base/common/uri'; import * as vscode from 'vscode'; -import { MainContext, MainThreadDiagnosticsShape, ExtHostDiagnosticsShape, IMainContext } from '../common/extHost.protocol'; +import { MainContext, MainThreadDiagnosticsShape, ExtHostDiagnosticsShape, IMainContext } from './extHost.protocol'; import { DiagnosticSeverity } from './extHostTypes'; import * as converter from './extHostTypeConverters'; import { mergeSort } from 'vs/base/common/arrays'; diff --git a/src/vs/workbench/api/node/extHostDialogs.ts b/src/vs/workbench/api/common/extHostDialogs.ts similarity index 100% rename from src/vs/workbench/api/node/extHostDialogs.ts rename to src/vs/workbench/api/common/extHostDialogs.ts diff --git a/src/vs/workbench/api/node/extHostDocumentContentProviders.ts b/src/vs/workbench/api/common/extHostDocumentContentProviders.ts similarity index 95% rename from src/vs/workbench/api/node/extHostDocumentContentProviders.ts rename to src/vs/workbench/api/common/extHostDocumentContentProviders.ts index 3a9b5cc0cb..af5bcd6449 100644 --- a/src/vs/workbench/api/node/extHostDocumentContentProviders.ts +++ b/src/vs/workbench/api/common/extHostDocumentContentProviders.ts @@ -6,9 +6,9 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; -import { MainContext, ExtHostDocumentContentProvidersShape, MainThreadDocumentContentProvidersShape, IMainContext } from '../common/extHost.protocol'; +import { MainContext, ExtHostDocumentContentProvidersShape, MainThreadDocumentContentProvidersShape, IMainContext } from './extHost.protocol'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors'; import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/api/node/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts similarity index 99% rename from src/vs/workbench/api/node/extHostDocumentData.ts rename to src/vs/workbench/api/common/extHostDocumentData.ts index 7ffa743ee9..66f82ecb68 100644 --- a/src/vs/workbench/api/node/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/model/wordHelper'; import { MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; -import { EndOfLine, Position, Range } from 'vs/workbench/api/node/extHostTypes'; +import { EndOfLine, Position, Range } from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; const _modeId2WordDefinition = new Map(); diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts similarity index 97% rename from src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts rename to src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index 00c2d98971..26269a732a 100644 --- a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -8,9 +8,9 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { sequence } from 'vs/base/common/async'; import { illegalState } from 'vs/base/common/errors'; import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, ResourceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; -import { TextEdit } from 'vs/workbench/api/node/extHostTypes'; -import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/node/extHostTypeConverters'; -import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; +import { TextEdit } from 'vs/workbench/api/common/extHostTypes'; +import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/common/extHostTypeConverters'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as vscode from 'vscode'; import { LinkedList } from 'vs/base/common/linkedList'; diff --git a/src/vs/workbench/api/node/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts similarity index 96% rename from src/vs/workbench/api/node/extHostDocuments.ts rename to src/vs/workbench/api/common/extHostDocuments.ts index bdb010545c..e59e9bfaed 100644 --- a/src/vs/workbench/api/node/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -8,9 +8,9 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import { ExtHostDocumentsShape, IMainContext, MainContext, MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDocumentData, setWordDefinitionFor } from 'vs/workbench/api/node/extHostDocumentData'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; -import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters'; +import { ExtHostDocumentData, setWordDefinitionFor } from 'vs/workbench/api/common/extHostDocumentData'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as vscode from 'vscode'; export class ExtHostDocuments implements ExtHostDocumentsShape { diff --git a/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts similarity index 94% rename from src/vs/workbench/api/node/extHostDocumentsAndEditors.ts rename to src/vs/workbench/api/common/extHostDocumentsAndEditors.ts index 6bbcebb18a..4014526d65 100644 --- a/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts +++ b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts @@ -8,10 +8,10 @@ import { Emitter, Event } from 'vs/base/common/event'; import { dispose } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocumentData'; -import { ExtHostTextEditor } from 'vs/workbench/api/node/extHostTextEditor'; -import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; +import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsShape { diff --git a/src/vs/workbench/api/node/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts similarity index 99% rename from src/vs/workbench/api/node/extHostExtensionActivator.ts rename to src/vs/workbench/api/common/extHostExtensionActivator.ts index 52d15c0a93..b126ac0ab9 100644 --- a/src/vs/workbench/api/node/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionActivationError, MissingDependencyError } from 'vs/workbench/services/extensions/common/extensions'; diff --git a/src/vs/workbench/api/node/extHostFileSystem.ts b/src/vs/workbench/api/common/extHostFileSystem.ts similarity index 91% rename from src/vs/workbench/api/node/extHostFileSystem.ts rename to src/vs/workbench/api/common/extHostFileSystem.ts index 785bd73330..f4fab83739 100644 --- a/src/vs/workbench/api/node/extHostFileSystem.ts +++ b/src/vs/workbench/api/common/extHostFileSystem.ts @@ -4,18 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { URI, UriComponents } from 'vs/base/common/uri'; -import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape, IFileChangeDto } from '../common/extHost.protocol'; +import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape, IFileChangeDto } from './extHost.protocol'; import * as vscode from 'vscode'; import * as files from 'vs/platform/files/common/files'; import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; -import { FileChangeType } from 'vs/workbench/api/node/extHostTypes'; -import * as typeConverter from 'vs/workbench/api/node/extHostTypeConverters'; -import { ExtHostLanguageFeatures } from 'vs/workbench/api/node/extHostLanguageFeatures'; +import { FileChangeType } from 'vs/workbench/api/common/extHostTypes'; +import * as typeConverter from 'vs/workbench/api/common/extHostTypeConverters'; +import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; import { Schemas } from 'vs/base/common/network'; import { ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { State, StateMachine, LinkComputer, Edge } from 'vs/editor/common/modes/linkComputer'; import { commonPrefixLength } from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; +import { VSBuffer } from 'vs/base/common/buffer'; class FsLinkProvider { @@ -229,14 +230,12 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { return Promise.resolve(this.getProvider(handle).readDirectory(URI.revive(resource))); } - $readFile(handle: number, resource: UriComponents): Promise { - return Promise.resolve(this.getProvider(handle).readFile(URI.revive(resource))).then(data => { - return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength); - }); + $readFile(handle: number, resource: UriComponents): Promise { + return Promise.resolve(this.getProvider(handle).readFile(URI.revive(resource))).then(data => VSBuffer.wrap(data)); } - $writeFile(handle: number, resource: UriComponents, content: Buffer, opts: files.FileWriteOptions): Promise { - return Promise.resolve(this.getProvider(handle).writeFile(URI.revive(resource), content, opts)); + $writeFile(handle: number, resource: UriComponents, content: VSBuffer, opts: files.FileWriteOptions): Promise { + return Promise.resolve(this.getProvider(handle).writeFile(URI.revive(resource), content.buffer, opts)); } $delete(handle: number, resource: UriComponents, opts: files.FileDeleteOptions): Promise { @@ -288,23 +287,23 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { return Promise.resolve(provider.close(fd)); } - $read(handle: number, fd: number, pos: number, length: number): Promise { + $read(handle: number, fd: number, pos: number, length: number): Promise { const provider = this.getProvider(handle); if (!provider.read) { throw new Error('FileSystemProvider does not implement "read"'); } - const data = Buffer.allocUnsafe(length); - return Promise.resolve(provider.read(fd, pos, data, 0, length)).then(read => { + const data = VSBuffer.alloc(length); + return Promise.resolve(provider.read(fd, pos, data.buffer, 0, length)).then(read => { return data.slice(0, read); // don't send zeros }); } - $write(handle: number, fd: number, pos: number, data: Buffer): Promise { + $write(handle: number, fd: number, pos: number, data: VSBuffer): Promise { const provider = this.getProvider(handle); if (!provider.write) { throw new Error('FileSystemProvider does not implement "write"'); } - return Promise.resolve(provider.write(fd, pos, data, 0, data.length)); + return Promise.resolve(provider.write(fd, pos, data.buffer, 0, data.byteLength)); } private getProvider(handle: number): vscode.FileSystemProvider { diff --git a/src/vs/workbench/api/node/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts similarity index 97% rename from src/vs/workbench/api/node/extHostFileSystemEventService.ts rename to src/vs/workbench/api/common/extHostFileSystemEventService.ts index 14ab0c9e1d..94e7dcb1f6 100644 --- a/src/vs/workbench/api/node/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -7,9 +7,9 @@ import { flatten } from 'vs/base/common/arrays'; import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, ResourceFileEditDto, ResourceTextEditDto, MainThreadTextEditorsShape } from '../common/extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, ResourceFileEditDto, ResourceTextEditDto, MainThreadTextEditorsShape } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/api/node/extHostHeapService.ts b/src/vs/workbench/api/common/extHostHeapService.ts similarity index 92% rename from src/vs/workbench/api/node/extHostHeapService.ts rename to src/vs/workbench/api/common/extHostHeapService.ts index 3991b7beb1..fa429b8dca 100644 --- a/src/vs/workbench/api/node/extHostHeapService.ts +++ b/src/vs/workbench/api/common/extHostHeapService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtHostHeapServiceShape } from '../common/extHost.protocol'; +import { ExtHostHeapServiceShape } from './extHost.protocol'; export class ExtHostHeapService implements ExtHostHeapServiceShape { diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts similarity index 99% rename from src/vs/workbench/api/node/extHostLanguageFeatures.ts rename to src/vs/workbench/api/common/extHostLanguageFeatures.ts index 13da1959e5..a504b46245 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -6,16 +6,16 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { mixin } from 'vs/base/common/objects'; import * as vscode from 'vscode'; -import * as typeConvert from 'vs/workbench/api/node/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol } from 'vs/workbench/api/node/extHostTypes'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol } from 'vs/workbench/api/common/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; -import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; -import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; -import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; -import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; +import { ExtHostHeapService } from 'vs/workbench/api/common/extHostHeapService'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import { asPromise } from 'vs/base/common/async'; -import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, MainThreadWebviewsShape, CodeInsetDto, SuggestDataDto, LinksListDto, ChainedCacheId } from '../common/extHost.protocol'; +import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, MainThreadWebviewsShape, CodeInsetDto, SuggestDataDto, LinksListDto, ChainedCacheId } from './extHost.protocol'; import { regExpLeadsToEndlessLoop, regExpFlags } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; @@ -25,7 +25,7 @@ import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ExtHostWebview } from 'vs/workbench/api/node/extHostWebview'; +import { ExtHostWebview } from 'vs/workbench/api/common/extHostWebview'; import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset'; import { generateUuid } from 'vs/base/common/uuid'; import * as callHierarchy from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; diff --git a/src/vs/workbench/api/node/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts similarity index 91% rename from src/vs/workbench/api/node/extHostLanguages.ts rename to src/vs/workbench/api/common/extHostLanguages.ts index 0eecc843b9..2016185902 100644 --- a/src/vs/workbench/api/node/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -3,9 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainContext, MainThreadLanguagesShape, IMainContext } from '../common/extHost.protocol'; +import { MainContext, MainThreadLanguagesShape, IMainContext } from './extHost.protocol'; import * as vscode from 'vscode'; -import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; export class ExtHostLanguages { diff --git a/src/vs/workbench/api/node/extHostLogService.ts b/src/vs/workbench/api/common/extHostLogService.ts similarity index 88% rename from src/vs/workbench/api/node/extHostLogService.ts rename to src/vs/workbench/api/common/extHostLogService.ts index 391fcdf905..64f865a3f8 100644 --- a/src/vs/workbench/api/node/extHostLogService.ts +++ b/src/vs/workbench/api/common/extHostLogService.ts @@ -5,7 +5,6 @@ import { join } from 'vs/base/common/path'; import { ILogService, DelegatedLogService, LogLevel } from 'vs/platform/log/common/log'; -import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; import { ExtHostLogServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; @@ -17,10 +16,10 @@ export class ExtHostLogService extends DelegatedLogService implements ILogServic readonly logFile: URI; constructor( - logLevel: LogLevel, + delegate: ILogService, logsPath: string, ) { - super(createSpdLogService(ExtensionHostLogFileName, logLevel, logsPath)); + super(delegate); this._logsPath = logsPath; this.logFile = URI.file(join(logsPath, `${ExtensionHostLogFileName}.log`)); } diff --git a/src/vs/workbench/api/node/extHostMessageService.ts b/src/vs/workbench/api/common/extHostMessageService.ts similarity index 97% rename from src/vs/workbench/api/node/extHostMessageService.ts rename to src/vs/workbench/api/common/extHostMessageService.ts index 603132e65e..db03a9f4d6 100644 --- a/src/vs/workbench/api/node/extHostMessageService.ts +++ b/src/vs/workbench/api/common/extHostMessageService.ts @@ -5,7 +5,7 @@ import Severity from 'vs/base/common/severity'; import * as vscode from 'vscode'; -import { MainContext, MainThreadMessageServiceShape, MainThreadMessageOptions, IMainContext } from '../common/extHost.protocol'; +import { MainContext, MainThreadMessageServiceShape, MainThreadMessageOptions, IMainContext } from './extHost.protocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; function isMessageItem(item: any): item is vscode.MessageItem { diff --git a/src/vs/workbench/api/common/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts new file mode 100644 index 0000000000..5dbf1fd509 --- /dev/null +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MainContext, MainThreadOutputServiceShape, IMainContext, ExtHostOutputServiceShape } from './extHost.protocol'; +import * as vscode from 'vscode'; +import { URI } from 'vs/base/common/uri'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; + +export abstract class AbstractExtHostOutputChannel extends Disposable implements vscode.OutputChannel { + + readonly _id: Promise; + private readonly _name: string; + protected readonly _proxy: MainThreadOutputServiceShape; + private _disposed: boolean; + private _offset: number; + + protected readonly _onDidAppend: Emitter = this._register(new Emitter()); + readonly onDidAppend: Event = this._onDidAppend.event; + + constructor(name: string, log: boolean, file: URI | undefined, proxy: MainThreadOutputServiceShape) { + super(); + + this._name = name; + this._proxy = proxy; + this._id = proxy.$register(this.name, log, file); + this._offset = 0; + } + + get name(): string { + return this._name; + } + + append(value: string): void { + this.validate(); + this._offset += value ? Buffer.from(value).byteLength : 0; + } + + update(): void { + this._id.then(id => this._proxy.$update(id)); + } + + appendLine(value: string): void { + this.validate(); + this.append(value + '\n'); + } + + clear(): void { + this.validate(); + const till = this._offset; + this._id.then(id => this._proxy.$clear(id, till)); + } + + show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { + this.validate(); + this._id.then(id => this._proxy.$reveal(id, !!(typeof columnOrPreserveFocus === 'boolean' ? columnOrPreserveFocus : preserveFocus))); + } + + hide(): void { + this.validate(); + this._id.then(id => this._proxy.$close(id)); + } + + protected validate(): void { + if (this._disposed) { + throw new Error('Channel has been closed'); + } + } + + dispose(): void { + super.dispose(); + + if (!this._disposed) { + this._id + .then(id => this._proxy.$dispose(id)) + .then(() => this._disposed = true); + } + } +} + +export class ExtHostPushOutputChannel extends AbstractExtHostOutputChannel { + + constructor(name: string, proxy: MainThreadOutputServiceShape) { + super(name, false, undefined, proxy); + } + + append(value: string): void { + super.append(value); + this._id.then(id => this._proxy.$append(id, value)); + this._onDidAppend.fire(); + } +} + +class ExtHostLogFileOutputChannel extends AbstractExtHostOutputChannel { + + constructor(name: string, file: URI, proxy: MainThreadOutputServiceShape) { + super(name, true, file, proxy); + } + + append(value: string): void { + throw new Error('Not supported'); + } +} + +export interface IOutputChannelFactory { + createOutputChannel(name: string, logsLocation: URI, proxy: MainThreadOutputServiceShape): Promise; +} + +export const PushOutputChannelFactory = new class implements IOutputChannelFactory { + async createOutputChannel(name: string, _logsLocation: URI, proxy: MainThreadOutputServiceShape): Promise { + return new ExtHostPushOutputChannel(name, proxy); + } +}; + +export class ExtHostOutputService implements ExtHostOutputServiceShape { + + private readonly _factory: IOutputChannelFactory; + private readonly _logsLocation: URI; + private readonly _proxy: MainThreadOutputServiceShape; + private readonly _channels: Map = new Map(); + private _visibleChannelDisposable: IDisposable; + + constructor(factory: IOutputChannelFactory, logsLocation: URI, mainContext: IMainContext) { + this._factory = factory; + this._logsLocation = logsLocation; + this._proxy = mainContext.getProxy(MainContext.MainThreadOutputService); + } + + $setVisibleChannel(channelId: string): void { + if (this._visibleChannelDisposable) { + this._visibleChannelDisposable = dispose(this._visibleChannelDisposable); + } + if (channelId) { + const channel = this._channels.get(channelId); + if (channel) { + this._visibleChannelDisposable = channel.onDidAppend(() => channel.update()); + } + } + } + + createOutputChannel(name: string): vscode.OutputChannel { + name = name.trim(); + if (!name) { + throw new Error('illegal argument `name`. must not be falsy'); + } else { + const extHostOutputChannel = this._factory.createOutputChannel(name, this._logsLocation, this._proxy); + extHostOutputChannel.then(channel => channel._id.then(id => this._channels.set(id, channel))); + return { + get name(): string { + return name; + }, + append(value: string): void { + extHostOutputChannel.then(channel => channel.append(value)); + }, + appendLine(value: string): void { + extHostOutputChannel.then(channel => channel.appendLine(value)); + }, + clear(): void { + extHostOutputChannel.then(channel => channel.clear()); + }, + show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { + extHostOutputChannel.then(channel => channel.show(columnOrPreserveFocus, preserveFocus)); + }, + hide(): void { + extHostOutputChannel.then(channel => channel.hide()); + }, + dispose(): void { + extHostOutputChannel.then(channel => channel.dispose()); + } + }; + } + } + + createOutputChannelFromLogFile(name: string, file: URI): vscode.OutputChannel { + name = name.trim(); + if (!name) { + throw new Error('illegal argument `name`. must not be falsy'); + } + if (!file) { + throw new Error('illegal argument `file`. must not be falsy'); + } + return new ExtHostLogFileOutputChannel(name, file, this._proxy); + } +} diff --git a/src/vs/workbench/api/node/extHostProgress.ts b/src/vs/workbench/api/common/extHostProgress.ts similarity index 99% rename from src/vs/workbench/api/node/extHostProgress.ts rename to src/vs/workbench/api/common/extHostProgress.ts index ac96f113fd..54413567e3 100644 --- a/src/vs/workbench/api/node/extHostProgress.ts +++ b/src/vs/workbench/api/common/extHostProgress.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ProgressOptions } from 'vscode'; -import { MainThreadProgressShape, ExtHostProgressShape } from '../common/extHost.protocol'; +import { MainThreadProgressShape, ExtHostProgressShape } from './extHost.protocol'; import { ProgressLocation } from './extHostTypeConverters'; import { Progress, IProgressStep } from 'vs/platform/progress/common/progress'; import { localize } from 'vs/nls'; diff --git a/src/vs/workbench/api/node/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts similarity index 98% rename from src/vs/workbench/api/node/extHostQuickOpen.ts rename to src/vs/workbench/api/common/extHostQuickOpen.ts index 4e3ca68410..d013a4272b 100644 --- a/src/vs/workbench/api/node/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -7,12 +7,12 @@ import { asPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; -import { IExtHostWorkspaceProvider } from 'vs/workbench/api/node/extHostWorkspace'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; import { InputBox, InputBoxOptions, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; -import { ExtHostQuickOpenShape, IMainContext, MainContext, MainThreadQuickOpenShape, TransferQuickPickItems, TransferQuickInput, TransferQuickInputButton } from '../common/extHost.protocol'; +import { ExtHostQuickOpenShape, IMainContext, MainContext, MainThreadQuickOpenShape, TransferQuickPickItems, TransferQuickInput, TransferQuickInputButton } from './extHost.protocol'; import { URI } from 'vs/base/common/uri'; -import { ThemeIcon, QuickInputButtons } from 'vs/workbench/api/node/extHostTypes'; +import { ThemeIcon, QuickInputButtons } from 'vs/workbench/api/common/extHostTypes'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { coalesce } from 'vs/base/common/arrays'; diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts similarity index 99% rename from src/vs/workbench/api/node/extHostSCM.ts rename to src/vs/workbench/api/common/extHostSCM.ts index 8fdec949f4..64c75d42b9 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -8,8 +8,8 @@ import { Event, Emitter } from 'vs/base/common/event'; import { debounce } from 'vs/base/common/decorators'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { asPromise } from 'vs/base/common/async'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; -import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, CommandDto } from '../common/extHost.protocol'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, CommandDto } from './extHost.protocol'; import { sortedDiff } from 'vs/base/common/arrays'; import { comparePaths } from 'vs/base/common/comparers'; import * as vscode from 'vscode'; diff --git a/src/vs/workbench/api/node/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts similarity index 99% rename from src/vs/workbench/api/node/extHostStatusBar.ts rename to src/vs/workbench/api/common/extHostStatusBar.ts index 291334dd84..547b6c0886 100644 --- a/src/vs/workbench/api/node/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -6,7 +6,7 @@ import { StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/platform/statusbar/common/statusbar'; import { StatusBarAlignment as ExtHostStatusBarAlignment, Disposable, ThemeColor } from './extHostTypes'; import { StatusBarItem, StatusBarAlignment } from 'vscode'; -import { MainContext, MainThreadStatusBarShape, IMainContext } from '../common/extHost.protocol'; +import { MainContext, MainThreadStatusBarShape, IMainContext } from './extHost.protocol'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export class ExtHostStatusBarEntry implements StatusBarItem { diff --git a/src/vs/workbench/api/node/extHostStorage.ts b/src/vs/workbench/api/common/extHostStorage.ts similarity index 96% rename from src/vs/workbench/api/node/extHostStorage.ts rename to src/vs/workbench/api/common/extHostStorage.ts index a95e7e0e2f..228c70b9b2 100644 --- a/src/vs/workbench/api/node/extHostStorage.ts +++ b/src/vs/workbench/api/common/extHostStorage.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainContext, MainThreadStorageShape, IMainContext, ExtHostStorageShape } from '../common/extHost.protocol'; +import { MainContext, MainThreadStorageShape, IMainContext, ExtHostStorageShape } from './extHost.protocol'; import { Emitter } from 'vs/base/common/event'; export interface IStorageChangeEvent { diff --git a/src/vs/workbench/api/node/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts similarity index 99% rename from src/vs/workbench/api/node/extHostTextEditor.ts rename to src/vs/workbench/api/common/extHostTextEditor.ts index 45f6387e0d..425b2e5af1 100644 --- a/src/vs/workbench/api/node/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -10,9 +10,9 @@ import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { IRange } from 'vs/editor/common/core/range'; import { ISingleEditOperation } from 'vs/editor/common/model'; import { IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocumentData'; -import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import { EndOfLine, Position, Range, Selection, SnippetString, TextEditorLineNumbersStyle, TextEditorRevealType } from 'vs/workbench/api/node/extHostTypes'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; +import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { EndOfLine, Position, Range, Selection, SnippetString, TextEditorLineNumbersStyle, TextEditorRevealType } from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; export class TextEditorDecorationType implements vscode.TextEditorDecorationType { diff --git a/src/vs/workbench/api/node/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts similarity index 96% rename from src/vs/workbench/api/node/extHostTextEditors.ts rename to src/vs/workbench/api/common/extHostTextEditors.ts index f47b678642..85ea8fdd1a 100644 --- a/src/vs/workbench/api/node/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -6,10 +6,10 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as arrays from 'vs/base/common/arrays'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; -import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/node/extHostTextEditor'; -import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import { TextEditorSelectionChangeKind } from 'vs/workbench/api/node/extHostTypes'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; +import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; export class ExtHostEditors implements ExtHostEditorsShape { diff --git a/src/vs/workbench/api/node/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts similarity index 99% rename from src/vs/workbench/api/node/extHostTreeViews.ts rename to src/vs/workbench/api/common/extHostTreeViews.ts index ba25c29f63..b11f50f8eb 100644 --- a/src/vs/workbench/api/node/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -9,11 +9,11 @@ import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from '../common/extHost.protocol'; +import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions } from 'vs/workbench/common/views'; -import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; +import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { asPromise } from 'vs/base/common/async'; -import { TreeItemCollapsibleState, ThemeIcon, MarkdownString } from 'vs/workbench/api/node/extHostTypes'; +import { TreeItemCollapsibleState, ThemeIcon, MarkdownString } from 'vs/workbench/api/common/extHostTypes'; import { isUndefinedOrNull, isString } from 'vs/base/common/types'; import { equals, coalesce } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts similarity index 99% rename from src/vs/workbench/api/node/extHostTypeConverters.ts rename to src/vs/workbench/api/common/extHostTypeConverters.ts index b3daebbb1d..7380a1f850 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -22,7 +22,7 @@ import * as languageSelector from 'vs/editor/common/modes/languageSelector'; import { WorkspaceEditDto, ResourceTextEditDto, ResourceFileEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { MarkerSeverity, IRelatedInformation, IMarkerData, MarkerTag } from 'vs/platform/markers/common/markers'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { isString, isNumber } from 'vs/base/common/types'; import * as marked from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts similarity index 100% rename from src/vs/workbench/api/node/extHostTypes.ts rename to src/vs/workbench/api/common/extHostTypes.ts diff --git a/src/vs/workbench/api/node/extHostUrls.ts b/src/vs/workbench/api/common/extHostUrls.ts similarity index 97% rename from src/vs/workbench/api/node/extHostUrls.ts rename to src/vs/workbench/api/common/extHostUrls.ts index 83d767b348..930d05087b 100644 --- a/src/vs/workbench/api/node/extHostUrls.ts +++ b/src/vs/workbench/api/common/extHostUrls.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { MainContext, IMainContext, ExtHostUrlsShape, MainThreadUrlsShape } from '../common/extHost.protocol'; +import { MainContext, IMainContext, ExtHostUrlsShape, MainThreadUrlsShape } from './extHost.protocol'; import { URI, UriComponents } from 'vs/base/common/uri'; import { toDisposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts similarity index 98% rename from src/vs/workbench/api/node/extHostWebview.ts rename to src/vs/workbench/api/common/extHostWebview.ts index a6750dd181..83893e8f52 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -5,10 +5,10 @@ import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import * as vscode from 'vscode'; -import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState, WebviewInsetHandle } from '../common/extHost.protocol'; +import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState, WebviewInsetHandle } from './extHost.protocol'; import { Disposable } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/api/node/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts similarity index 76% rename from src/vs/workbench/api/node/extHostWindow.ts rename to src/vs/workbench/api/common/extHostWindow.ts index e7d29e9556..c15a5c76e5 100644 --- a/src/vs/workbench/api/node/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostWindowShape, MainContext, MainThreadWindowShape, IMainContext } from '../common/extHost.protocol'; +import { ExtHostWindowShape, MainContext, MainThreadWindowShape, IMainContext } from './extHost.protocol'; import { WindowState } from 'vscode'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -38,12 +38,19 @@ export class ExtHostWindow implements ExtHostWindowShape { this._onDidChangeWindowState.fire(this._state); } - openUri(uri: URI): Promise { - if (isFalsyOrWhitespace(uri.scheme)) { - return Promise.reject('Invalid scheme - cannot be empty'); - } else if (uri.scheme === Schemas.command) { - return Promise.reject(`Invalid scheme '${uri.scheme}'`); + openUri(stringOrUri: string | URI): Promise { + if (typeof stringOrUri === 'string') { + try { + stringOrUri = URI.parse(stringOrUri); + } catch (e) { + return Promise.reject(`Invalid uri - '${stringOrUri}'`); + } } - return this._proxy.$openUri(uri); + if (isFalsyOrWhitespace(stringOrUri.scheme)) { + return Promise.reject('Invalid scheme - cannot be empty'); + } else if (stringOrUri.scheme === Schemas.command) { + return Promise.reject(`Invalid scheme '${stringOrUri.scheme}'`); + } + return this._proxy.$openUri(stringOrUri); } } diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts similarity index 99% rename from src/vs/workbench/api/node/extHostWorkspace.ts rename to src/vs/workbench/api/common/extHostWorkspace.ts index 7bcdd527af..10af142a5e 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -18,10 +18,10 @@ import { ILogService } from 'vs/platform/log/common/log'; import { Severity } from 'vs/platform/notification/common/notification'; import { IRawFileMatch2, resultIsMatch } from 'vs/workbench/services/search/common/search'; import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { Range, RelativePattern } from 'vs/workbench/api/node/extHostTypes'; +import { Range, RelativePattern } from 'vs/workbench/api/common/extHostTypes'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import * as vscode from 'vscode'; -import { ExtHostWorkspaceShape, IWorkspaceData, MainThreadMessageServiceShape, MainThreadWorkspaceShape, IMainContext, MainContext, IStaticWorkspaceData } from '../common/extHost.protocol'; +import { ExtHostWorkspaceShape, IWorkspaceData, MainThreadMessageServiceShape, MainThreadWorkspaceShape, IMainContext, MainContext, IStaticWorkspaceData } from './extHost.protocol'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Barrier } from 'vs/base/common/async'; diff --git a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts index 44aa26ab31..2cb4fd0a1d 100644 --- a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts @@ -55,7 +55,7 @@ import '../browser/mainThreadWorkspace'; import '../browser/mainThreadComments'; import '../browser/mainThreadTask'; import './mainThreadWebview'; -import 'vs/workbench/api/node/apiCommands'; +import 'vs/workbench/api/common/apiCommands'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 22f47c6ae1..19aa221c1a 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -16,58 +16,60 @@ import { OverviewRulerLane } from 'vs/editor/common/model'; import * as languageConfiguration from 'vs/editor/common/modes/languageConfiguration'; import { score } from 'vs/editor/common/modes/languageSelector'; import * as files from 'vs/platform/files/common/files'; -import { ExtHostContext, IInitData, IMainContext, MainContext, MainThreadKeytarShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostApiCommands } from 'vs/workbench/api/node/extHostApiCommands'; -import { ExtHostClipboard } from 'vs/workbench/api/node/extHostClipboard'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; -import { ExtHostComments } from 'vs/workbench/api/node/extHostComments'; -import { ExtHostConfiguration, ExtHostConfigProvider } from 'vs/workbench/api/node/extHostConfiguration'; +import { ExtHostContext, IInitData, IMainContext, MainContext, MainThreadKeytarShape, IEnvironment, MainThreadWindowShape, MainThreadTelemetryShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostApiCommands } from 'vs/workbench/api/common/extHostApiCommands'; +import { ExtHostClipboard } from 'vs/workbench/api/common/extHostClipboard'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostComments } from 'vs/workbench/api/common/extHostComments'; +import { ExtHostConfiguration, ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; // {{SQL CARBON EDIT}} - Remove ExtHostDebugService // import { ExtHostDebugService } from 'vs/workbench/api/node/extHostDebugService'; // {{SQL CARBON EDIT}} - Import product import product from 'vs/platform/product/node/product'; -import { ExtHostDecorations } from 'vs/workbench/api/node/extHostDecorations'; -import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; -import { ExtHostDialogs } from 'vs/workbench/api/node/extHostDialogs'; -import { ExtHostDocumentContentProvider } from 'vs/workbench/api/node/extHostDocumentContentProviders'; -import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; -import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; -import { ExtensionActivatedByAPI } from 'vs/workbench/api/node/extHostExtensionActivator'; +import { ExtHostDecorations } from 'vs/workbench/api/common/extHostDecorations'; +import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; +import { ExtHostDialogs } from 'vs/workbench/api/common/extHostDialogs'; +import { ExtHostDocumentContentProvider } from 'vs/workbench/api/common/extHostDocumentContentProviders'; +import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostDocumentSaveParticipant'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtensionActivatedByAPI } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; -import { ExtHostFileSystem } from 'vs/workbench/api/node/extHostFileSystem'; -import { ExtHostFileSystemEventService } from 'vs/workbench/api/node/extHostFileSystemEventService'; -import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; -import { ExtHostLanguageFeatures, ISchemeTransformer } from 'vs/workbench/api/node/extHostLanguageFeatures'; -import { ExtHostLanguages } from 'vs/workbench/api/node/extHostLanguages'; -import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; -import { ExtHostMessageService } from 'vs/workbench/api/node/extHostMessageService'; -import { ExtHostOutputService } from 'vs/workbench/api/node/extHostOutputService'; -import { ExtHostProgress } from 'vs/workbench/api/node/extHostProgress'; -import { ExtHostQuickOpen } from 'vs/workbench/api/node/extHostQuickOpen'; -import { ExtHostSCM } from 'vs/workbench/api/node/extHostSCM'; +import { ExtHostFileSystem } from 'vs/workbench/api/common/extHostFileSystem'; +import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; +import { ExtHostHeapService } from 'vs/workbench/api/common/extHostHeapService'; +import { ExtHostLanguageFeatures, ISchemeTransformer } from 'vs/workbench/api/common/extHostLanguageFeatures'; +import { ExtHostLanguages } from 'vs/workbench/api/common/extHostLanguages'; +import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; +import { ExtHostMessageService } from 'vs/workbench/api/common/extHostMessageService'; +import { ExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; +import { LogOutputChannelFactory } from 'vs/workbench/api/node/extHostOutputService'; +import { ExtHostProgress } from 'vs/workbench/api/common/extHostProgress'; +import { ExtHostQuickOpen } from 'vs/workbench/api/common/extHostQuickOpen'; +import { ExtHostSCM } from 'vs/workbench/api/common/extHostSCM'; import { ExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; -import { ExtHostStatusBar } from 'vs/workbench/api/node/extHostStatusBar'; -import { ExtHostStorage } from 'vs/workbench/api/node/extHostStorage'; +import { ExtHostStatusBar } from 'vs/workbench/api/common/extHostStatusBar'; +import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostTask } from 'vs/workbench/api/node/extHostTask'; import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService'; -import { ExtHostEditors } from 'vs/workbench/api/node/extHostTextEditors'; -import { ExtHostTreeViews } from 'vs/workbench/api/node/extHostTreeViews'; -import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; -import { ExtHostUrls } from 'vs/workbench/api/node/extHostUrls'; -import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview'; -import { ExtHostWindow } from 'vs/workbench/api/node/extHostWindow'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; +import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; +import { ExtHostTreeViews } from 'vs/workbench/api/common/extHostTreeViews'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; +import { ExtHostUrls } from 'vs/workbench/api/common/extHostUrls'; +import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWindow } from 'vs/workbench/api/common/extHostWindow'; +import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { throwProposedApiError, checkProposedApiEnabled, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as vscode from 'vscode'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { originalFSPath } from 'vs/base/common/resources'; import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { withNullAsUndefined } from 'vs/base/common/types'; import { values } from 'vs/base/common/collections'; +import { endsWith } from 'vs/base/common/strings'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -126,7 +128,7 @@ export function createApiFactory( const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); - const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, new ExtHostOutputService(initData.logsLocation, rpcProtocol)); + const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, new ExtHostOutputService(LogOutputChannelFactory, initData.logsLocation, rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); if (initData.remoteAuthority) { const cliServer = new CLIServer(extHostCommands); @@ -880,34 +882,59 @@ class Extension implements vscode.Extension { } } +interface LoadFunction { + (request: string, parent: { filename: string; }, isMain: any): any; +} + interface INodeModuleFactory { - readonly nodeModuleName: string; - load(request: string, parent: { filename: string; }): any; + readonly nodeModuleName: string | string[]; + load(request: string, parent: { filename: string; }, isMain: any, original: LoadFunction): any; + alternaiveModuleName?(name: string): string | undefined; } export class NodeModuleRequireInterceptor { public static INSTANCE = new NodeModuleRequireInterceptor(); private readonly _factories: Map; + private readonly _alternatives: ((moduleName: string) => string | undefined)[]; constructor() { this._factories = new Map(); - this._installInterceptor(this._factories); + this._alternatives = []; + this._installInterceptor(this._factories, this._alternatives); } - private _installInterceptor(factories: Map): void { + private _installInterceptor(factories: Map, alternatives: ((moduleName: string) => string | undefined)[]): void { const node_module = require.__$__nodeRequire('module'); const original = node_module._load; node_module._load = function load(request: string, parent: { filename: string; }, isMain: any) { + for (let alternativeModuleName of alternatives) { + let alternative = alternativeModuleName(request); + if (alternative) { + request = alternative; + break; + } + } if (!factories.has(request)) { return original.apply(this, arguments); } - return factories.get(request)!.load(request, parent); + return factories.get(request)!.load(request, parent, isMain, original); }; } public register(interceptor: INodeModuleFactory): void { - this._factories.set(interceptor.nodeModuleName, interceptor); + if (Array.isArray(interceptor.nodeModuleName)) { + for (let moduleName of interceptor.nodeModuleName) { + this._factories.set(moduleName, interceptor); + } + } else { + this._factories.set(interceptor.nodeModuleName, interceptor); + } + if (typeof interceptor.alternaiveModuleName === 'function') { + this._alternatives.push((moduleName) => { + return interceptor.alternaiveModuleName!(moduleName); + }); + } } } @@ -957,11 +984,24 @@ interface IKeytarModule { } export class KeytarNodeModuleFactory implements INodeModuleFactory { - public readonly nodeModuleName = 'keytar'; + public readonly nodeModuleName: string = 'keytar'; + private alternativeNames: Set | undefined; private _impl: IKeytarModule; - constructor(mainThreadKeytar: MainThreadKeytarShape) { + constructor(mainThreadKeytar: MainThreadKeytarShape, environment: IEnvironment) { + if (environment.appRoot) { + let appRoot = environment.appRoot.fsPath; + if (process.platform === 'win32') { + appRoot = appRoot.replace(/\\/g, '/'); + } + if (appRoot[appRoot.length - 1] === '/') { + appRoot = appRoot.substr(0, appRoot.length - 1); + } + this.alternativeNames = new Set(); + this.alternativeNames.add(`${appRoot}/node_modules.asar/keytar`); + this.alternativeNames.add(`${appRoot}/node_modules/keytar`); + } this._impl = { getPassword: (service: string, account: string): Promise => { return mainThreadKeytar.$getPassword(service, account); @@ -981,4 +1021,100 @@ export class KeytarNodeModuleFactory implements INodeModuleFactory { public load(request: string, parent: { filename: string; }): any { return this._impl; } + + public alternaiveModuleName(name: string): string | undefined { + const length = name.length; + // We need at least something like: `?/keytar` which requires + // more than 7 characters. + if (length <= 7 || !this.alternativeNames) { + return undefined; + } + const sep = length - 7; + if ((name.charAt(sep) === '/' || name.charAt(sep) === '\\') && endsWith(name, 'keytar')) { + name = name.replace(/\\/g, '/'); + if (this.alternativeNames.has(name)) { + return 'keytar'; + } + } + return undefined; + } +} + +interface OpenOptions { + wait: boolean; + app: string | string[]; +} + +interface IOriginalOpen { + (target: string, options?: OpenOptions): Thenable; +} + +interface IOpenModule { + (target: string, options?: OpenOptions): Thenable; +} + +export class OpenNodeModuleFactory implements INodeModuleFactory { + + public readonly nodeModuleName: string[] = ['open', 'opn']; + + private _extensionId: string | undefined; + private _original: IOriginalOpen; + private _impl: IOpenModule; + + constructor(mainThreadWindow: MainThreadWindowShape, private _mainThreadTelemerty: MainThreadTelemetryShape, private readonly _extensionPaths: TernarySearchTree) { + this._impl = (target, options) => { + const uri: URI = URI.parse(target); + // If we have options use the original method. + if (options) { + return this.callOriginal(target, options); + } + if (uri.scheme === 'http' || uri.scheme === 'https') { + return mainThreadWindow.$openUri(uri); + } else if (uri.scheme === 'mailto') { + return mainThreadWindow.$openUri(uri); + } + return this.callOriginal(target, options); + }; + } + + public load(request: string, parent: { filename: string; }, isMain: any, original: LoadFunction): any { + // get extension id from filename and api for extension + const extension = this._extensionPaths.findSubstr(URI.file(parent.filename).fsPath); + if (extension) { + this._extensionId = extension.identifier.value; + this.sendShimmingTelemetry(); + } + + this._original = original(request, parent, isMain); + return this._impl; + } + + private callOriginal(target: string, options: OpenOptions | undefined): Thenable { + this.sendNoForwardTelemetry(); + return this._original(target, options); + } + + private sendShimmingTelemetry(): void { + if (!this._extensionId) { + return; + } + /* __GDPR__ + "shimming.open" : { + "extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this._mainThreadTelemerty.$publicLog('shimming.open', { extension: this._extensionId }); + } + + private sendNoForwardTelemetry(): void { + if (!this._extensionId) { + return; + } + /* __GDPR__ + "shimming.open.call.noForward" : { + "extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this._mainThreadTelemerty.$publicLog('shimming.open.call.noForward', { extension: this._extensionId }); + } } diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index ec00bbd207..78eae2800b 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -6,7 +6,7 @@ import { generateRandomPipeName } from 'vs/base/parts/ipc/node/ipc.net'; import * as http from 'http'; import * as fs from 'fs'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { IURIToOpen, IOpenSettings } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 5498a79aea..aab3c8baf4 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -16,24 +16,24 @@ import { IMainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto } from 'vs/workbench/api/common/extHost.protocol'; import * as vscode from 'vscode'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable } from 'vs/workbench/api/node/extHostTypes'; +import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable } from 'vs/workbench/api/common/extHostTypes'; import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { IExtHostWorkspaceProvider } from 'vs/workbench/api/node/extHostWorkspace'; +import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { ITerminalSettings, IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor } from 'vs/workbench/contrib/debug/common/debug'; import { getTerminalLauncher, hasChildProcesses, prepareCommand } from 'vs/workbench/contrib/debug/node/terminals'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { ExtHostConfiguration, ExtHostConfigProvider } from './extHostConfiguration'; +import { ExtHostConfiguration, ExtHostConfigProvider } from '../common/extHostConfiguration'; import { convertToVSCPaths, convertToDAPaths, isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 85ddfef924..b2efdf0ae1 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -16,15 +16,15 @@ import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; // {{SQL CARBON EDIT}} - Remove createApiFactory initializeExtensionApi, and IExtensionApiFactory imports //import { createApiFactory, IExtensionApiFactory, NodeModuleRequireInterceptor, VSCodeNodeModuleFactory } from 'vs/workbench/api/node/extHost.api.impl'; -import { NodeModuleRequireInterceptor, VSCodeNodeModuleFactory, KeytarNodeModuleFactory } from 'vs/workbench/api/node/extHost.api.impl'; +import { NodeModuleRequireInterceptor, KeytarNodeModuleFactory, OpenNodeModuleFactory } from 'vs/workbench/api/node/extHost.api.impl'; import { ExtHostExtensionServiceShape, IEnvironment, IInitData, IMainContext, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape, IStaticWorkspaceData } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; -import { ActivatedExtension, EmptyExtension, ExtensionActivatedByAPI, ExtensionActivatedByEvent, ExtensionActivationReason, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionContext, IExtensionMemento, IExtensionModule, HostExtension } from 'vs/workbench/api/node/extHostExtensionActivator'; -import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; -import { ExtHostStorage } from 'vs/workbench/api/node/extHostStorage'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; +import { ExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; +import { ActivatedExtension, EmptyExtension, ExtensionActivatedByAPI, ExtensionActivatedByEvent, ExtensionActivationReason, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionContext, IExtensionMemento, IExtensionModule, HostExtension } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; +import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; +import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { connectProxyResolver } from 'vs/workbench/services/extensions/node/proxyResolver'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; @@ -35,6 +35,7 @@ import { IWorkspace } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; import { withNullAsUndefined } from 'vs/base/common/types'; import { realpath } from 'vs/base/node/extpath'; +import { VSBuffer } from 'vs/base/common/buffer'; class ExtensionMemento implements IExtensionMemento { @@ -165,6 +166,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { private readonly _extHostContext: IMainContext; private readonly _extHostWorkspace: ExtHostWorkspace; private readonly _extHostConfiguration: ExtHostConfiguration; + private readonly _environment: IEnvironment; private readonly _extHostLogService: ExtHostLogService; private readonly _mainThreadWorkspaceProxy: MainThreadWorkspaceShape; @@ -190,6 +192,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { extHostContext: IMainContext, extHostWorkspace: ExtHostWorkspace, extHostConfiguration: ExtHostConfiguration, + environment: IEnvironment, extHostLogService: ExtHostLogService ) { this._nativeExit = nativeExit; @@ -197,6 +200,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { this._extHostContext = extHostContext; this._extHostWorkspace = extHostWorkspace; this._extHostConfiguration = extHostConfiguration; + this._environment = environment; this._extHostLogService = extHostLogService; this._mainThreadWorkspaceProxy = this._extHostContext.getProxy(MainContext.MainThreadWorkspace); @@ -246,12 +250,18 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { private async _initialize(): Promise { try { const configProvider = await this._extHostConfiguration.getConfigProvider(); - + const extensionPaths = await this.getExtensionPathIndex(); // {{SQL CARBON EDIT}} - disable VSCodeNodeModuleFactory and use older initializeExtensionApi - // const extensionPaths = await this.getExtensionPathIndex(); // NodeModuleRequireInterceptor.INSTANCE.register(new VSCodeNodeModuleFactory(this._extensionApiFactory, extensionPaths, this._registry, configProvider)); await initializeExtensionApi(this, this._extensionApiFactory, this._registry, configProvider); - NodeModuleRequireInterceptor.INSTANCE.register(new KeytarNodeModuleFactory(this._extHostContext.getProxy(MainContext.MainThreadKeytar))); + NodeModuleRequireInterceptor.INSTANCE.register(new KeytarNodeModuleFactory(this._extHostContext.getProxy(MainContext.MainThreadKeytar), this._environment)); + if (this._initData.remoteAuthority) { + NodeModuleRequireInterceptor.INSTANCE.register(new OpenNodeModuleFactory( + this._extHostContext.getProxy(MainContext.MainThreadWindow), + this._extHostContext.getProxy(MainContext.MainThreadTelemetry), + extensionPaths + )); + } // Do this when extension service exists, but extensions are not being activated yet. await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._extHostLogService, this._mainThreadTelemetryProxy); @@ -608,7 +618,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { } private _doHandleExtensionTests(): Promise { - const { extensionDevelopmentLocationURI, extensionTestsLocationURI } = this._initData.environment; + const { extensionDevelopmentLocationURI: extensionDevelopmentLocationURI, extensionTestsLocationURI } = this._initData.environment; if (!(extensionDevelopmentLocationURI && extensionTestsLocationURI && extensionTestsLocationURI.scheme === Schemas.file)) { return Promise.resolve(undefined); } @@ -759,13 +769,12 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { return n; } - public async $test_up(b: Buffer): Promise { - return b.length; + public async $test_up(b: VSBuffer): Promise { + return b.byteLength; } - public async $test_down(size: number): Promise { - const b = Buffer.alloc(size, Math.random() % 256); - return b; + public async $test_down(size: number): Promise { + return VSBuffer.wrap(Buffer.alloc(size, Math.random() % 256)); } } diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index ada8b93e4d..d176cac5e8 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -3,101 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainContext, MainThreadOutputServiceShape, IMainContext, ExtHostOutputServiceShape } from '../common/extHost.protocol'; +import { MainThreadOutputServiceShape } from '../common/extHost.protocol'; import * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender'; import { toLocalISOString } from 'vs/base/common/date'; -import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { dirExists, mkdirp } from 'vs/base/node/pfs'; +import { AbstractExtHostOutputChannel, IOutputChannelFactory, ExtHostPushOutputChannel } from 'vs/workbench/api/common/extHostOutput'; -abstract class AbstractExtHostOutputChannel extends Disposable implements vscode.OutputChannel { - - readonly _id: Promise; - private readonly _name: string; - protected readonly _proxy: MainThreadOutputServiceShape; - private _disposed: boolean; - private _offset: number; - - protected readonly _onDidAppend: Emitter = this._register(new Emitter()); - readonly onDidAppend: Event = this._onDidAppend.event; - - constructor(name: string, log: boolean, file: URI | undefined, proxy: MainThreadOutputServiceShape) { - super(); - - this._name = name; - this._proxy = proxy; - this._id = proxy.$register(this.name, log, file); - this._offset = 0; - } - - get name(): string { - return this._name; - } - - append(value: string): void { - this.validate(); - this._offset += value ? Buffer.from(value).byteLength : 0; - } - - update(): void { - this._id.then(id => this._proxy.$update(id)); - } - - appendLine(value: string): void { - this.validate(); - this.append(value + '\n'); - } - - clear(): void { - this.validate(); - const till = this._offset; - this._id.then(id => this._proxy.$clear(id, till)); - } - - show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { - this.validate(); - this._id.then(id => this._proxy.$reveal(id, !!(typeof columnOrPreserveFocus === 'boolean' ? columnOrPreserveFocus : preserveFocus))); - } - - hide(): void { - this.validate(); - this._id.then(id => this._proxy.$close(id)); - } - - protected validate(): void { - if (this._disposed) { - throw new Error('Channel has been closed'); - } - } - - dispose(): void { - super.dispose(); - - if (!this._disposed) { - this._id - .then(id => this._proxy.$dispose(id)) - .then(() => this._disposed = true); - } - } -} - -class ExtHostPushOutputChannel extends AbstractExtHostOutputChannel { - - constructor(name: string, proxy: MainThreadOutputServiceShape) { - super(name, false, undefined, proxy); - } - - append(value: string): void { - super.append(value); - this._id.then(id => this._proxy.$append(id, value)); - this._onDidAppend.fire(); - } -} - -class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { +export class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { private _appender: OutputAppender; @@ -128,95 +43,23 @@ class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel { } } -class ExtHostLogFileOutputChannel extends AbstractExtHostOutputChannel { +export const LogOutputChannelFactory = new class implements IOutputChannelFactory { - constructor(name: string, file: URI, proxy: MainThreadOutputServiceShape) { - super(name, true, file, proxy); - } + _namePool = 1; - append(value: string): void { - throw new Error('Not supported'); - } -} - -let namePool = 1; -async function createExtHostOutputChannel(name: string, outputDirPromise: Promise, proxy: MainThreadOutputServiceShape): Promise { - try { - const outputDir = await outputDirPromise; - const fileName = `${namePool++}-${name}`; - const file = URI.file(join(outputDir, `${fileName}.log`)); - const appender = new OutputAppender(fileName, file.fsPath); - return new ExtHostOutputChannelBackedByFile(name, appender, proxy); - } catch (error) { - // Do not crash if logger cannot be created - console.log(error); - return new ExtHostPushOutputChannel(name, proxy); - } -} - -export class ExtHostOutputService implements ExtHostOutputServiceShape { - - private readonly _outputDir: Promise; - private _proxy: MainThreadOutputServiceShape; - private _channels: Map = new Map(); - private _visibleChannelDisposable: IDisposable; - - constructor(logsLocation: URI, mainContext: IMainContext) { - const outputDirPath = join(logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); - this._outputDir = dirExists(outputDirPath).then(exists => exists ? exists : mkdirp(outputDirPath).then(() => true)).then(() => outputDirPath); - this._proxy = mainContext.getProxy(MainContext.MainThreadOutputService); - } - - $setVisibleChannel(channelId: string): void { - if (this._visibleChannelDisposable) { - this._visibleChannelDisposable = dispose(this._visibleChannelDisposable); - } - if (channelId) { - const channel = this._channels.get(channelId); - if (channel) { - this._visibleChannelDisposable = channel.onDidAppend(() => channel.update()); - } + async createOutputChannel(name: string, logsLocation: URI, proxy: MainThreadOutputServiceShape): Promise { + try { + const outputDirPath = join(logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); + const outputDir = await dirExists(outputDirPath).then(exists => exists ? exists : mkdirp(outputDirPath).then(() => true)).then(() => outputDirPath); + const fileName = `${this._namePool++}-${name}`; + const file = URI.file(join(outputDir, `${fileName}.log`)); + const appender = new OutputAppender(fileName, file.fsPath); + return new ExtHostOutputChannelBackedByFile(name, appender, proxy); + } catch (error) { + // Do not crash if logger cannot be created + console.log(error); + return new ExtHostPushOutputChannel(name, proxy); } } +}; - createOutputChannel(name: string): vscode.OutputChannel { - name = name.trim(); - if (!name) { - throw new Error('illegal argument `name`. must not be falsy'); - } else { - const extHostOutputChannel = createExtHostOutputChannel(name, this._outputDir, this._proxy); - extHostOutputChannel.then(channel => channel._id.then(id => this._channels.set(id, channel))); - return { - append(value: string): void { - extHostOutputChannel.then(channel => channel.append(value)); - }, - appendLine(value: string): void { - extHostOutputChannel.then(channel => channel.appendLine(value)); - }, - clear(): void { - extHostOutputChannel.then(channel => channel.clear()); - }, - show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { - extHostOutputChannel.then(channel => channel.show(columnOrPreserveFocus, preserveFocus)); - }, - hide(): void { - extHostOutputChannel.then(channel => channel.hide()); - }, - dispose(): void { - extHostOutputChannel.then(channel => channel.dispose()); - } - }; - } - } - - createOutputChannelFromLogFile(name: string, file: URI): vscode.OutputChannel { - name = name.trim(); - if (!name) { - throw new Error('illegal argument `name`. must not be falsy'); - } - if (!file) { - throw new Error('illegal argument `file`. must not be falsy'); - } - return new ExtHostLogFileOutputChannel(name, file, this._proxy); - } -} diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 08a7202688..a39e90bfea 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -14,8 +14,8 @@ import { win32 } from 'vs/base/node/processes'; import { MainContext, MainThreadTaskShape, ExtHostTaskShape, IMainContext } from 'vs/workbench/api/common/extHost.protocol'; -import * as types from 'vs/workbench/api/node/extHostTypes'; -import { ExtHostWorkspace, IExtHostWorkspaceProvider } from 'vs/workbench/api/node/extHostWorkspace'; +import * as types from 'vs/workbench/api/common/extHostTypes'; +import { ExtHostWorkspace, IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; import * as vscode from 'vscode'; import { TaskDefinitionDTO, TaskExecutionDTO, TaskPresentationOptionsDTO, @@ -26,8 +26,8 @@ import { } from '../common/shared/tasks'; // {{SQL CARBON EDIT}} // import { ExtHostVariableResolverService } from 'vs/workbench/api/node/extHostDebugService'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; -import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/node/extHostTerminalService'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 9236f8238d..7371b8e76e 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -11,7 +11,7 @@ import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { Event, Emitter } from 'vs/base/common/event'; import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext, ShellLaunchConfigDto } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; +import { ExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ILogService } from 'vs/platform/log/common/log'; import { EXT_HOST_CREATION_DELAY, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 8e204d0d3b..99dc626b6c 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -16,6 +16,7 @@ import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDE import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Schemas } from 'vs/base/common/network'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class OpenFileAction extends Action { @@ -289,14 +290,15 @@ export class DuplicateWorkspaceInNewWindowAction extends Action { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, @IWindowService private readonly windowService: IWindowService, - @IWorkspacesService private readonly workspacesService: IWorkspacesService + @IWorkspacesService private readonly workspacesService: IWorkspacesService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(id, label); } run(): Promise { const folders = this.workspaceContextService.getWorkspace().folders; - const remoteAuthority = this.windowService.getConfiguration().remoteAuthority; + const remoteAuthority = this.environmentService.configuration.remoteAuthority; return this.workspacesService.createUntitledWorkspace(folders, remoteAuthority).then(newWorkspace => { return this.workspaceEditingService.copyWorkspaceSettings(newWorkspace).then(() => { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 9c392ad5be..9d6a8cdee2 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -44,6 +44,12 @@ export abstract class Composite extends Component implements IComposite { return this._onDidFocus.event; } + protected fireOnDidFocus(): void { + if (this._onDidFocus) { + this._onDidFocus.fire(); + } + } + private _onDidBlur: Emitter; get onDidBlur(): Event { if (!this._onDidBlur) { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index e80fbe7919..92e9b8f71d 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -7,13 +7,13 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; -import { IWindowService, IWindowsConfiguration } from 'vs/platform/windows/common/windows'; +import { IWindowsConfiguration } from 'vs/platform/windows/common/windows'; import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext } from 'vs/workbench/common/editor'; -import { IsMacContext, IsLinuxContext, IsWindowsContext, HasMacNativeTabsContext, IsDevelopmentContext, SupportsWorkspacesContext, SupportsOpenFileFolderContext, WorkbenchStateContext, WorkspaceFolderCountContext, IsRemoteContext } from 'vs/workbench/common/contextkeys'; +import { IsMacContext, IsLinuxContext, IsWindowsContext, HasMacNativeTabsContext, IsDevelopmentContext, SupportsWorkspacesContext, WorkbenchStateContext, WorkspaceFolderCountContext, RemoteAuthorityContext } from 'vs/workbench/common/contextkeys'; import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; @@ -43,8 +43,7 @@ export class WorkbenchContextKeysHandler extends Disposable { @IContextKeyService private contextKeyService: IContextKeyService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IConfigurationService private configurationService: IConfigurationService, - @IEnvironmentService private environmentService: IEnvironmentService, - @IWindowService private windowService: IWindowService, + @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, @IEditorService private editorService: IEditorService, @IEditorGroupsService private editorGroupService: IEditorGroupsService, @IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService, @@ -88,7 +87,7 @@ export class WorkbenchContextKeysHandler extends Disposable { IsLinuxContext.bindTo(this.contextKeyService); IsWindowsContext.bindTo(this.contextKeyService); - IsRemoteContext.bindTo(this.contextKeyService).set(!!this.windowService.getConfiguration().remoteAuthority); + RemoteAuthorityContext.bindTo(this.contextKeyService).set(this.environmentService.configuration.remoteAuthority || ''); // macOS Native Tabs const windowConfig = this.configurationService.getValue(); @@ -99,7 +98,6 @@ export class WorkbenchContextKeysHandler extends Disposable { // File Pickers SupportsWorkspacesContext.bindTo(this.contextKeyService); - SupportsOpenFileFolderContext.bindTo(this.contextKeyService).set(!!this.windowService.getConfiguration().remoteAuthority); // Editors this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index c562637f54..22d63b3a97 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -16,7 +16,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { Schemas } from 'vs/base/common/network'; -import { FileKind, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; +import { FileKind, FILES_ASSOCIATIONS_CONFIG, IFileService } from 'vs/platform/files/common/files'; import { ITextModel } from 'vs/editor/common/model'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; @@ -93,7 +93,8 @@ export class ResourceLabels extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IModelService private readonly modelService: IModelService, @IDecorationsService private readonly decorationsService: IDecorationsService, - @IThemeService private readonly themeService: IThemeService + @IThemeService private readonly themeService: IThemeService, + @IFileService private readonly fileService: IFileService ) { super(); @@ -116,7 +117,7 @@ export class ResourceLabels extends Disposable { return; // we need the resource to compare } - if (e.model.uri.scheme === Schemas.file && e.oldModeId === PLAINTEXT_MODE_ID) { // todo@remote does this apply? + if (this.fileService.canHandleResource(e.model.uri) && e.oldModeId === PLAINTEXT_MODE_ID) { return; // ignore transitions in files from no mode to specific mode because this happens each time a model is created } @@ -202,9 +203,9 @@ export class ResourceLabel extends ResourceLabels { @IModelService modelService: IModelService, @IDecorationsService decorationsService: IDecorationsService, @IThemeService themeService: IThemeService, - @ILabelService labelService: ILabelService + @IFileService fileService: IFileService ) { - super(DEFAULT_LABELS_CONTAINER, instantiationService, extensionService, configurationService, modelService, decorationsService, themeService); + super(DEFAULT_LABELS_CONTAINER, instantiationService, extensionService, configurationService, modelService, decorationsService, themeService, fileService); this._label = this._register(this.create(container, options)); } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 25cdc8edad..f0de12726b 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -25,7 +25,7 @@ import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { LifecyclePhase, StartupKind, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IWindowService, IPath, MenuBarVisibility, getTitleBarStyle } from 'vs/platform/windows/common/windows'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +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'; import { Sizing, Direction, Grid, View } from 'vs/base/browser/ui/grid/grid'; @@ -89,7 +89,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private editorPartView: View; private statusBarPartView: View; - private environmentService: IEnvironmentService; + private environmentService: IWorkbenchEnvironmentService; private configurationService: IConfigurationService; private lifecycleService: ILifecycleService; private storageService: IStorageService; @@ -161,12 +161,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi protected initLayout(accessor: ServicesAccessor): void { // Services - this.environmentService = accessor.get(IEnvironmentService); + this.environmentService = accessor.get(IWorkbenchEnvironmentService); this.configurationService = accessor.get(IConfigurationService); this.lifecycleService = accessor.get(ILifecycleService); this.windowService = accessor.get(IWindowService); this.contextService = accessor.get(IWorkspaceContextService); this.storageService = accessor.get(IStorageService); + this.backupFileService = accessor.get(IBackupFileService); // Parts this.editorService = accessor.get(IEditorService); @@ -389,7 +390,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private resolveEditorsToOpen(): Promise | IResourceEditor[] { - const configuration = this.windowService.getConfiguration(); + const configuration = this.environmentService.configuration; const hasInitialFilesToOpen = this.hasInitialFilesToOpen(); // Only restore editors if we are not instructed to open files initially @@ -418,8 +419,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Empty workbench else if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY && this.configurationService.inspect('workbench.startupEditor').value === 'newUntitledFile') { - const isEmpty = this.editorGroupService.count === 1 && this.editorGroupService.activeGroup.count === 0; - if (!isEmpty) { + if (this.editorGroupService.willRestoreEditors) { return []; // do not open any empty untitled file if we restored editors from previous session } @@ -436,7 +436,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private hasInitialFilesToOpen(): boolean { - const configuration = this.windowService.getConfiguration(); + const configuration = this.environmentService.configuration; return !!( (configuration.filesToCreate && configuration.filesToCreate.length > 0) || diff --git a/src/vs/workbench/browser/nodeless.main.ts b/src/vs/workbench/browser/nodeless.main.ts index cdfe9e5423..f570de9f1e 100644 --- a/src/vs/workbench/browser/nodeless.main.ts +++ b/src/vs/workbench/browser/nodeless.main.ts @@ -8,8 +8,18 @@ import { domContentLoaded, addDisposableListener, EventType } from 'vs/base/brow import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService } from 'vs/platform/log/common/log'; import { Disposable } from 'vs/base/common/lifecycle'; -import { SimpleLogService } from 'vs/workbench/browser/nodeless.simpleservices'; +import { SimpleLogService, SimpleProductService, SimpleWorkbenchEnvironmentService } from 'vs/workbench/browser/nodeless.simpleservices'; import { Workbench } from 'vs/workbench/browser/workbench'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME, RemoteExtensionsFileSystemProvider } from 'vs/platform/remote/common/remoteAgentFileSystemChannel'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IProductService } from 'vs/platform/product/common/product'; +import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentServiceImpl'; +import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService3 } from 'vs/workbench/services/files2/browser/fileService2'; class CodeRendererMain extends Disposable { @@ -42,14 +52,41 @@ class CodeRendererMain extends Disposable { private initServices(): { serviceCollection: ServiceCollection, logService: ILogService } { const serviceCollection = new ServiceCollection(); - const logService = new SimpleLogService(); - serviceCollection.set(ILogService, logService); - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // NOTE: DO NOT ADD ANY OTHER SERVICE INTO THE COLLECTION HERE. // CONTRIBUTE IT VIA WORKBENCH.MAIN.TS AND registerSingleton(). // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // Log + const logService = new SimpleLogService(); + serviceCollection.set(ILogService, logService); + + // Environment + const environmentService = new SimpleWorkbenchEnvironmentService(); + serviceCollection.set(IWorkbenchEnvironmentService, environmentService); + + // Product + const productService = new SimpleProductService(); + serviceCollection.set(IProductService, productService); + + // Remote + const remoteAuthorityResolverService = new RemoteAuthorityResolverService(); + serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); + + const remoteAgentService = this._register(new RemoteAgentService(environmentService, productService, remoteAuthorityResolverService)); + serviceCollection.set(IRemoteAgentService, remoteAgentService); + + // Files + const fileService = this._register(new FileService3(logService)); + serviceCollection.set(IFileService, fileService); + + const connection = remoteAgentService.getConnection(); + if (connection) { + const channel = connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); + const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); + fileService.registerProvider('vscode-remote', remoteFileSystemProvider); + } + return { serviceCollection, logService }; } } diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts index 982cf20263..3896079251 100644 --- a/src/vs/workbench/browser/nodeless.simpleservices.ts +++ b/src/vs/workbench/browser/nodeless.simpleservices.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { ITextSnapshot, IFileStat, IContent, IFileService, IResourceEncodings, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IStreamContent, IUpdateContentOptions, snapshotToString, ICreateFileOptions, IResourceEncoding, IFileStatWithMetadata, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { ITextSnapshot } from 'vs/platform/files/common/files'; import { ITextBufferFactory } from 'vs/editor/common/model'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { keys, ResourceMap } from 'vs/base/common/map'; @@ -17,7 +17,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { SimpleConfigurationService as StandaloneEditorConfigurationService, StandaloneKeybindingService, SimpleResourcePropertiesService } from 'vs/editor/standalone/browser/simpleServices'; import { IDownloadService } from 'vs/platform/download/common/download'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IEnvironmentService, IExtensionHostDebugParams, IDebugParams } from 'vs/platform/environment/common/environment'; +import { IExtensionHostDebugParams, IDebugParams } from 'vs/platform/environment/common/environment'; import { IExtensionGalleryService, IQueryOptions, IGalleryExtension, InstallOperation, StatisticType, ITranslation, IGalleryExtensionVersion, IExtensionIdentifier, IReportedExtension, IExtensionManagementService, ILocalExtension, IGalleryMetadata, IExtensionTipsService, ExtensionRecommendationReason, IExtensionRecommendation, IExtensionEnablementService, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IPager } from 'vs/base/common/paging'; import { IExtensionManifest, ExtensionType, ExtensionIdentifier, IExtension } from 'vs/platform/extensions/common/extensions'; @@ -32,9 +32,7 @@ import { ILogService, LogLevel, ConsoleLogService } from 'vs/platform/log/common import { ShutdownReason, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { IProductService } from 'vs/platform/product/common/product'; -import { IRemoteAuthorityResolverService, ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { joinPath, isEqualOrParent, isEqual, dirname } from 'vs/base/common/resources'; -import { basename } from 'vs/base/common/path'; +import { isEqualOrParent, isEqual } from 'vs/base/common/resources'; import { ISearchService, ITextQueryProps, ISearchProgressItem, ISearchComplete, IFileQueryProps, SearchProviderType, ISearchResultProvider, ITextQuery, IFileMatch, QueryType, FileMatch, pathIncludedInQuery } from 'vs/workbench/services/search/common/search'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -48,7 +46,7 @@ import { ITextMateService, IGrammar as ITextMategrammar } from 'vs/workbench/ser import { LanguageId, TokenizationRegistry } from 'vs/editor/common/modes'; import { IUpdateService, State } from 'vs/platform/update/common/update'; import { IWindowConfiguration, IPath, IPathsToWaitFor, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IURIToOpen, IMessageBoxResult, IWindowsService, IOpenSettings } from 'vs/platform/windows/common/windows'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { IProcessEnvironment } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData, isSingleFolderWorkspaceIdentifier, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { ExportData } from 'vs/base/common/performance'; import { IRecentlyOpened, IRecent } from 'vs/platform/history/common/history'; @@ -59,10 +57,14 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/resour import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Color, RGBA } from 'vs/base/common/color'; -import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { IRemoteAgentService, IRemoteAgentConnection } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -export const workspaceResource = URI.file(isWindows ? 'C:\\simpleWorkspace' : '/simpleWorkspace'); +export const workspaceResource = URI.from({ + scheme: 'vscode-remote', + authority: document.location.host, + path: (self).USER_HOME_DIR || '/' +}); //#region Backup File @@ -225,7 +227,8 @@ registerSingleton(IDownloadService, SimpleDownloadService, true); //#region Environment -export class SimpleEnvironmentService implements IEnvironmentService { +export class SimpleWorkbenchEnvironmentService implements IWorkbenchEnvironmentService { + configuration: IWindowConfiguration = new SimpleWindowConfiguration(); untitledWorkspacesHome: URI; extensionTestsLocationURI?: URI; _serviceBrand: any; @@ -240,6 +243,8 @@ export class SimpleEnvironmentService implements IEnvironmentService { appSettingsHome: string = '/nodeless/settings'; appSettingsPath: string = '/nodeless/settings/settings.json'; appKeybindingsPath: string = '/nodeless/settings/keybindings.json'; + machineSettingsHome: string; + machineSettingsPath: string; settingsSearchBuildId?: number; settingsSearchUrl?: string; globalStorageHome: string; @@ -251,7 +256,7 @@ export class SimpleEnvironmentService implements IEnvironmentService { disableExtensions: boolean | string[]; builtinExtensionsPath: string; extensionsPath: string; - extensionDevelopmentLocationURI?: URI | URI[]; + extensionDevelopmentLocationURI?: URI[]; extensionTestsPath?: string; debugExtensionHost: IExtensionHostDebugParams; debugSearch: IDebugParams; @@ -275,7 +280,6 @@ export class SimpleEnvironmentService implements IEnvironmentService { driverVerbose: boolean; } -registerSingleton(IEnvironmentService, SimpleEnvironmentService); //#endregion @@ -667,425 +671,6 @@ export class SimpleProductService implements IProductService { enableTelemetry: boolean = false; } -registerSingleton(IProductService, SimpleProductService, true); - -//#endregion - -//#region Remote Agent - -export class SimpleRemoteAgentService implements IRemoteAgentService { - - _serviceBrand: any; - - getConnection(): IRemoteAgentConnection | null { - return null; - } - - getEnvironment(): Promise { - return Promise.resolve(null); - } -} - -registerSingleton(IRemoteAgentService, SimpleRemoteAgentService); - -//#endregion - -//#region Remote Authority Resolver - -export class SimpleRemoteAuthorityResolverService implements IRemoteAuthorityResolverService { - - _serviceBrand: any; - - resolveAuthority(authority: string): Promise { - // @ts-ignore - return Promise.resolve(undefined); - } - - clearResolvedAuthority(authority: string): void { } - - setResolvedAuthority(resolvedAuthority: ResolvedAuthority): void { } - - setResolvedAuthorityError(authority: string, err: any): void { } -} - -registerSingleton(IRemoteAuthorityResolverService, SimpleRemoteAuthorityResolverService, true); - -//#endregion - -//#region File Servie - -const fileMap: ResourceMap = new ResourceMap(); -const contentMap: ResourceMap = new ResourceMap(); -initFakeFileSystem(); - -export class SimpleRemoteFileService implements IFileService { - - _serviceBrand: any; - - encoding: IResourceEncodings; - - readonly onFileChanges = Event.None; - readonly onAfterOperation = Event.None; - readonly onDidChangeFileSystemProviderRegistrations = Event.None; - readonly onWillActivateFileSystemProvider = Event.None; - readonly onError = Event.None; - - resolve(resource: URI, options?: IResolveFileOptions): Promise { - // @ts-ignore - return Promise.resolve(fileMap.get(resource)); - } - - resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise { - return Promise.all(toResolve.map(resourceAndOption => this.resolve(resourceAndOption.resource, resourceAndOption.options))).then(stats => stats.map(stat => ({ stat, success: true }))); - } - - exists(resource: URI): Promise { - return Promise.resolve(fileMap.has(resource)); - } - - resolveContent(resource: URI, _options?: IResolveContentOptions): Promise { - // @ts-ignore - return Promise.resolve(contentMap.get(resource)); - } - - resolveStreamContent(resource: URI, _options?: IResolveContentOptions): Promise { - return Promise.resolve(contentMap.get(resource)).then(content => { - return { - // @ts-ignore - resource: content.resource, - value: { - on: (event: string, callback: Function): void => { - if (event === 'data') { - // @ts-ignore - callback(content.value); - } - - if (event === 'end') { - callback(); - } - } - }, - // @ts-ignore - etag: content.etag, - // @ts-ignore - encoding: content.encoding, - // @ts-ignore - mtime: content.mtime, - // @ts-ignore - name: content.name, - // @ts-ignore - size: content.size - }; - }); - } - - updateContent(resource: URI, value: string | ITextSnapshot, _options?: IUpdateContentOptions): Promise { - // @ts-ignore - return Promise.resolve(fileMap.get(resource)).then(file => { - const content = contentMap.get(resource); - - if (typeof value === 'string') { - // @ts-ignore - content.value = value; - } else { - // @ts-ignore - content.value = snapshotToString(value); - } - - return file; - }); - } - - move(_source: URI, _target: URI, _overwrite?: boolean): Promise { return Promise.resolve(null!); } - - copy(_source: URI, _target: URI, _overwrite?: boolean): Promise { - const parent = fileMap.get(dirname(_target)); - if (!parent) { - return Promise.resolve(undefined); - } - - return this.resolveContent(_source).then(content => { - return Promise.resolve(createFile(parent, basename(_target.path), content.value)); - }); - } - - createFile(_resource: URI, _content?: string, _options?: ICreateFileOptions): Promise { - const parent = fileMap.get(dirname(_resource)); - if (!parent) { - return Promise.reject(new Error(`Unable to create file in ${dirname(_resource).path}`)); - } - - return Promise.resolve(createFile(parent, basename(_resource.path))); - } - - createFolder(_resource: URI): Promise { - const parent = fileMap.get(dirname(_resource)); - if (!parent) { - return Promise.reject(new Error(`Unable to create folder in ${dirname(_resource).path}`)); - } - - return Promise.resolve(createFolder(parent, basename(_resource.path))); - } - - registerProvider() { return { dispose() { } }; } - - activateProvider(_scheme: string): Promise { return Promise.resolve(undefined); } - - canHandleResource(resource: URI): boolean { return resource.scheme === 'file'; } - - hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean { return false; } - - del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean }): Promise { return Promise.resolve(); } - - watch(_resource: URI): IDisposable { return Disposable.None; } - - getWriteEncoding(_resource: URI): IResourceEncoding { return { encoding: 'utf8', hasBOM: false }; } - - dispose(): void { } -} - -function createFile(parent: IFileStat, name: string, content: string = ''): IFileStatWithMetadata { - const file: IFileStatWithMetadata = { - resource: joinPath(parent.resource, name), - etag: Date.now().toString(), - mtime: Date.now(), - isDirectory: false, - name, - size: -1 - }; - - // @ts-ignore - parent.children.push(file); - - fileMap.set(file.resource, file); - - contentMap.set(file.resource, { - resource: joinPath(parent.resource, name), - etag: Date.now().toString(), - mtime: Date.now(), - value: content, - encoding: 'utf8', - name - } as IContent); - - return file; -} - -function createFolder(parent: IFileStat, name: string): IFileStatWithMetadata { - const folder: IFileStatWithMetadata = { - resource: joinPath(parent.resource, name), - etag: Date.now().toString(), - mtime: Date.now(), - isDirectory: true, - name, - size: 0, - children: [] - }; - - // @ts-ignore - parent.children.push(folder); - - fileMap.set(folder.resource, folder); - - return folder; -} - -function initFakeFileSystem(): void { - - const root: IFileStat = { - resource: workspaceResource, - etag: Date.now().toString(), - mtime: Date.now(), - isDirectory: true, - name: basename(workspaceResource.fsPath), - children: [], - size: 0 - }; - - fileMap.set(root.resource, root); - - createFile(root, '.gitignore', `out -node_modules -.vscode-test/ -*.vsix -`); - createFile(root, '.vscodeignore', `.vscode/** -.vscode-test/** -out/test/** -src/** -.gitignore -vsc-extension-quickstart.md -**/tsconfig.json -**/tslint.json -**/*.map -**/*.ts`); - createFile(root, 'CHANGELOG.md', `# Change Log -All notable changes to the "test-ts" extension will be documented in this file. - -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. - -## [Unreleased] -- Initial release`); - createFile(root, 'package.json', `{ - "name": "test-ts", - "displayName": "test-ts", - "description": "", - "version": "0.0.1", - "engines": { - "vscode": "^1.31.0" - }, - "categories": [ - "Other" - ], - "activationEvents": [ - "onCommand:extension.helloWorld" - ], - "main": "./out/extension.js", - "contributes": { - "commands": [ - { - "command": "extension.helloWorld", - "title": "Hello World" - } - ] - }, - "scripts": { - "vscode:prepublish": "npm run compile", - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", - "postinstall": "node ./node_modules/vscode/bin/install", - "test": "npm run compile && node ./node_modules/vscode/bin/test" - }, - "devDependencies": { - "typescript": "^3.3.1", - "vscode": "^1.1.28", - "tslint": "^5.12.1", - "@types/node": "^8.10.25", - "@types/mocha": "^2.2.42" - } -} -`); - createFile(root, 'tsconfig.json', `{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "outDir": "out", - "lib": [ - "es6" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - }, - "exclude": [ - "node_modules", - ".vscode-test" - ] -} -`); - createFile(root, 'tslint.json', `{ - "rules": { - "no-string-throw": true, - "no-unused-expression": true, - "no-duplicate-variable": true, - "curly": true, - "class-name": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": true - }, - "defaultSeverity": "warning" -} -`); - - const src = createFolder(root, 'src'); - createFile(src, 'extension.ts', `// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import * as vscode from 'vscode'; - -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { - - // Use the console to output diagnostic information (console.log) and errors (console.error) - // This line of code will only be executed once when your extension is activated - console.log('Congratulations, your extension "test-ts" is now active!'); - - // The command has been defined in the package.json file - // Now provide the implementation of the command with registerCommand - // The commandId parameter must match the command field in package.json - let disposable = vscode.commands.registerCommand('extension.helloWorld', () => { - // The code you place here will be executed every time your command is executed - - // Display a message box to the user - vscode.window.showInformationMessage('Hello World!'); - }); - - context.subscriptions.push(disposable); -} - -// this method is called when your extension is deactivated -export function deactivate() {} -`); - - const test = createFolder(src, 'test'); - - createFile(test, 'extension.test.ts', `// -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. -// - -// The module 'assert' provides assertion methods from node -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -// import * as vscode from 'vscode'; -// import * as myExtension from '../extension'; - -// Defines a Mocha test suite to group tests of similar kind together -suite("Extension Tests", function () { - - // Defines a Mocha unit test - test("Something 1", function() { - assert.equal(-1, [1, 2, 3].indexOf(5)); - assert.equal(-1, [1, 2, 3].indexOf(0)); - }); -});`); - - createFile(test, 'index.ts', `// -// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING -// -// This file is providing the test runner to use when running extension tests. -// By default the test runner in use is Mocha based. -// -// You can provide your own test runner if you want to override it by exporting -// a function run(testRoot: string, clb: (error:Error) => void) that the extension -// host can call to run the tests. The test runner is expected to use console.log -// to report the results back to the caller. When the tests are finished, return -// a possible error to the callback or null if none. - -import * as testRunner from 'vscode/lib/testrunner'; - -// You can directly control Mocha options by configuring the test runner below -// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options -// for more info -testRunner.configure({ - ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - useColors: true // colored output from test results -}); - -module.exports = testRunner;`); -} - -registerSingleton(IFileService, SimpleRemoteFileService); - //#endregion //#region Request @@ -1382,7 +967,7 @@ export class SimpleWindowService implements IWindowService { hasFocus = true; - private configuration: IWindowConfiguration = new SimpleWindowConfiguration(); + readonly windowId = 0; isFocused(): Promise { return Promise.resolve(false); @@ -1392,14 +977,6 @@ export class SimpleWindowService implements IWindowService { return Promise.resolve(false); } - getConfiguration(): IWindowConfiguration { - return this.configuration; - } - - getCurrentWindowId(): number { - return 0; - } - pickFileFolderAndOpen(_options: INativeOpenDialogOptions): Promise { return Promise.resolve(); } @@ -1788,7 +1365,7 @@ export class SimpleWorkspaceService implements IWorkspaceContextService { constructor() { this.workspace = new Workspace( workspaceResource.toString(), - toWorkspaceFolders([{ path: workspaceResource.fsPath }]) + toWorkspaceFolders([{ uri: workspaceResource.toString() }]) ); } @@ -1861,3 +1438,16 @@ export class SimpleWorkspacesService implements IWorkspacesService { registerSingleton(IWorkspacesService, SimpleWorkspacesService); //#endregion + +//#region remote + +class SimpleTunnelService implements ITunnelService { + _serviceBrand: any; + openTunnel(remotePort: number) { + return undefined; + } +} + +registerSingleton(ITunnelService, SimpleTunnelService); + +//#endregion \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index aa4f9e3df6..e277e6a7ec 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -51,6 +51,8 @@ import { Schemas } from 'vs/base/common/network'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { ZoomStatusbarItem } from 'vs/workbench/browser/parts/editor/resourceViewer'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { toLocalResource } from 'vs/base/common/resources'; // Register String Editor Registry.as(EditorExtensions.Editors).registerEditor( @@ -111,7 +113,8 @@ interface ISerializedUntitledEditorInput { class UntitledEditorInputFactory implements IEditorInputFactory { constructor( - @ITextFileService private readonly textFileService: ITextFileService + @ITextFileService private readonly textFileService: ITextFileService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { } serialize(editorInput: EditorInput): string | undefined { @@ -128,7 +131,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory { let resource = untitledEditorInput.getResource(); if (untitledEditorInput.hasAssociatedFilePath) { - resource = resource.with({ scheme: Schemas.file }); // untitled with associated file path use the file schema + resource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); // untitled with associated file path use the local schema } const serialized: ISerializedUntitledEditorInput = { @@ -145,7 +148,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory { return instantiationService.invokeFunction(accessor => { const deserialized: ISerializedUntitledEditorInput = JSON.parse(serializedEditorInput); const resource = !!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource); - const filePath = resource.scheme === Schemas.file ? resource.fsPath : undefined; + const filePath = resource.scheme === Schemas.untitled ? undefined : resource.scheme === Schemas.file ? resource.fsPath : resource.path; const language = deserialized.modeId; const encoding = deserialized.encoding; diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index c30c9b54de..4b772bc084 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -231,6 +231,10 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro return this._whenRestored; } + get willRestoreEditors(): boolean { + return !!this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY]; + } + getGroups(order = GroupsOrder.CREATION_TIME): IEditorGroupView[] { switch (order) { case GroupsOrder.CREATION_TIME: diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index e7c1e32d8d..98c250d6a1 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, INotificationViewItem } from 'vs/workbench/common/notifications'; -import { IStatusbarService, StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IStatusbarService, StatusbarAlignment, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; +import { Disposable } from 'vs/base/common/lifecycle'; import { HIDE_NOTIFICATIONS_CENTER, SHOW_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { localize } from 'vs/nls'; export class NotificationsStatus extends Disposable { - private statusItem: IDisposable; + private statusItem: IStatusbarEntryAccessor; private isNotificationsCenterVisible: boolean; private _counter: Set; @@ -64,19 +64,18 @@ export class NotificationsStatus extends Disposable { } private updateNotificationsStatusItem(): void { - - // Dispose old first - if (this.statusItem) { - this.statusItem.dispose(); - } - - // Create new - this.statusItem = this.statusbarService.addEntry({ + const statusProperties: IStatusbarEntry = { text: this.count === 0 ? '$(bell)' : `$(bell) ${this.count}`, command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, tooltip: this.getTooltip(), showBeak: this.isNotificationsCenterVisible - }, StatusbarAlignment.RIGHT, -1000 /* towards the far end of the right hand side */); + }; + + if (!this.statusItem) { + this.statusItem = this.statusbarService.addEntry(statusProperties, StatusbarAlignment.RIGHT, -1000 /* towards the far end of the right hand side */); + } else { + this.statusItem.update(statusProperties); + } } private getTooltip(): string { @@ -98,12 +97,4 @@ export class NotificationsStatus extends Disposable { return localize('notifications', "{0} New Notifications", this.count); } - - dispose() { - super.dispose(); - - if (this.statusItem) { - this.statusItem.dispose(); - } - } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 4af7b828af..9a7130125d 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -49,16 +49,27 @@ .monaco-workbench .part.statusbar > .statusbar-item.right + .statusbar-item.left { padding-left: 7px; } -.monaco-workbench .part.statusbar > .statusbar-item.has-background-color.left:first-child, -.monaco-workbench .part.statusbar > .statusbar-item.right + .statusbar-item.has-background-color.left { - padding-right: 7px; /* expand padding if background color is configured for the status bar entry to make it look centered properly */ -} + /* adding padding to the most right status bar item */ .monaco-workbench .part.statusbar > .statusbar-item.right:first-child { padding-right: 7px; } + +/* tweak appearance for items with background to improve hover feedback */ +.monaco-workbench .part.statusbar > .statusbar-item.has-background-color.left:first-child, +.monaco-workbench .part.statusbar > .statusbar-item.right + .statusbar-item.has-background-color.left, .monaco-workbench .part.statusbar > .statusbar-item.has-background-color.right:first-child { - padding-left: 7px; /* expand padding if background color is configured for the status bar entry to make it look centered properly */ + padding-right: 0; + padding-left: 0; +} + +.monaco-workbench .part.statusbar > .statusbar-item.has-background-color.left > :first-child, +.monaco-workbench .part.statusbar > .statusbar-item.has-background-color.right > :first-child +{ + margin-right: 0; + margin-left: 0; + padding-left: 10px; + padding-right: 10px; } .monaco-workbench .part.statusbar > .statusbar-item a { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 522491d0bf..53559bb5b0 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -6,16 +6,16 @@ import 'vs/css!./media/statusbarpart'; import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { dispose, IDisposable, toDisposable, combinedDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Part } from 'vs/workbench/browser/part'; -import { IStatusbarRegistry, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IStatusbarRegistry, Extensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { StatusbarAlignment, IStatusbarService, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Action } from 'vs/base/common/actions'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector, ThemeColor } from 'vs/platform/theme/common/themeService'; @@ -24,11 +24,14 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; import { Color } from 'vs/base/common/color'; -import { addClass, EventHelper, createStyleSheet, addDisposableListener } from 'vs/base/browser/dom'; +import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, clearNode, removeClass } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { coalesce } from 'vs/base/common/arrays'; + +interface PendingEntry { entry: IStatusbarEntry; alignment: StatusbarAlignment; priority: number; accessor?: IStatusbarEntryAccessor; } export class StatusbarPart extends Part implements IStatusbarService { @@ -46,10 +49,10 @@ export class StatusbarPart extends Part implements IStatusbarService { //#endregion - private statusMsgDispose: IDisposable; + private statusMessageDispose: IDisposable; private styleElement: HTMLStyleElement; - private pendingEntries: { entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number, disposable: IDisposable }[] = []; + private pendingEntries: PendingEntry[] = []; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -67,24 +70,38 @@ export class StatusbarPart extends Part implements IStatusbarService { this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); } - addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number = 0): IDisposable { + addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number = 0): IStatusbarEntryAccessor { // As long as we have not been created into a container yet, record all entries // that are pending so that they can get created at a later point if (!this.element) { - const pendingEntry = { entry, alignment, priority, disposable: Disposable.None }; + const pendingEntry: PendingEntry = { + entry, alignment, priority + }; this.pendingEntries.push(pendingEntry); - return toDisposable(() => { - this.pendingEntries = this.pendingEntries.filter(e => e !== pendingEntry); - pendingEntry.disposable.dispose(); - }); + const accessor: IStatusbarEntryAccessor = { + update: (entry: IStatusbarEntry) => { + if (pendingEntry.accessor) { + pendingEntry.accessor.update(entry); + } else { + pendingEntry.entry = entry; + } + }, + dispose: () => { + if (pendingEntry.accessor) { + pendingEntry.accessor.dispose(); + } else { + this.pendingEntries = this.pendingEntries.filter(entry => entry !== pendingEntry); + } + } + }; + return accessor; } // Render entry in status bar - const el = this.doCreateStatusItem(alignment, priority, entry.showBeak ? 'has-beak' : undefined); - const item = this.instantiationService.createInstance(StatusBarEntryItem, entry); - const toDispose = item.render(el); + const el = this.doCreateStatusItem(alignment, priority, ...coalesce(['statusbar-entry', entry.showBeak ? 'has-beak' : undefined])); + const item = this.instantiationService.createInstance(StatusBarEntryItem, el, entry); // Insert according to priority const container = this.element; @@ -106,13 +123,24 @@ export class StatusbarPart extends Part implements IStatusbarService { container.appendChild(el); } - return toDisposable(() => { - el.remove(); + return { + update: entry => { - if (toDispose) { - toDispose.dispose(); + // Update beak + if (entry.showBeak) { + addClass(el, 'has-beak'); + } else { + removeClass(el, 'has-beak'); + } + + // Update entry + item.update(entry); + }, + dispose: () => { + el.remove(); + dispose(item); } - }); + }; } private getEntries(alignment: StatusbarAlignment): HTMLElement[] { @@ -164,7 +192,7 @@ export class StatusbarPart extends Part implements IStatusbarService { while (this.pendingEntries.length) { const entry = this.pendingEntries.shift(); if (entry) { - entry.disposable = this.addEntry(entry.entry, entry.alignment, entry.priority); + entry.accessor = this.addEntry(entry.entry, entry.alignment, entry.priority); } } @@ -195,11 +223,11 @@ export class StatusbarPart extends Part implements IStatusbarService { this.styleElement.innerHTML = `.monaco-workbench .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor}; }`; } - private doCreateStatusItem(alignment: StatusbarAlignment, priority: number = 0, extraClass?: string): HTMLElement { + private doCreateStatusItem(alignment: StatusbarAlignment, priority: number = 0, ...extraClasses: string[]): HTMLElement { const el = document.createElement('div'); addClass(el, 'statusbar-item'); - if (extraClass) { - addClass(el, extraClass); + if (extraClasses) { + addClasses(el, ...extraClasses); } if (alignment === StatusbarAlignment.RIGHT) { @@ -215,20 +243,20 @@ export class StatusbarPart extends Part implements IStatusbarService { } setStatusMessage(message: string, autoDisposeAfter: number = -1, delayBy: number = 0): IDisposable { - if (this.statusMsgDispose) { - this.statusMsgDispose.dispose(); // dismiss any previous - } + + // Dismiss any previous + dispose(this.statusMessageDispose); // Create new - let statusDispose: IDisposable; + let statusMessageEntry: IStatusbarEntryAccessor; let showHandle: any = setTimeout(() => { - statusDispose = this.addEntry({ text: message }, StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */); + statusMessageEntry = this.addEntry({ text: message }, StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */); showHandle = null; }, delayBy); let hideHandle: any; // Dispose function takes care of timeouts and actual entry - const dispose = { + const statusMessageDispose = { dispose: () => { if (showHandle) { clearTimeout(showHandle); @@ -238,18 +266,18 @@ export class StatusbarPart extends Part implements IStatusbarService { clearTimeout(hideHandle); } - if (statusDispose) { - statusDispose.dispose(); + if (statusMessageEntry) { + statusMessageEntry.dispose(); } } }; - this.statusMsgDispose = dispose; + this.statusMessageDispose = statusMessageDispose; if (typeof autoDisposeAfter === 'number' && autoDisposeAfter > 0) { - hideHandle = setTimeout(() => dispose.dispose(), autoDisposeAfter); + hideHandle = setTimeout(() => statusMessageDispose.dispose(), autoDisposeAfter); } - return dispose; + return statusMessageDispose; } layout(width: number, height: number): void { @@ -264,10 +292,12 @@ export class StatusbarPart extends Part implements IStatusbarService { } let manageExtensionAction: ManageExtensionAction; -class StatusBarEntryItem implements IStatusbarItem { +class StatusBarEntryItem extends Disposable { + private entryDisposables: IDisposable[] = []; constructor( - private entry: IStatusbarEntry, + private container: HTMLElement, + entry: IStatusbarEntry, @ICommandService private readonly commandService: ICommandService, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, @@ -276,78 +306,80 @@ class StatusBarEntryItem implements IStatusbarItem { @IEditorService private readonly editorService: IEditorService, @IThemeService private readonly themeService: IThemeService ) { - this.entry = entry; + super(); if (!manageExtensionAction) { manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); } + + this.render(entry); } - render(el: HTMLElement): IDisposable { - let toDispose: IDisposable[] = []; - addClass(el, 'statusbar-entry'); + update(entry: IStatusbarEntry): void { + clearNode(this.container); + this.entryDisposables = dispose(this.entryDisposables); + + this.render(entry); + } + + private render(entry: IStatusbarEntry): void { // Text Container let textContainer: HTMLElement; - if (this.entry.command) { + if (entry.command) { textContainer = document.createElement('a'); - toDispose.push(addDisposableListener(textContainer, 'click', () => this.executeCommand(this.entry.command!, this.entry.arguments))); + this.entryDisposables.push((addDisposableListener(textContainer, 'click', () => this.executeCommand(entry.command!, entry.arguments)))); } else { textContainer = document.createElement('span'); } // Label - new OcticonLabel(textContainer).text = this.entry.text; + new OcticonLabel(textContainer).text = entry.text; // Tooltip - if (this.entry.tooltip) { - textContainer.title = this.entry.tooltip; + if (entry.tooltip) { + textContainer.title = entry.tooltip; } // Color (only applies to text container) - toDispose.push(this.applyColor(textContainer, this.entry.color)); + this.applyColor(textContainer, entry.color); // Background Color (applies to parent element to fully fill container) - if (this.entry.backgroundColor) { - toDispose.push(this.applyColor(el, this.entry.backgroundColor, true)); - addClass(el, 'has-background-color'); + if (entry.backgroundColor) { + this.applyColor(this.container, entry.backgroundColor, true); + addClass(this.container, 'has-background-color'); } // Context Menu - if (this.entry.extensionId) { - toDispose.push(addDisposableListener(textContainer, 'contextmenu', e => { + if (entry.extensionId) { + this.entryDisposables.push((addDisposableListener(textContainer, 'contextmenu', e => { EventHelper.stop(e, true); this.contextMenuService.showContextMenu({ - getAnchor: () => el, - getActionsContext: () => this.entry.extensionId!.value, + getAnchor: () => this.container, + getActionsContext: () => entry.extensionId!.value, getActions: () => [manageExtensionAction] }); - })); + }))); } - el.appendChild(textContainer); - - return toDisposable(() => toDispose = dispose(toDispose)); + this.container.appendChild(textContainer); } - private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): IDisposable { - const disposable: IDisposable[] = []; - + private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): void { if (color) { if (isThemeColor(color)) { const colorId = color.id; color = (this.themeService.getTheme().getColor(colorId) || Color.transparent).toString(); - disposable.push(this.themeService.onThemeChange(theme => { + this.entryDisposables.push(((this.themeService.onThemeChange(theme => { const colorValue = (theme.getColor(colorId) || Color.transparent).toString(); isBackground ? container.style.backgroundColor = colorValue : container.style.color = colorValue; - })); + })))); } + isBackground ? container.style.backgroundColor = color : container.style.color = color; } - - return combinedDisposable(disposable); } private executeCommand(id: string, args?: unknown[]) { @@ -368,6 +400,12 @@ class StatusBarEntryItem implements IStatusbarItem { this.telemetryService.publicLog('workbenchActionExecuted', { id, from: 'status bar' }); this.commandService.executeCommand(id, ...args).then(undefined, err => this.notificationService.error(toErrorMessage(err))); } + + dispose(): void { + super.dispose(); + + this.entryDisposables = dispose(this.entryDisposables); + } } class ManageExtensionAction extends Action { diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 9c42b1eac4..453dc6c75c 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -312,7 +312,7 @@ export class MenubarControl extends Disposable { // Send menus to main process to be rendered by Electron const menubarData = { menus: {}, keybindings: {} }; if (this.getMenubarMenus(menubarData)) { - this.menubarService.updateMenubar(this.windowService.getCurrentWindowId(), menubarData); + this.menubarService.updateMenubar(this.windowService.windowId, menubarData); } } } @@ -440,7 +440,7 @@ export class MenubarControl extends Disposable { return null; case StateType.Idle: - const windowId = this.windowService.getCurrentWindowId(); + const windowId = this.windowService.windowId; return new Action('update.check', nls.localize({ key: 'checkForUpdates', comment: ['&& denotes a mnemonic'] }, "Check for &&Updates..."), undefined, true, () => this.updateService.checkForUpdates({ windowId })); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 9331b78df7..f869dcce7d 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -18,7 +18,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { EditorInput, toResource, Verbosity, SideBySideEditor } from 'vs/workbench/common/editor'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER } from 'vs/workbench/common/theme'; @@ -85,7 +85,7 @@ export class TitlebarPart extends Part implements ITitleService { @IConfigurationService private readonly configurationService: IConfigurationService, @IWindowsService private readonly windowsService: IWindowsService, @IEditorService private readonly editorService: IEditorService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -412,7 +412,7 @@ export class TitlebarPart extends Part implements ITitleService { // Resizer this.resizer = append(this.element, $('div.resizer')); - const isMaximized = this.windowService.getConfiguration().maximized ? true : false; + const isMaximized = this.environmentService.configuration.maximized ? true : false; this.onDidChangeMaximized(isMaximized); this.windowService.onDidChangeMaximize(this.onDidChangeMaximized, this); } diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 570431b31c..cc32e5df65 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -66,6 +66,7 @@ export abstract class ViewletPanel extends Panel implements IView { protected actionRunner?: IActionRunner; protected toolbar: ToolBar; private headerContainer: HTMLElement; + private titleContainer: HTMLElement; constructor( options: IViewletPanelOptions, @@ -141,7 +142,12 @@ export abstract class ViewletPanel extends Panel implements IView { } protected renderHeaderTitle(container: HTMLElement, title: string): void { - append(container, $('h3.title', undefined, title)); + this.titleContainer = append(container, $('h3.title', undefined, title)); + } + + protected updateTitle(title: string): void { + this.titleContainer.textContent = title; + this._onDidChangeTitleArea.fire(); } focus(): void { diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index f6b1d615f3..87037c2e09 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -200,7 +200,11 @@ export class Workbench extends Layout { // TODO@Ben legacy file service const fileService = accessor.get(IFileService) as any; if (typeof fileService.setLegacyService === 'function') { - fileService.setLegacyService(accessor.get(ILegacyFileService)); + try { + fileService.setLegacyService(accessor.get(ILegacyFileService)); + } catch (error) { + //ignore, legacy file service might not be registered + } } // TODO@Sandeep debt around cyclic dependencies @@ -391,7 +395,7 @@ export class Workbench extends Layout { // Restore Zen Mode if (this.state.zenMode.restore) { - this.toggleZenMode(true, true); + this.toggleZenMode(false, true); } // Restore Editor Center Mode diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index a78e86052d..61602d1ee8 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -9,12 +9,12 @@ import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform'; export const IsMacContext = new RawContextKey('isMac', isMacintosh); export const IsLinuxContext = new RawContextKey('isLinux', isLinux); export const IsWindowsContext = new RawContextKey('isWindows', isWindows); -export const IsRemoteContext = new RawContextKey('isRemote', false); + +export const RemoteAuthorityContext = new RawContextKey('remoteAuthority', ''); export const HasMacNativeTabsContext = new RawContextKey('hasMacNativeTabs', false); export const SupportsWorkspacesContext = new RawContextKey('supportsWorkspaces', true); -export const SupportsOpenFileFolderContext = new RawContextKey('supportsOpenFileFolder', isMacintosh); export const IsDevelopmentContext = new RawContextKey('isDevelopment', false); diff --git a/src/vs/workbench/contrib/debug/browser/debugActionItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionItems.ts index 1da6c8d30d..5cf356aae9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionItems.ts @@ -235,9 +235,9 @@ export class FocusSessionActionItem extends SelectActionItem { } protected getSessions(): ReadonlyArray { - const hideSubSessions = this.configurationService.getValue('debug').hideSubSessions; + const showSubSessions = this.configurationService.getValue('debug').showSubSessionsInToolBar; const sessions = this.debugService.getModel().getSessions(); - return hideSubSessions ? sessions.filter(s => !s.parentSession) : sessions; + return showSubSessions ? sessions : sessions.filter(s => !s.parentSession); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 4865970c42..0086a958e8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -185,9 +185,9 @@ export function registerCommands(): void { if (!session || !session.getId) { session = debugService.getViewModel().focusedSession; const configurationService = accessor.get(IConfigurationService); - const hideSubSessions = configurationService.getValue('debug').hideSubSessions; + const showSubSessions = configurationService.getValue('debug').showSubSessionsInToolBar; // Stop should be sent to the root parent session - while (hideSubSessions && session && session.parentSession) { + while (!showSubSessions && session && session.parentSession) { session = session.parentSession; } } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 8a69b5e031..1fed4e546e 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -150,6 +150,9 @@ export interface IDebugSession extends ITreeElement { readonly state: State; readonly root: IWorkspaceFolder; readonly parentSession: IDebugSession | undefined; + readonly subId: string | undefined; + + setSubId(subId: string | undefined): void; getLabel(): string; @@ -424,7 +427,7 @@ export interface IDebugConfiguration { internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; extensionHostDebugAdapter: boolean; enableAllHovers: boolean; - hideSubSessions: boolean; + showSubSessionsInToolBar: boolean; console: { fontSize: number; fontFamily: string; diff --git a/src/vs/workbench/contrib/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/electron-browser/debug.contribution.ts index 9a748a6d94..78a2a0171f 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debug.contribution.ts @@ -229,6 +229,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize({ comment: ['This is the description for a setting'], key: 'enableAllHovers' }, "Controls whether the non-debug hovers should be enabled while debugging. When enabled the hover providers will be called to provide a hover. Regular hovers will not be shown even if this setting is enabled."), default: false }, + 'debug.showSubSessionsInToolBar': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showSubSessionsInToolBar' }, "Controls whether the debug sub-sessions are shown in the debug tool bar. When this setting is false the stop command on a sub-session will also stop the parent session."), + default: false + }, 'debug.console.fontSize': { type: 'number', description: nls.localize('debug.console.fontSize', "Controls the font size in pixels in the debug console."), diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/electron-browser/debugConfigurationManager.ts index f3a12c638a..a13c07a909 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugConfigurationManager.ts @@ -34,6 +34,7 @@ import { launchSchema, debuggersExtPoint, breakpointsExtPoint } from 'vs/workben import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); jsonRegistry.registerSchema(launchSchemaId, launchSchema); @@ -535,6 +536,7 @@ class Launch extends AbstractLaunch implements ILaunch { private configurationManager: ConfigurationManager, public workspace: IWorkspaceFolder, @IFileService private readonly fileService: IFileService, + @ITextFileService private readonly textFileService: ITextFileService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService ) { @@ -574,7 +576,7 @@ class Launch extends AbstractLaunch implements ILaunch { } created = true; // pin only if config file is created #8727 - return this.fileService.updateContent(resource, content).then(() => { + return this.textFileService.write(resource, content).then(() => { // convert string into IContent; see #32135 return content; }); diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts index a1c641c524..cd1784cfeb 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts @@ -135,27 +135,28 @@ export class DebugService implements IDebugService { this.toDispose.push(this.storageService.onWillSaveState(this.saveState, this)); this.lifecycleService.onShutdown(this.dispose, this); - this.toDispose.push(this.extensionHostDebugService.onAttachSession(data => { - const session = this.model.getSession(data.id, true); + this.toDispose.push(this.extensionHostDebugService.onAttachSession(event => { + const session = this.model.getSession(event.sessionId, true); if (session) { // EH was started in debug mode -> attach to it session.configuration.request = 'attach'; - session.configuration.port = data.port; + session.configuration.port = event.port; + session.setSubId(event.subId); this.launchOrAttachToSession(session).then(undefined, errors.onUnexpectedError); } })); - this.toDispose.push(this.extensionHostDebugService.onTerminateSession(sessionId => { - const session = this.model.getSession(sessionId); - if (session) { + this.toDispose.push(this.extensionHostDebugService.onTerminateSession(event => { + const session = this.model.getSession(event.sessionId); + if (session && session.subId === event.subId) { session.disconnect().then(undefined, errors.onUnexpectedError); } })); - this.toDispose.push(this.extensionHostDebugService.onLogToSession(data => { - const session = this.model.getSession(data.id, true); + this.toDispose.push(this.extensionHostDebugService.onLogToSession(event => { + const session = this.model.getSession(event.sessionId, true); if (session) { // extension logged output -> show it in REPL - const sev = data.log.severity === 'warn' ? severity.Warning : data.log.severity === 'error' ? severity.Error : severity.Info; - const { args, stack } = parse(data.log); + const sev = event.log.severity === 'warn' ? severity.Warning : event.log.severity === 'error' ? severity.Error : severity.Info; + const { args, stack } = parse(event.log); const frame = !!stack ? getFirstFrame(stack) : undefined; session.logToRepl(sev, args, frame); } @@ -439,9 +440,9 @@ export class DebugService implements IDebugService { } this.viewModel.firstSessionStart = false; - const hideSubSessions = this.configurationService.getValue('debug').hideSubSessions; + const showSubSessions = this.configurationService.getValue('debug').showSubSessionsInToolBar; const sessions = this.model.getSessions(); - const shownSessions = hideSubSessions ? sessions.filter(s => !s.parentSession) : sessions; + const shownSessions = showSubSessions ? sessions : sessions.filter(s => !s.parentSession); if (shownSessions.length > 1) { this.viewModel.setMultiSessionView(true); } diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugSession.ts b/src/vs/workbench/contrib/debug/electron-browser/debugSession.ts index ea0e3666c6..43acf75d30 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugSession.ts @@ -35,7 +35,9 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { INotificationService } from 'vs/platform/notification/common/notification'; export class DebugSession implements IDebugSession { + private id: string; + private _subId: string | undefined; private raw: RawDebugSession | undefined; private initialized = false; @@ -76,6 +78,14 @@ export class DebugSession implements IDebugSession { return this.id; } + setSubId(subId: string | undefined) { + this._subId = subId; + } + + get subId(): string | undefined { + return this._subId; + } + get configuration(): IConfig { return this._configuration.resolved; } diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index f8edf85345..3b5db8c9c6 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -124,6 +124,12 @@ export class MockDebugService implements IDebugService { export class MockSession implements IDebugSession { + subId: string | undefined; + + setSubId(subId: string | undefined): void { + throw new Error('Method not implemented.'); + } + get parentSession(): IDebugSession | undefined { return undefined; } diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts index f087e22e67..107ae5d251 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { INotificationService, IPromptChoice, Severity, IPromptOptions } from 'vs/platform/notification/common/notification'; +import { INotificationService, IPromptChoice, IPromptOptions, Severity } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -37,7 +37,6 @@ suite('Experimental Prompts', () => { commands: [ { text: 'Yes', - externalLink: 'https://code.visualstudio.com' }, { text: 'No' @@ -83,30 +82,30 @@ suite('Experimental Prompts', () => { }); - // test('Show experimental prompt if experiment should be run. Choosing option with link should mark experiment as complete', () => { + test('Show experimental prompt if experiment should be run. Choosing option with link should mark experiment as complete', () => { - // storageData = { - // enabled: true, - // state: ExperimentState.Run - // }; + storageData = { + enabled: true, + state: ExperimentState.Run + }; - // instantiationService.stub(INotificationService, { - // prompt: (a: Severity, b: string, c: IPromptChoice[], options: IPromptOptions) => { - // assert.equal(b, promptText); - // assert.equal(c.length, 2); - // c[0].run(); - // return undefined!; - // } - // }); + instantiationService.stub(INotificationService, { + prompt: (a: Severity, b: string, c: IPromptChoice[], options: IPromptOptions) => { + assert.equal(b, promptText); + assert.equal(c.length, 2); + c[0].run(); + return undefined!; + } + }); - // experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts); - // onExperimentEnabledEvent.fire(experiment); + experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts); + onExperimentEnabledEvent.fire(experiment); - // return Promise.resolve(null).then(result => { - // assert.equal(storageData['state'], ExperimentState.Complete); - // }); + return Promise.resolve(null).then(result => { + assert.equal(storageData['state'], ExperimentState.Complete); + }); - // }); + }); test('Show experimental prompt if experiment should be run. Choosing negative option should mark experiment as complete', () => { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 0e63dbcc06..77050d40e8 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -7,7 +7,7 @@ import { IViewlet } from 'vs/workbench/common/viewlet'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { IPager } from 'vs/base/common/paging'; -import { IQueryOptions, EnablementState, ILocalExtension, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IQueryOptions, EnablementState, ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IViewContainersRegistry, ViewContainer, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -32,40 +32,41 @@ export const enum ExtensionState { } export interface IExtension { - type?: ExtensionType; - state: ExtensionState; - name: string; - displayName: string; - identifier: IExtensionIdentifier; - publisher: string; - publisherDisplayName: string; - version: string; - latestVersion: string; - description: string; - url?: string; + readonly type?: ExtensionType; + readonly state: ExtensionState; + readonly name: string; + readonly displayName: string; + readonly identifier: IExtensionIdentifier; + readonly publisher: string; + readonly publisherDisplayName: string; + readonly version: string; + readonly latestVersion: string; + readonly description: string; + readonly url?: string; // {{SQL CARBON EDIT}} - downloadPage: string; - repository?: string; - iconUrl: string; - iconUrlFallback: string; - licenseUrl?: string; - installCount?: number; - rating?: number; - ratingCount?: number; - outdated: boolean; - enablementState: EnablementState; - dependencies: string[]; - extensionPack: string[]; - telemetryData: any; - preview: boolean; + readonly downloadPage?: string; + readonly repository?: string; + readonly iconUrl: string; + readonly iconUrlFallback: string; + readonly licenseUrl?: string; + readonly installCount?: number; + readonly rating?: number; + readonly ratingCount?: number; + readonly outdated: boolean; + readonly enablementState: EnablementState; + readonly dependencies: string[]; + readonly extensionPack: string[]; + readonly telemetryData: any; + readonly preview: boolean; getManifest(token: CancellationToken): Promise; getReadme(token: CancellationToken): Promise; hasReadme(): boolean; getChangelog(token: CancellationToken): Promise; hasChangelog(): boolean; - local?: ILocalExtension; + readonly server?: IExtensionManagementServer; + readonly local?: ILocalExtension; gallery?: IGalleryExtension; - isMalicious: boolean; + readonly isMalicious: boolean; } export interface IExtensionDependencies { @@ -84,7 +85,8 @@ export interface IExtensionsWorkbenchService { _serviceBrand: any; onChange: Event; local: IExtension[]; - queryLocal(): Promise; + outdated: IExtension[]; + queryLocal(server?: IExtensionManagementServer): Promise; queryGallery(token: CancellationToken): Promise>; queryGallery(options: IQueryOptions, token: CancellationToken): Promise>; canInstall(extension: IExtension): boolean; @@ -128,6 +130,7 @@ export enum ExtensionsPolicy { export interface IExtensionContainer { extension: IExtension | null; + updateWhenCounterExtensionChanges?: boolean; update(): void; } @@ -149,7 +152,11 @@ export class ExtensionContainers extends Disposable { for (const container of this.containers) { if (extension && container.extension) { if (areSameExtensions(container.extension.identifier, extension.identifier)) { - container.extension = extension; + if (!container.extension.server || container.extension.server === extension.server) { + container.extension = extension; + } else if (container.updateWhenCounterExtensionChanges) { + container.update(); + } } } else { container.update(); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts index a6e9aa1a4f..a484405094 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts @@ -28,7 +28,7 @@ import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/electron-browser/extensionsWidgets'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; +import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, SystemDisabledLabelAction, SystemDisabledWarningAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -215,6 +215,8 @@ export class ExtensionEditor extends BaseEditor { createEditor(parent: HTMLElement): void { const root = append(parent, $('.extension-editor')); + root.tabIndex = 0; // this is required for the focus tracker on the editor + root.style.outline = 'none'; this.header = append(root, $('.header')); this.iconContainer = append(this.header, $('.icon-container')); @@ -380,6 +382,8 @@ export class ExtensionEditor extends BaseEditor { // this.instantiationService.createInstance(RatingsWidget, this.rating, false) ]; const reloadAction = this.instantiationService.createInstance(ReloadAction); + const combinedInstallAction = this.instantiationService.createInstance(CombinedInstallAction); + const systemDisabledWarningAction = this.instantiationService.createInstance(SystemDisabledWarningAction); const actions = [ reloadAction, this.instantiationService.createInstance(StatusLabelAction), @@ -388,7 +392,10 @@ export class ExtensionEditor extends BaseEditor { this.instantiationService.createInstance(SetFileIconThemeAction, fileIconThemes), this.instantiationService.createInstance(EnableDropDownAction), this.instantiationService.createInstance(DisableDropDownAction, runningExtensions), - this.instantiationService.createInstance(CombinedInstallAction), + this.instantiationService.createInstance(RemoteInstallAction), + combinedInstallAction, + systemDisabledWarningAction, + this.instantiationService.createInstance(SystemDisabledLabelAction, systemDisabledWarningAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, true), ]; const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); @@ -410,6 +417,9 @@ export class ExtensionEditor extends BaseEditor { this.extensionManifest.get() .promise .then(manifest => { + if (manifest) { + combinedInstallAction.manifest = manifest; + } if (extension.extensionPack.length) { this.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); } @@ -543,19 +553,20 @@ export class ExtensionEditor extends BaseEditor { .then(renderBody) .then(removeEmbeddedSVGs) .then(body => { - const wbeviewElement = this.instantiationService.createInstance(WebviewElement, + const webviewElement = this.instantiationService.createInstance(WebviewElement, { enableFindWidget: true, }, { svgWhiteList: this.extensionsWorkbenchService.allowedBadgeProviders }); - wbeviewElement.mountTo(this.content); - const removeLayoutParticipant = arrays.insert(this.layoutParticipants, wbeviewElement); + webviewElement.mountTo(this.content); + this.contentDisposables.push(webviewElement.onDidFocus(() => this.fireOnDidFocus())); + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, webviewElement); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); - wbeviewElement.contents = body; + webviewElement.contents = body; - wbeviewElement.onDidClickLink(link => { + this.contentDisposables.push(webviewElement.onDidClickLink(link => { if (!link) { return; } @@ -563,9 +574,9 @@ export class ExtensionEditor extends BaseEditor { if (['http', 'https', 'mailto'].indexOf(link.scheme) >= 0 || (link.scheme === 'command' && link.path === ShowCurrentReleaseNotesAction.ID)) { this.openerService.open(link); } - }, null, this.contentDisposables); - this.contentDisposables.push(wbeviewElement); - return wbeviewElement; + }, null, this.contentDisposables)); + this.contentDisposables.push(webviewElement); + return webviewElement; }) .then(undefined, () => { const p = append(this.content, $('p.nocontent')); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts index b8d769f00f..67d803af75 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts @@ -16,9 +16,9 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; // {{SQL CARBON EDIT}} import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewlet, AutoUpdateConfigurationKey, IExtensionContainer, EXTENSIONS_CONFIG, ExtensionsPolicy, ExtensionsPolicyKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; -import { IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionRecommendation, IGalleryExtension, IExtensionsConfigContent, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionRecommendation, IGalleryExtension, IExtensionsConfigContent, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -56,6 +56,11 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { alert } from 'vs/base/browser/ui/aria/aria'; import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, COLOR_THEME_SETTING, ICON_THEME_SETTING, IFileIconTheme, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; +import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; // {{SQL CARBON EDIT}} import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -145,16 +150,29 @@ export class InstallAction extends ExtensionAction { private static readonly Class = 'extension-action prominent install'; private static readonly InstallingClass = 'extension-action install installing'; + private disposables: IDisposable[] = []; + + private _manifest: IExtensionManifest | null; + set manifest(manifest: IExtensionManifest) { + this._manifest = manifest; + this.updateLabel(); + } + constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, @IOpenerService private readonly openerService: IOpenerService, @IExtensionService private readonly runtimeExtensionService: IExtensionService, - @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService + @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILabelService private readonly labelService: ILabelService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, ) { super(`extensions.install`, InstallAction.INSTALL_LABEL, InstallAction.Class, false); this.update(); + this.labelService.onDidChangeFormatters(() => this.updateLabel(), this, this.disposables); } update(): void { @@ -166,15 +184,28 @@ export class InstallAction extends ExtensionAction { } this.enabled = this.extensionsWorkbenchService.canInstall(this.extension) && this.extension.state === ExtensionState.Uninstalled; + this.class = this.extension.state === ExtensionState.Installing ? InstallAction.InstallingClass : InstallAction.Class; + this.updateLabel(); + } + private updateLabel(): void { if (this.extension.state === ExtensionState.Installing) { this.label = InstallAction.INSTALLING_LABEL; - this.class = InstallAction.InstallingClass; this.tooltip = InstallAction.INSTALLING_LABEL; } else { - this.label = InstallAction.INSTALL_LABEL; - this.class = InstallAction.Class; - this.tooltip = InstallAction.INSTALL_LABEL; + if (this._manifest && this.workbenchEnvironmentService.configuration.remoteAuthority) { + if (isUIExtension(this._manifest, this.configurationService)) { + this.label = `${InstallAction.INSTALL_LABEL} (${this.extensionManagementServerService.localExtensionManagementServer.label})`; + this.tooltip = `${InstallAction.INSTALL_LABEL} (${this.extensionManagementServerService.localExtensionManagementServer.label})`; + } else { + const host = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.workbenchEnvironmentService.configuration.remoteAuthority) || localize('remote', "Remote"); + this.label = `${InstallAction.INSTALL_LABEL} (${host})`; + this.tooltip = `${InstallAction.INSTALL_LABEL} (${host})`; + } + } else { + this.label = InstallAction.INSTALL_LABEL; + this.tooltip = InstallAction.INSTALL_LABEL; + } } } @@ -185,7 +216,8 @@ export class InstallAction extends ExtensionAction { const extension = await this.install(this.extension); - // {{SQL CARBON EDIT}} + alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); + // Add extension object check since ADS third party extensions will be directed to a download page // and the extension object will be undefined. if (extension && extension.local) { @@ -246,6 +278,93 @@ export class InstallAction extends ExtensionAction { } return null; } + + dispose(): void { + this.disposables = dispose(this.disposables); + super.dispose(); + } +} + +export class RemoteInstallAction extends ExtensionAction { + + private static INSTALL_LABEL = localize('install', "Install"); + private static INSTALLING_LABEL = localize('installing', "Installing"); + + private static readonly Class = 'extension-action prominent install'; + private static readonly InstallingClass = 'extension-action install installing'; + + updateWhenCounterExtensionChanges: boolean = true; + private disposables: IDisposable[] = []; + private installing: boolean = false; + + constructor( + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(`extensions.remoteinstall`, RemoteInstallAction.INSTALL_LABEL, RemoteInstallAction.Class, false); + this.labelService.onDidChangeFormatters(() => this.updateLabel(), this, this.disposables); + this.updateLabel(); + this.update(); + } + + private updateLabel(): void { + if (this.installing) { + this.label = RemoteInstallAction.INSTALLING_LABEL; + return; + } + const remoteAuthority = this.environmentService.configuration.remoteAuthority; + if (remoteAuthority) { + const host = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.environmentService.configuration.remoteAuthority) || localize('remote', "Remote"); + this.label = `${RemoteInstallAction.INSTALL_LABEL} (${host})`; + return; + } + } + + update(): void { + this.enabled = false; + this.class = RemoteInstallAction.Class; + if (this.installing) { + this.enabled = true; + this.class = RemoteInstallAction.InstallingClass; + this.updateLabel(); + return; + } + if (this.environmentService.configuration.remoteAuthority + // Installed User Extension + && this.extension && this.extension.local && this.extension.type === ExtensionType.User && this.extension.state === ExtensionState.Installed + // Local Workspace Extension + && this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService) + // Extension does not exist in remote + && !this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer) + && this.extensionsWorkbenchService.canInstall(this.extension) + ) { + this.enabled = true; + this.updateLabel(); + return; + } + } + + async run(): Promise { + if (this.extensionManagementServerService.remoteExtensionManagementServer && !this.installing) { + this.installing = true; + this.update(); + this.extensionsWorkbenchService.open(this.extension); + alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); + if (this.extension.gallery) { + await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(this.extension.gallery); + this.installing = false; + this.update(); + } + } + } + + dispose(): void { + this.disposables = dispose(this.disposables); + super.dispose(); + } } export class UninstallAction extends ExtensionAction { @@ -281,19 +400,12 @@ export class UninstallAction extends ExtensionAction { this.label = UninstallAction.UninstallLabel; this.class = UninstallAction.UninstallClass; - const installedExtensions = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier)); - - if (!installedExtensions.length) { - this.enabled = false; - return; - } - if (state !== ExtensionState.Installed) { this.enabled = false; return; } - if (installedExtensions[0].type !== ExtensionType.User) { + if (this.extension.type !== ExtensionType.User) { this.enabled = false; return; } @@ -330,6 +442,8 @@ export class CombinedInstallAction extends ExtensionAction { this.update(); } + set manifest(manifiest: IExtensionManifest) { this.installAction.manifest = manifiest; this.update(); } + update(): void { this.installAction.extension = this.extension; this.uninstallAction.extension = this.extension; @@ -901,36 +1015,32 @@ export class CheckForUpdatesAction extends Action { } private checkUpdatesAndNotify(): void { - this.extensionsWorkbenchService.queryLocal().then( - extensions => { - const outdatedExtensions = extensions.filter(ext => ext.outdated === true); - if (!outdatedExtensions.length) { - this.notificationService.info(localize('noUpdatesAvailable', "All Extensions are up to date.")); - return; - } + const outdated = this.extensionsWorkbenchService.outdated; + if (!outdated.length) { + this.notificationService.info(localize('noUpdatesAvailable', "All Extensions are up to date.")); + return; + } - let msgAvailableExtensions = outdatedExtensions.length === 1 ? localize('singleUpdateAvailable', "An extension update is available.") : localize('updatesAvailable', "{0} extension updates are available.", outdatedExtensions.length); + let msgAvailableExtensions = outdated.length === 1 ? localize('singleUpdateAvailable', "An extension update is available.") : localize('updatesAvailable', "{0} extension updates are available.", outdated.length); - const disabledExtensionsCount = outdatedExtensions.filter(ext => ext.enablementState === EnablementState.Disabled || ext.enablementState === EnablementState.WorkspaceDisabled).length; - if (disabledExtensionsCount) { - if (outdatedExtensions.length === 1) { - msgAvailableExtensions = localize('singleDisabledUpdateAvailable', "An update to an extension which is disabled is available."); - } else if (disabledExtensionsCount === 1) { - msgAvailableExtensions = localize('updatesAvailableOneDisabled', "{0} extension updates are available. One of them is for a disabled extension.", outdatedExtensions.length); - } else if (disabledExtensionsCount === outdatedExtensions.length) { - msgAvailableExtensions = localize('updatesAvailableAllDisabled', "{0} extension updates are available. All of them are for disabled extensions.", outdatedExtensions.length); - } else { - msgAvailableExtensions = localize('updatesAvailableIncludingDisabled', "{0} extension updates are available. {1} of them are for disabled extensions.", outdatedExtensions.length, disabledExtensionsCount); - } - } - - this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => viewlet.search('')); - - this.notificationService.info(msgAvailableExtensions); + const disabledExtensionsCount = outdated.filter(ext => ext.enablementState === EnablementState.Disabled || ext.enablementState === EnablementState.WorkspaceDisabled).length; + if (disabledExtensionsCount) { + if (outdated.length === 1) { + msgAvailableExtensions = localize('singleDisabledUpdateAvailable', "An update to an extension which is disabled is available."); + } else if (disabledExtensionsCount === 1) { + msgAvailableExtensions = localize('updatesAvailableOneDisabled', "{0} extension updates are available. One of them is for a disabled extension.", outdated.length); + } else if (disabledExtensionsCount === outdated.length) { + msgAvailableExtensions = localize('updatesAvailableAllDisabled', "{0} extension updates are available. All of them are for disabled extensions.", outdated.length); + } else { + msgAvailableExtensions = localize('updatesAvailableIncludingDisabled', "{0} extension updates are available. {1} of them are for disabled extensions.", outdated.length, disabledExtensionsCount); } - ); + } + + this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => viewlet.search('')); + + this.notificationService.info(msgAvailableExtensions); } run(): Promise { @@ -1009,16 +1119,12 @@ export class UpdateAllAction extends Action { this.update(); } - private get outdated(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(e => e.outdated && e.state !== ExtensionState.Installing); - } - private update(): void { - this.enabled = this.outdated.length > 0; + this.enabled = this.extensionsWorkbenchService.outdated.length > 0; } run(): Promise { - return Promise.all(this.outdated.map(e => this.install(e))); + return Promise.all(this.extensionsWorkbenchService.outdated.map(e => this.install(e))); } private install(extension: IExtension): Promise { @@ -1044,16 +1150,18 @@ export class ReloadAction extends ExtensionAction { private static readonly EnabledClass = 'extension-action reload'; private static readonly DisabledClass = `${ReloadAction.EnabledClass} disabled`; + updateWhenCounterExtensionChanges: boolean = true; private disposables: IDisposable[] = []; - private _runningExtensions: IExtensionDescription[] = []; - private get runningExtensions(): IExtensionDescription[] { return this._runningExtensions; } - private set runningExtensions(runningExtensions: IExtensionDescription[]) { this._runningExtensions = runningExtensions; this.update(); } + private _runningExtensions: IExtensionDescription[] | null = null; constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWindowService private readonly windowService: IWindowService, @IExtensionService private readonly extensionService: IExtensionService, - @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService + @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this, this.disposables); @@ -1061,81 +1169,90 @@ export class ReloadAction extends ExtensionAction { } private updateRunningExtensions(): void { - this.extensionService.getExtensions().then(runningExtensions => this.runningExtensions = runningExtensions); + this.extensionService.getExtensions().then(runningExtensions => { this._runningExtensions = runningExtensions; this.update(); }); } update(): void { this.enabled = false; this.tooltip = ''; - if (!this.extension) { + if (!this.extension || !this._runningExtensions) { return; } const state = this.extension.state; if (state === ExtensionState.Installing || state === ExtensionState.Uninstalling) { return; } - const installed = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier))[0]; - const local = this.extension.local || (installed && installed.local); - if (local && local.manifest && local.manifest.contributes && local.manifest.contributes.localizations && local.manifest.contributes.localizations.length > 0) { + if (this.extension.local && this.extension.local.manifest && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.localizations && this.extension.local.manifest.contributes.localizations.length > 0) { return; } - this.computeReloadState(installed); + this.computeReloadState(); this.class = this.enabled ? ReloadAction.EnabledClass : ReloadAction.DisabledClass; } - private computeReloadState(installed: IExtension): void { + private computeReloadState(): void { + if (!this._runningExtensions) { + return; + } const isUninstalled = this.extension.state === ExtensionState.Uninstalled; - const isDisabled = this.extension.local ? !this.extensionEnablementService.isEnabled(this.extension.local) : false; - const isEnabled = this.extension.local ? this.extensionEnablementService.isEnabled(this.extension.local) : false; - const runningExtension = this.runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier))[0]; + const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier))[0]; - if (installed && installed.local) { + if (isUninstalled) { if (runningExtension) { - const isDifferentVersionRunning = this.extension.version !== runningExtension.version; - if (isDifferentVersionRunning && !isDisabled) { - if (!(this.extension.local && this.extensionService.canAddExtension(toExtensionDescription(this.extension.local)))) { - // Requires reload to run the updated extension - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - this.tooltip = localize('postUpdateTooltip', "Please reload Azure Data Studio to complete the updating of this extension."); - } - return; - } - if (isDisabled) { - // Requires reload to disable the extension - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - this.tooltip = localize('postDisableTooltip', "Please reload Azure Data Studio to complete the disabling of this extension."); - return; - } - } else { - if (!isDisabled && !(this.extension.local && this.extensionService.canAddExtension(toExtensionDescription(this.extension.local)))) { - this.enabled = true; - if (isEnabled) { - this.label = localize('reloadRequired', "Reload Required"); - // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to complete the enabling of this extension."); - } else { - this.label = localize('reloadRequired', "Reload Required"); - // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - this.tooltip = localize('postInstallTooltip', "Please reload Azure Data Studio to complete the installation of this extension."); - alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); - } - } + this.enabled = true; + this.label = localize('reloadRequired', "Reload Required"); + // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + this.tooltip = localize('postUninstallTooltip', "Please reload Azure Data Studio to complete the uninstallation of this extension."); + alert(localize('uninstallExtensionComplete', "Please reload Azure Data Studio to complete the uninstallation of the extension {0}.", this.extension.displayName)); } return; } - - if (isUninstalled && runningExtension) { - // Requires reload to deactivate the extension - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - this.tooltip = localize('postUninstallTooltip', "Please reload Azure Data Studio to complete the uninstallation of this extension."); - alert(localize('uninstallExtensionComplete', "Please reload Azure Data Studio to complete the uninstallation of the extension {0}.", this.extension.displayName)); - return; + if (this.extension.local) { + const isEnabled = this.extensionEnablementService.isEnabled(this.extension.local); + if (runningExtension) { + // Extension is running + const isSameExtensionRunning = this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation); + const isSameVersionRunning = isSameExtensionRunning && this.extension.version === runningExtension.version; + if (isEnabled) { + if (!isSameVersionRunning && !this.extensionService.canAddExtension(toExtensionDescription(this.extension.local))) { + this.enabled = true; + this.label = localize('reloadRequired', "Reload Required"); + // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + this.tooltip = localize('postUpdateTooltip', "Please reload Azure Data Studio to enable the updated extension."); + } + } else { + if (isSameExtensionRunning) { + this.enabled = true; + this.label = localize('reloadRequired', "Reload Required"); + // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + this.tooltip = localize('postDisableTooltip', "Please reload Azure Data Studio to disable this extension."); + } + } + return; + } else { + // Extension is not running + if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(this.extension.local))) { + this.enabled = true; + this.label = localize('reloadRequired', "Reload Required"); + // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to enable this extension."); + return; + } + if (this.workbenchEnvironmentService.configuration.remoteAuthority + // Local Workspace Extension + && this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService) + ) { + const remoteExtension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer)[0]; + // Extension exist in remote and enabled + if (remoteExtension && remoteExtension.local && this.extensionEnablementService.isEnabled(remoteExtension.local)) { + this.enabled = true; + this.label = localize('reloadRequired', "Reload Required"); + // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to enable this extension."); + alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); + return; + } + } + } } } @@ -1394,10 +1511,11 @@ export class ClearExtensionsInputAction extends Action { id: string, label: string, onSearchChange: Event, + value: string, @IViewletService private readonly viewletService: IViewletService ) { super(id, label, 'clear-extensions', true); - this.enabled = false; + this.onSearchChange(value); onSearchChange(this.onSearchChange, this, this.disposables); } @@ -1893,6 +2011,7 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio label: string, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IFileService private readonly fileService: IFileService, + @ITextFileService private readonly textFileService: ITextFileService, @IEditorService protected editorService: IEditorService, @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, @ITextModelService private readonly textModelResolverService: ITextModelService @@ -2041,7 +2160,7 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio return Promise.resolve(this.fileService.resolveContent(extensionsFileResource)).then(content => { return { created: false, extensionsFileResource, content: content.value }; }, err => { - return this.fileService.updateContent(extensionsFileResource, ExtensionsConfigurationInitialContent).then(() => { + return this.textFileService.write(extensionsFileResource, ExtensionsConfigurationInitialContent).then(() => { return { created: true, extensionsFileResource, content: ExtensionsConfigurationInitialContent }; }); }); @@ -2059,12 +2178,13 @@ export class ConfigureWorkspaceRecommendedExtensionsAction extends AbstractConfi id: string, label: string, @IFileService fileService: IFileService, + @ITextFileService textFileService: ITextFileService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IEditorService editorService: IEditorService, @IJSONEditingService jsonEditingService: IJSONEditingService, @ITextModelService textModelResolverService: ITextModelService ) { - super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); + super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); this.contextService.onDidChangeWorkbenchState(() => this.update(), this, this.disposables); this.update(); } @@ -2100,13 +2220,14 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac id: string, label: string, @IFileService fileService: IFileService, + @ITextFileService textFileService: ITextFileService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IEditorService editorService: IEditorService, @IJSONEditingService jsonEditingService: IJSONEditingService, @ITextModelService textModelResolverService: ITextModelService, @ICommandService private readonly commandService: ICommandService ) { - super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); + super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); this.contextService.onDidChangeWorkspaceFolders(() => this.update(), this, this.disposables); this.update(); } @@ -2145,6 +2266,7 @@ export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigure id: string, label: string, @IFileService fileService: IFileService, + @ITextFileService textFileService: ITextFileService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IEditorService editorService: IEditorService, @IJSONEditingService jsonEditingService: IJSONEditingService, @@ -2152,7 +2274,7 @@ export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigure @ICommandService private readonly commandService: ICommandService, @INotificationService private readonly notificationService: INotificationService ) { - super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); + super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); } run(shouldRecommend: boolean): Promise { @@ -2228,13 +2350,14 @@ export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecomm id: string, label: string, @IFileService fileService: IFileService, + @ITextFileService textFileService: ITextFileService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IEditorService editorService: IEditorService, @IJSONEditingService jsonEditingService: IJSONEditingService, @ITextModelService textModelResolverService: ITextModelService, @INotificationService private readonly notificationService: INotificationService ) { - super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); + super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); } run(shouldRecommend: boolean): Promise { @@ -2410,6 +2533,103 @@ export class MaliciousStatusLabelAction extends ExtensionAction { } } +export class SystemDisabledLabelAction extends ExtensionAction { + + private static readonly Class = 'disable-status'; + + updateWhenCounterExtensionChanges: boolean = true; + private disposables: IDisposable[] = []; + + constructor( + private readonly warningAction: SystemDisabledWarningAction, + ) { + super('extensions.systemDisabledLabel', warningAction.tooltip, `${SystemDisabledLabelAction.Class} hide`, false); + warningAction.onDidChange(() => this.update(), this, this.disposables); + } + + update(): void { + this.enabled = this.warningAction.enabled; + if (this.enabled) { + this.class = SystemDisabledLabelAction.Class; + this.label = this.warningAction.tooltip; + } else { + this.class = `${SystemDisabledLabelAction.Class} hide`; + this.label = ''; + } + } + + run(): Promise { + return Promise.resolve(null); + } + + dispose(): void { + dispose(this.disposables); + super.dispose(); + } +} + +export class SystemDisabledWarningAction extends ExtensionAction { + + private static readonly Class = 'disable-warning'; + + updateWhenCounterExtensionChanges: boolean = true; + private disposables: IDisposable[] = []; + private _runningExtensions: IExtensionDescription[] | null = null; + + constructor( + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionService private readonly extensionService: IExtensionService, + ) { + super('extensions.install', '', `${SystemDisabledWarningAction.Class} hide`, false); + this.labelService.onDidChangeFormatters(() => this.update(), this, this.disposables); + this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this, this.disposables); + this.updateRunningExtensions(); + this.update(); + } + + private updateRunningExtensions(): void { + this.extensionService.getExtensions().then(runningExtensions => { this._runningExtensions = runningExtensions; this.update(); }); + } + + update(): void { + this.enabled = false; + this.class = `${SystemDisabledWarningAction.Class} hide`; + this.tooltip = ''; + if (this.extension && this.extension.local && this._runningExtensions) { + if ( + // Remote Window + this.workbenchEnvironmentService.configuration.remoteAuthority + // Local Workspace Extension + && this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService) + // Extension does not exist in remote + && !this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer) + ) { + this.enabled = true; + this.class = `${SystemDisabledWarningAction.Class}`; + const host = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.workbenchEnvironmentService.configuration.remoteAuthority) || localize('remote', "Remote"); + this.tooltip = localize('disabled workspace Extension', "This extension is disabled because it cannot run in a window connected to the remote server.", host, host); + if (this.extensionsWorkbenchService.canInstall(this.extension)) { + this.tooltip = `${this.tooltip} ${localize('Install in remote server', "Install it in '{0}' server to enable.", host)}`; + } + return; + } + } + } + + run(): Promise { + return Promise.resolve(null); + } + + dispose(): void { + dispose(this.disposables); + super.dispose(); + } +} + export class DisableAllAction extends Action { static readonly ID = 'workbench.extensions.action.disableAll'; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts index bb6b7de3c8..505cf16538 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts @@ -12,8 +12,8 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { IExtension, IExtensionsWorkbenchService, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; -import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionItem, StatusLabelAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; +import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { Label, RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/electron-browser/extensionsWidgets'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -54,9 +54,9 @@ export class Renderer implements IPagedRenderer { private extensionViewState: IExtensionsViewState, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService ) { } get templateId() { return 'extension'; } @@ -100,7 +100,9 @@ export class Renderer implements IPagedRenderer { this.instantiationService.createInstance(UpdateAction), this.instantiationService.createInstance(ReloadAction), this.instantiationService.createInstance(InstallAction), + this.instantiationService.createInstance(RemoteInstallAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, false), + this.instantiationService.createInstance(SystemDisabledWarningAction), this.instantiationService.createInstance(ManageExtensionAction) ]; const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); @@ -136,23 +138,18 @@ export class Renderer implements IPagedRenderer { renderElement(extension: IExtension, index: number, data: ITemplateData): void { removeClass(data.element, 'loading'); + if (extension.state !== ExtensionState.Uninstalled && !extension.server) { + // Get the extension if it is installed and has no server information + extension = this.extensionsWorkbenchService.local.filter(e => e.server === extension.server && areSameExtensions(e.identifier, extension.identifier))[0] || extension; + } + data.extensionDisposables = dispose(data.extensionDisposables); const updateEnablement = async () => { const runningExtensions = await this.extensionService.getExtensions(); - const installed = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; - if (installed && installed.local) { - const installedExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(installed.local.location); - const isSameExtensionRunning = runningExtensions.some(e => { - if (!areSameExtensions({ id: e.identifier.value }, extension.identifier)) { - return false; - } - const runningExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(e.extensionLocation); - if (!installedExtensionServer || !runningExtensionServer) { - return false; - } - return installedExtensionServer.authority === runningExtensionServer.authority; - }); + if (extension.local) { + const runningExtension = runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, extension.identifier))[0]; + const isSameExtensionRunning = runningExtension && extension.server === this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation); toggleClass(data.root, 'disabled', !isSameExtensionRunning); } else { removeClass(data.root, 'disabled'); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts index 73b832acdb..1cff90dd73 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts @@ -24,9 +24,9 @@ import { ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; -import { IExtensionManagementService, IExtensionManagementServerService, IExtensionManagementServer, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionManagementServerService, IExtensionManagementServer, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, GroupByServerExtensionsView, DefaultRecommendedExtensionsView } from './extensionsViews'; +import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, ServerExtensionsView, DefaultRecommendedExtensionsView } from './extensionsViews'; import { OpenGlobalSettingsAction } from 'vs/workbench/contrib/preferences/browser/preferencesActions'; import { IProgressService2, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -35,7 +35,6 @@ import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/servi import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views'; -import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -54,6 +53,8 @@ import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; +import { RemoteAuthorityContext as RemoteAuthorityContext } from 'vs/workbench/common/contextkeys'; +import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; interface SearchInputEvent extends Event { target: HTMLInputElement; @@ -61,12 +62,13 @@ interface SearchInputEvent extends Event { } const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', false); -const SearchExtensionsContext = new RawContextKey('searchExtensions', false); +const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); +const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); +const SearchServerExtensionsContext = new RawContextKey('searchServerExtensions', false); const HasInstalledExtensionsContext = new RawContextKey('hasInstalledExtensions', true); const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltInExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); const DefaultRecommendedExtensionsContext = new RawContextKey('defaultRecommendedExtensions', false); -const GroupByServersContext = new RawContextKey('groupByServersContext', false); const viewIdNameMappings: { [id: string]: string } = { 'extensions.listView': localize('marketPlace', "Marketplace"), 'extensions.enabledExtensionList': localize('enabledExtensions', "Enabled"), @@ -92,10 +94,10 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio private registerViews(): void { let viewDescriptors: IViewDescriptor[] = []; viewDescriptors.push(this.createMarketPlaceExtensionsListViewDescriptor()); - viewDescriptors.push(this.createEnabledExtensionsListViewDescriptor()); - viewDescriptors.push(this.createDisabledExtensionsListViewDescriptor()); + viewDescriptors.push(this.createDefaultEnabledExtensionsListViewDescriptor()); + viewDescriptors.push(this.createDefaultDisabledExtensionsListViewDescriptor()); // {{SQL CARBON EDIT}} - // viewDescriptors.push(this.createPopularExtensionsListViewDescriptor()); + // viewDescriptors.push(this.createDefaultPopularExtensionsListViewDescriptor()); viewDescriptors.push(this.createBuiltInExtensionsListViewDescriptor()); viewDescriptors.push(this.createBuiltInBasicsExtensionsListViewDescriptor()); viewDescriptors.push(this.createBuiltInThemesExtensionsListViewDescriptor()); @@ -103,8 +105,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio viewDescriptors.push(this.createOtherRecommendedExtensionsListViewDescriptor()); viewDescriptors.push(this.createWorkspaceRecommendedExtensionsListViewDescriptor()); + viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.localExtensionManagementServer)); if (this.extensionManagementServerService.remoteExtensionManagementServer) { - viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.localExtensionManagementServer)); viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.remoteExtensionManagementServer)); } @@ -118,20 +120,20 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id, name: viewIdNameMappings[id], ctorDescriptor: { ctor: ExtensionsListView }, - when: ContextKeyExpr.and(ContextKeyExpr.has('searchExtensions'), ContextKeyExpr.not('searchInstalledExtensions'), ContextKeyExpr.not('searchBuiltInExtensions'), ContextKeyExpr.not('recommendedExtensions'), ContextKeyExpr.not('groupByServersContext')), + when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions')), weight: 100 }; } // Separate view for enabled extensions required as we need to show enabled, disabled and recommended sections // in the default view when there is no search text, but user has installed extensions. - private createEnabledExtensionsListViewDescriptor(): IViewDescriptor { + private createDefaultEnabledExtensionsListViewDescriptor(): IViewDescriptor { const id = 'extensions.enabledExtensionList'; return { id, name: viewIdNameMappings[id], ctorDescriptor: { ctor: EnabledExtensionsView }, - when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.isEqualTo('')), weight: 40, canToggleVisibility: true, order: 1 @@ -140,13 +142,13 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio // Separate view for disabled extensions required as we need to show enabled, disabled and recommended sections // in the default view when there is no search text, but user has installed extensions. - private createDisabledExtensionsListViewDescriptor(): IViewDescriptor { + private createDefaultDisabledExtensionsListViewDescriptor(): IViewDescriptor { const id = 'extensions.disabledExtensionList'; return { id, name: viewIdNameMappings[id], ctorDescriptor: { ctor: DisabledExtensionsView }, - when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.isEqualTo('')), weight: 10, canToggleVisibility: true, order: 3, @@ -154,28 +156,36 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio }; } - // {{SQL CARBON EDIT}} - // // Separate view for popular extensions required as we need to show popular and recommended sections - // // in the default view when there is no search text, and user has no installed extensions. - // private createPopularExtensionsListViewDescriptor(): IViewDescriptor { - // const id = 'extensions.popularExtensionsList'; - // return { - // id, - // name: viewIdNameMappings[id], - // ctorDescriptor: { ctor: ExtensionsListView }, - // when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.not('hasInstalledExtensions')), - // weight: 60, - // order: 1 - // }; - // } + /* // {{SQL CARBON EDIT}} + // Separate view for popular extensions required as we need to show popular and recommended sections + // in the default view when there is no search text, and user has no installed extensions. + private createDefaultPopularExtensionsListViewDescriptor(): IViewDescriptor { + const id = 'extensions.popularExtensionsList'; + return { + id, + name: viewIdNameMappings[id], + ctorDescriptor: { ctor: ExtensionsListView }, + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), + weight: 60, + order: 1 + }; + } + */ private createExtensionsViewDescriptorsForServer(server: IExtensionManagementServer): IViewDescriptor[] { return [{ - id: `server.extensionsList.${server.authority}`, + id: `extensions.${server.authority}.installed`, name: server.label, - ctorDescriptor: { ctor: GroupByServerExtensionsView }, - when: ContextKeyExpr.has('groupByServersContext'), + ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, + when: ContextKeyExpr.and(ContextKeyExpr.has('searchServerExtensions')), weight: 100 + }, { + id: `extensions.${server.authority}.default`, + name: server.label, + ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.notEqualsTo('')), + weight: 40, + order: 1 }]; } @@ -188,7 +198,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id, name: viewIdNameMappings[id], ctorDescriptor: { ctor: DefaultRecommendedExtensionsView }, - when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('defaultRecommendedExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('defaultRecommendedExtensions')), weight: 40, order: 2, canToggleVisibility: true @@ -266,10 +276,11 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private onSearchChange: EventOf; private nonEmptyWorkspaceContextKey: IContextKey; - private searchExtensionsContextKey: IContextKey; + private defaultViewsContextKey: IContextKey; + private searchMarketplaceExtensionsContextKey: IContextKey; + private searchServerExtensionsContextKey: IContextKey; private hasInstalledExtensionsContextKey: IContextKey; private searchBuiltInExtensionsContextKey: IContextKey; - private groupByServersContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; private defaultRecommendedExtensionsContextKey: IContextKey; @@ -304,11 +315,12 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.searchDelayer = new Delayer(500); this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService); - this.searchExtensionsContextKey = SearchExtensionsContext.bindTo(contextKeyService); + this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService); + this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); + this.searchServerExtensionsContextKey = SearchServerExtensionsContext.bindTo(contextKeyService); this.hasInstalledExtensionsContextKey = HasInstalledExtensionsContext.bindTo(contextKeyService); this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService); this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); - this.groupByServersContextKey = GroupByServersContext.bindTo(contextKeyService); this.defaultRecommendedExtensionsContextKey = DefaultRecommendedExtensionsContext.bindTo(contextKeyService); this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); this.disposables.push(this.viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables)); @@ -393,7 +405,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio getActions(): IAction[] { if (!this.primaryActions) { this.primaryActions = [ - this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange) + this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox.getValue()) ]; } return this.primaryActions; @@ -455,19 +467,20 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private doSearch(): Promise { const value = this.normalizedQuery(); - this.searchExtensionsContextKey.set(!!value); - this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); - this.groupByServersContextKey.set(ExtensionsListView.isGroupByServersExtensionsQuery(value)); - this.recommendedExtensionsContextKey.set(ExtensionsListView.isRecommendedExtensionsQuery(value)); + this.defaultViewsContextKey.set(!value); + const isServerExtensionsQuery = ExtensionsListView.isServerExtensionsQuery(value); + const isBuiltInExtensionsQuery = ExtensionsListView.isBuiltInExtensionsQuery(value); + const isRecommendedExtensionsQuery = ExtensionsListView.isRecommendedExtensionsQuery(value); + this.searchServerExtensionsContextKey.set(isServerExtensionsQuery); + this.searchBuiltInExtensionsContextKey.set(isBuiltInExtensionsQuery); + this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); + this.searchMarketplaceExtensionsContextKey.set(!!value && !isServerExtensionsQuery && !isBuiltInExtensionsQuery && !isRecommendedExtensionsQuery); this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY); - if (value) { - return this.progress(Promise.all(this.panels.map(view => - (view).show(this.normalizedQuery()) - .then(model => this.alertSearchResult(model.length, view.id)) - ))).then(() => undefined); - } - return Promise.resolve(); + return this.progress(Promise.all(this.panels.map(view => + (view).show(this.normalizedQuery()) + .then(model => this.alertSearchResult(model.length, view.id)) + ))).then(() => undefined); } protected onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { @@ -565,7 +578,8 @@ export class StatusUpdater implements IWorkbenchContribution { constructor( @IActivityService private readonly activityService: IActivityService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService ) { extensionsWorkbenchService.onChange(this.onServiceChange, this, this.disposables); } @@ -579,7 +593,7 @@ export class StatusUpdater implements IWorkbenchContribution { return; } - const outdated = this.extensionsWorkbenchService.local.reduce((r, e) => r + (e.outdated && e.enablementState !== EnablementState.Disabled && e.enablementState !== EnablementState.WorkspaceDisabled ? 1 : 0), 0); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) ? 1 : 0), 0); if (outdated > 0) { const badge = new NumberBadge(outdated, n => localize('outdatedExtensions', '{0} Outdated Extensions', n)); this.badgeHandle = this.activityService.showActivity(VIEWLET_ID, badge, 'extensions-badge count-badge'); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts index 59ca4c231f..1592695c31 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { assign } from 'vs/base/common/objects'; import { Event, Emitter } from 'vs/base/common/event'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortBy, SortOrder, IQueryOptions, IExtensionTipsService, IExtensionRecommendation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortBy, SortOrder, IQueryOptions, IExtensionTipsService, IExtensionRecommendation, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -45,6 +45,9 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import product from 'vs/platform/product/node/product'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; class ExtensionsViewState extends Disposable implements IExtensionsViewState { @@ -63,8 +66,13 @@ class ExtensionsViewState extends Disposable implements IExtensionsViewState { } } +export interface ExtensionsListViewOptions extends IViewletViewOptions { + server?: IExtensionManagementServer; +} + export class ExtensionsListView extends ViewletPanel { + private readonly server: IExtensionManagementServer | undefined; private messageBox: HTMLElement; private extensionsList: HTMLElement; private badge: CountBadge; @@ -73,7 +81,7 @@ export class ExtensionsListView extends ViewletPanel { private queryRequest: { query: string, request: CancelablePromise> } | null; constructor( - private options: IViewletViewOptions, + options: ExtensionsListViewOptions, @INotificationService protected notificationService: INotificationService, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @@ -91,6 +99,7 @@ export class ExtensionsListView extends ViewletPanel { @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService ) { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); + this.server = options.server; } protected renderHeader(container: HTMLElement): void { @@ -98,7 +107,7 @@ export class ExtensionsListView extends ViewletPanel { } renderHeaderTitle(container: HTMLElement): void { - super.renderHeaderTitle(container, this.options.title); + super.renderHeaderTitle(container, this.title); this.badgeContainer = append(container, $('.count-badge-wrapper')); this.badge = new CountBadge(this.badgeContainer); @@ -233,7 +242,7 @@ export class ExtensionsListView extends ViewletPanel { private async queryByIds(ids: string[], options: IQueryOptions, token: CancellationToken): Promise> { const idsSet: Set = ids.reduce((result, id) => { result.add(id.toLowerCase()); return result; }, new Set()); - const result = (await this.extensionsWorkbenchService.queryLocal()) + const result = (await this.extensionsWorkbenchService.queryLocal(this.server)) .filter(e => idsSet.has(e.identifier.id.toLowerCase())); if (result.length) { @@ -261,7 +270,7 @@ export class ExtensionsListView extends ViewletPanel { } value = value.replace(/@builtin/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); - let result = await this.extensionsWorkbenchService.queryLocal(); + let result = await this.extensionsWorkbenchService.queryLocal(this.server); result = result .filter(e => e.type === ExtensionType.System && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)); @@ -313,7 +322,7 @@ export class ExtensionsListView extends ViewletPanel { // Show installed extensions value = value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); - let result = await this.extensionsWorkbenchService.queryLocal(); + let result = await this.extensionsWorkbenchService.queryLocal(this.server); result = result .filter(e => e.type === ExtensionType.User @@ -327,7 +336,7 @@ export class ExtensionsListView extends ViewletPanel { if (/@outdated/i.test(value)) { value = value.replace(/@outdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); - const local = await this.extensionsWorkbenchService.queryLocal(); + const local = await this.extensionsWorkbenchService.queryLocal(this.server); const result = local .sort((e1, e2) => e1.displayName.localeCompare(e2.displayName)) .filter(extension => extension.outdated @@ -340,7 +349,7 @@ export class ExtensionsListView extends ViewletPanel { if (/@disabled/i.test(value)) { value = value.replace(/@disabled/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); - const local = await this.extensionsWorkbenchService.queryLocal(); + const local = await this.extensionsWorkbenchService.queryLocal(this.server); const runningExtensions = await this.extensionService.getExtensions(); const result = local @@ -355,7 +364,7 @@ export class ExtensionsListView extends ViewletPanel { if (/@enabled/i.test(value)) { value = value ? value.replace(/@enabled/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase() : ''; - const local = (await this.extensionsWorkbenchService.queryLocal()).filter(e => e.type === ExtensionType.User); + const local = (await this.extensionsWorkbenchService.queryLocal(this.server)).filter(e => e.type === ExtensionType.User); const runningExtensions = await this.extensionService.getExtensions(); const result = local @@ -485,7 +494,7 @@ export class ExtensionsListView extends ViewletPanel { private getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const value = query.value.replace(/@recommended:all/g, '').replace(/@recommended/g, '').trim().toLowerCase(); - return this.extensionsWorkbenchService.queryLocal() + return this.extensionsWorkbenchService.queryLocal(this.server) .then(result => result.filter(e => e.type === ExtensionType.User)) .then(local => { const fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); @@ -540,7 +549,7 @@ export class ExtensionsListView extends ViewletPanel { private getRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); - return this.extensionsWorkbenchService.queryLocal() + return this.extensionsWorkbenchService.queryLocal(this.server) .then(result => result.filter(e => e.type === ExtensionType.User)) .then(local => { let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); @@ -780,8 +789,8 @@ export class ExtensionsListView extends ViewletPanel { return /@installed|@outdated|@enabled|@disabled/i.test(query); } - static isGroupByServersExtensionsQuery(query: string): boolean { - return !!Query.parse(query).groupBy; + static isServerExtensionsQuery(query: string): boolean { + return /@installed|@outdated/i.test(query); } static isRecommendedExtensionsQuery(query: string): boolean { @@ -818,10 +827,40 @@ export class ExtensionsListView extends ViewletPanel { } } -export class GroupByServerExtensionsView extends ExtensionsListView { +function getServerLabel(server: IExtensionManagementServer, labelService: ILabelService, workbenchEnvironmentService: IWorkbenchEnvironmentService): string { + return workbenchEnvironmentService.configuration.remoteAuthority === server.authority ? labelService.getHostLabel(REMOTE_HOST_SCHEME, server.authority) || server.label : server.label; +} + +export class ServerExtensionsView extends ExtensionsListView { + + constructor( + server: IExtensionManagementServer, + options: ExtensionsListViewOptions, + @INotificationService notificationService: INotificationService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IExtensionService extensionService: IExtensionService, + @IEditorService editorService: IEditorService, + @IExtensionTipsService tipsService: IExtensionTipsService, + @IModeService modeService: IModeService, + @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IExperimentService experimentService: IExperimentService, + @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @ILabelService labelService: ILabelService, + @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService + ) { + options.title = getServerLabel(server, labelService, workbenchEnvironmentService); + options.server = server; + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, modeService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService); + this.disposables.push(labelService.onDidChangeFormatters(() => this.updateTitle(getServerLabel(server, labelService, workbenchEnvironmentService)))); + } async show(query: string): Promise> { - query = query.replace(/@group:server/g, '').trim(); query = query ? query : '@installed'; if (!ExtensionsListView.isInstalledExtensionsQuery(query) && !ExtensionsListView.isBuiltInExtensionsQuery(query)) { query = query += ' @installed'; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts index 94ac13a551..7dbd2b8cab 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts @@ -16,7 +16,7 @@ import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension; @@ -205,7 +205,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { @IThemeService private readonly themeService: IThemeService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IWindowService private readonly windowService: IWindowService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(); this.render(); @@ -222,11 +222,10 @@ export class RemoteBadgeWidget extends ExtensionWidget { render(): void { this.clear(); - if (!this.extension || !this.extension.local) { + if (!this.extension || !this.extension.local || !this.extension.server) { return; } - const server = this.extensionManagementServerService.getExtensionManagementServer(this.extension.local.location); - if (server === this.extensionManagementServerService.remoteExtensionManagementServer) { + if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { this.element = append(this.parent, $('div.extension-remote-badge')); append(this.element, $('span.octicon.octicon-file-symlink-directory')); @@ -245,7 +244,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { const updateTitle = () => { if (this.element) { - this.element.title = localize('remote extension title', "Extension in {0}", this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.windowService.getConfiguration().remoteAuthority)); + this.element.title = localize('remote extension title', "Extension in {0}", this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.environmentService.configuration.remoteAuthority)); } }; this.labelService.onDidChangeFormatters(() => updateTitle(), this, this.disposables); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionActions.css index 4671a2886f..a275945e03 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionActions.css @@ -35,6 +35,7 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.extension-editor-dropdown-action, .monaco-action-bar .action-item.disabled .action-label.extension-action.reload, .monaco-action-bar .action-item.disabled .action-label.disable-status.hide, +.monaco-action-bar .action-item.disabled .action-label.disable-warning.hide, .monaco-action-bar .action-item.disabled .action-label.extension-status-label.hide, .monaco-action-bar .action-item.disabled .action-label.malicious-status.not-malicious { display: none; @@ -61,6 +62,29 @@ font-style: italic; } +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.disable-status { + margin-left: 0; + padding-left: 0; +} + +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.disable-warning, +.extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.disable-warning { + cursor: default; + margin: 0.1em; +} + +.monaco-action-bar .action-item .action-label.disable-warning.icon { + opacity: 1; + height: 18px; + width: 10px; + background: url('status-warning.svg') center center no-repeat; + margin-top: 0.15em +} + +.vs-dark .monaco-action-bar .action-item .action-label.disable-warning.icon { + background: url('status-warning-inverse.svg') center center no-repeat; +} + .extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.extension-status-label, .extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.disable-status, .extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.malicious-status { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/status-warning-inverse.svg b/src/vs/workbench/contrib/extensions/electron-browser/media/status-warning-inverse.svg new file mode 100644 index 0000000000..df44e61b32 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/status-warning-inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/status-warning.svg b/src/vs/workbench/contrib/extensions/electron-browser/media/status-warning.svg new file mode 100644 index 0000000000..f4e2a84b0a --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/status-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index bfc72e821a..081ad1d08b 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -43,6 +43,7 @@ import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -119,7 +120,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IStorageService storageService: IStorageService, @ILabelService private readonly _labelService: ILabelService, - @IWindowService private readonly _windowService: IWindowService + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService ) { super(RuntimeExtensionsEditor.ID, telemetryService, themeService, storageService); @@ -381,7 +382,7 @@ export class RuntimeExtensionsEditor extends BaseEditor { el.innerHTML = renderOcticons(`$(rss) ${element.description.extensionLocation.authority}`); data.msgContainer.appendChild(el); - const hostLabel = this._labelService.getHostLabel(REMOTE_HOST_SCHEME, this._windowService.getConfiguration().remoteAuthority); + const hostLabel = this._labelService.getHostLabel(REMOTE_HOST_SCHEME, this._environmentService.configuration.remoteAuthority); if (hostLabel) { el.innerHTML = renderOcticons(`$(rss) ${hostLabel}`); } diff --git a/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts index 46d9a7ec6d..f10d75de73 100644 --- a/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts @@ -9,15 +9,15 @@ import { Event, Emitter } from 'vs/base/common/event'; import { index, distinct } from 'vs/base/common/arrays'; import { ThrottledDelayer } from 'vs/base/common/async'; import { isPromiseCanceledError } from 'vs/base/common/errors'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; // {{SQL CARBON EDIT}} import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, - InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementServerService, INSTALL_ERROR_INCOMPATIBLE + InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, INSTALL_ERROR_INCOMPATIBLE } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowService } from 'vs/platform/windows/common/windows'; @@ -37,7 +37,6 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; import { IExtensionManifest, ExtensionType, ExtensionIdentifierWithVersion, IExtension as IPlatformExtension } from 'vs/platform/extensions/common/extensions'; -import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil'; // {{SQL CARBON EDIT}} import { isEngineValid } from 'vs/platform/extensions/node/extensionValidator'; @@ -55,6 +54,7 @@ class Extension implements IExtension { constructor( private galleryService: IExtensionGalleryService, private stateProvider: IExtensionStateProvider, + public readonly server: IExtensionManagementServer | undefined, public local: ILocalExtension | undefined, public gallery: IGalleryExtension | undefined, private telemetryService: ITelemetryService, @@ -370,14 +370,179 @@ class ExtensionDependencies implements IExtensionDependencies { } } -export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, IURLHandler { +class Extensions extends Disposable { - private static readonly SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours - _serviceBrand: any; - private stateProvider: IExtensionStateProvider; + private readonly _onChange: Emitter = new Emitter(); + get onChange(): Event { return this._onChange.event; } + + private readonly stateProvider: IExtensionStateProvider; private installing: Extension[] = []; private uninstalling: Extension[] = []; private installed: Extension[] = []; + + constructor( + private readonly server: IExtensionManagementServer, + @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ILogService private readonly logService: ILogService, + @IFileService private readonly fileService: IFileService, + @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService + ) { + super(); + this.stateProvider = ext => this.getExtensionState(ext); + this._register(server.extensionManagementService.onInstallExtension(e => this.onInstallExtension(e))); + this._register(server.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e))); + this._register(server.extensionManagementService.onUninstallExtension(e => this.onUninstallExtension(e))); + this._register(server.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); + this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e))); + } + + get local(): IExtension[] { + const installing = this.installing + .filter(e => !this.installed.some(installed => areSameExtensions(installed.identifier, e.identifier))) + .map(e => e); + + return [...this.installed, ...installing]; + } + + async queryInstalled(): Promise { + const installed = await this.server.extensionManagementService.getInstalled(); + const byId = index(this.installed, e => e.identifier.id); + this.installed = installed.map(local => { + const extension = byId[local.identifier.id] || new Extension(this.galleryService, this.stateProvider, this.server, local, undefined, this.telemetryService, this.logService, this.fileService); + extension.local = local; + extension.enablementState = this.extensionEnablementService.getEnablementState(local); + return extension; + }); + this._onChange.fire(undefined); + return this.local; + } + + async syncLocalWithGalleryExtension(gallery: IGalleryExtension, maliciousExtensionSet: Set): Promise { + const extension = this.getInstalledExtensionMatchingGallery(gallery); + if (!extension) { + return false; + } + if (maliciousExtensionSet.has(extension.identifier.id)) { + extension.isMalicious = true; + } + // Loading the compatible version only there is an engine property + // Otherwise falling back to old way so that we will not make many roundtrips + const compatible = gallery.properties.engine ? await this.galleryService.getCompatibleExtension(gallery) : gallery; + if (!compatible) { + return false; + } + // Sync the local extension with gallery extension if local extension doesnot has metadata + if (extension.local) { + const local = extension.local.metadata ? extension.local : await this.server.extensionManagementService.updateMetadata(extension.local, { id: compatible.identifier.uuid, publisherDisplayName: compatible.publisherDisplayName, publisherId: compatible.publisherId }); + extension.local = local; + extension.gallery = compatible; + this._onChange.fire(extension); + return true; + } + return false; + } + + private getInstalledExtensionMatchingGallery(gallery: IGalleryExtension): Extension | null { + for (const installed of this.installed) { + if (installed.uuid) { // Installed from Gallery + if (installed.uuid === gallery.identifier.uuid) { + return installed; + } + } else { + if (areSameExtensions(installed.identifier, gallery.identifier)) { // Installed from other sources + return installed; + } + } + } + return null; + } + + private onInstallExtension(event: InstallExtensionEvent): void { + const { gallery } = event; + if (gallery) { + const extension = this.installed.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0] + || new Extension(this.galleryService, this.stateProvider, this.server, undefined, gallery, this.telemetryService, this.logService, this.fileService); + this.installing.push(extension); + this._onChange.fire(extension); + } + } + + private onDidInstallExtension(event: DidInstallExtensionEvent): void { + const { local, zipPath, error, gallery } = event; + const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0] : null; + this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; + + let extension: Extension | undefined = installingExtension ? installingExtension : zipPath ? new Extension(this.galleryService, this.stateProvider, this.server, local, undefined, this.telemetryService, this.logService, this.fileService) : undefined; + if (extension) { + if (local) { + const installed = this.installed.filter(e => areSameExtensions(e.identifier, extension!.identifier))[0]; + if (installed) { + extension = installed; + } else { + this.installed.push(extension); + } + extension.local = local; + if (!extension.gallery) { + extension.gallery = gallery; + } + } + } + this._onChange.fire(error ? undefined : extension); + } + + private onUninstallExtension(identifier: IExtensionIdentifier): void { + const extension = this.installed.filter(e => areSameExtensions(e.identifier, identifier))[0]; + if (extension) { + const uninstalling = this.uninstalling.filter(e => areSameExtensions(e.identifier, identifier))[0] || extension; + this.uninstalling = [uninstalling, ...this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier))]; + this._onChange.fire(uninstalling); + } + } + + private onDidUninstallExtension({ identifier, error }: DidUninstallExtensionEvent): void { + if (!error) { + this.installed = this.installed.filter(e => !areSameExtensions(e.identifier, identifier)); + } + const uninstalling = this.uninstalling.filter(e => areSameExtensions(e.identifier, identifier))[0]; + this.uninstalling = this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier)); + if (uninstalling) { + this._onChange.fire(uninstalling); + } + } + + private onEnablementChanged(platformExtensions: IPlatformExtension[]) { + const extensions = this.local.filter(e => platformExtensions.some(p => areSameExtensions(e.identifier, p.identifier))); + for (const extension of extensions) { + if (extension.local) { + const enablementState = this.extensionEnablementService.getEnablementState(extension.local); + if (enablementState !== extension.enablementState) { + (extension as Extension).enablementState = enablementState; + this._onChange.fire(extension as Extension); + } + } + } + } + + getExtensionState(extension: Extension): ExtensionState { + if (extension.gallery && this.installing.some(e => !!e.gallery && areSameExtensions(e.gallery.identifier, extension.gallery!.identifier))) { + return ExtensionState.Installing; + } + if (this.uninstalling.some(e => areSameExtensions(e.identifier, extension.identifier))) { + return ExtensionState.Uninstalling; + } + const local = this.installed.filter(e => e === extension || (e.gallery && extension.gallery && areSameExtensions(e.gallery.identifier, extension.gallery.identifier)))[0]; + return local ? ExtensionState.Installed : ExtensionState.Uninstalled; + } +} + +export class ExtensionsWorkbenchService extends Disposable implements IExtensionsWorkbenchService, IURLHandler { + + private static readonly SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours + _serviceBrand: any; + + private readonly localExtensions: Extensions; + private readonly remoteExtensions: Extensions | null; private syncDelayer: ThrottledDelayer; private autoUpdateDelayer: ThrottledDelayer; private disposables: IDisposable[] = []; @@ -404,13 +569,15 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, @IStorageService private readonly storageService: IStorageService, @IFileService private readonly fileService: IFileService ) { - this.stateProvider = ext => this.getExtensionState(ext); - - extensionService.onInstallExtension(this.onInstallExtension, this, this.disposables); - extensionService.onDidInstallExtension(this.onDidInstallExtension, this, this.disposables); - extensionService.onUninstallExtension(this.onUninstallExtension, this, this.disposables); - extensionService.onDidUninstallExtension(this.onDidUninstallExtension, this, this.disposables); - extensionEnablementService.onEnablementChanged(this.onEnablementChanged, this, this.disposables); + super(); + this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer)); + this._register(this.localExtensions.onChange(e => this._onChange.fire(e))); + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer)); + this._register(this.remoteExtensions.onChange(e => this._onChange.fire(e))); + } else { + this.remoteExtensions = null; + } this.syncDelayer = new ThrottledDelayer(ExtensionsWorkbenchService.SyncPeriod); this.autoUpdateDelayer = new ThrottledDelayer(1000); @@ -438,29 +605,40 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } get local(): IExtension[] { - const installing = this.installing - .filter(e => !this.installed.some(installed => areSameExtensions(installed.identifier, e.identifier))) - .map(e => e); - - return [...this.installed, ...installing]; + const result = [...this.localExtensions.local]; + if (!this.remoteExtensions) { + return result; + } + result.push(...this.remoteExtensions.local); + const byId = groupByExtension(result, r => r.identifier); + return byId.reduce((result, extensions) => { result.push(this.getPrimaryExtension(extensions)); return result; }, []); } - queryLocal(): Promise { - return this.extensionService.getInstalled() - .then(installed => { - if (this.extensionManagementServerService.remoteExtensionManagementServer) { - installed = installed.filter(installed => this.belongsToWindow(installed)); - } - const installedById = index(this.installed, e => e.identifier.id); - this.installed = installed.map(local => { - const extension = installedById[local.identifier.id] || new Extension(this.galleryService, this.stateProvider, local, undefined, this.telemetryService, this.logService, this.fileService); - extension.enablementState = this.extensionEnablementService.getEnablementState(local); - return extension; - }); + get outdated(): IExtension[] { + const allLocal = [...this.localExtensions.local]; + if (this.remoteExtensions) { + allLocal.push(...this.remoteExtensions.local); + } + return allLocal.filter(e => e.outdated && e.local && e.state === ExtensionState.Installed); + } - this._onChange.fire(undefined); - return this.local; - }); + async queryLocal(server?: IExtensionManagementServer): Promise { + if (server) { + if (this.extensionManagementServerService.localExtensionManagementServer === server) { + return this.localExtensions.queryInstalled(); + } + if (this.remoteExtensions && this.extensionManagementServerService.remoteExtensionManagementServer === server) { + return this.remoteExtensions.queryInstalled(); + } + } + + await this.localExtensions.queryInstalled(); + if (this.remoteExtensions) { + await Promise.all([this.localExtensions.queryInstalled(), this.remoteExtensions.queryInstalled()]); + } else { + await this.localExtensions.queryInstalled(); + } + return this.local; } queryGallery(token: CancellationToken): Promise>; @@ -509,50 +687,41 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return Promise.resolve(this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), undefined, sideByside ? SIDE_GROUP : ACTIVE_GROUP)); } - private belongsToWindow(extension: ILocalExtension): boolean { - if (!this.extensionManagementServerService.remoteExtensionManagementServer) { - return true; + private getPrimaryExtension(extensions: IExtension[]): IExtension { + if (extensions.length === 1) { + return extensions[0]; } - const extensionManagementServer = this.extensionManagementServerService.getExtensionManagementServer(extension.location); - if (isUIExtension(extension.manifest, this.configurationService)) { - if (this.extensionManagementServerService.localExtensionManagementServer === extensionManagementServer) { - return true; - } - } else { - if (this.extensionManagementServerService.remoteExtensionManagementServer === extensionManagementServer) { - return true; - } - } - return false; + const pickRemoteOrFirstExtension = (from: IExtension[]): IExtension => { + const remoteExtension = from.filter(e => e.server === this.extensionManagementServerService.remoteExtensionManagementServer)[0]; + return remoteExtension ? remoteExtension : from[0]; + }; + const enabledExtensions = extensions.filter(e => e.local && this.extensionEnablementService.isEnabled(e.local)); + return enabledExtensions.length === 1 ? enabledExtensions[0] : pickRemoteOrFirstExtension(extensions); } - private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set): Extension { - let result = this.getInstalledExtensionMatchingGallery(gallery); + private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set): IExtension { + Promise.all([this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet), this.remoteExtensions ? this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false)]) + .then(result => { + if (result[0] || result[1]) { + this.eventuallyAutoUpdateExtensions(); + } + }); - if (result) { - // Loading the compatible version only there is an engine property - // Otherwise falling back to old way so that we will not make many roundtrips - if (gallery.properties.engine) { - this.galleryService.getCompatibleExtension(gallery) - .then(compatible => compatible ? this.syncLocalWithGalleryExtension(result!, compatible) : null); - } else { - this.syncLocalWithGalleryExtension(result, gallery); - } - } else { - result = new Extension(this.galleryService, this.stateProvider, undefined, gallery, this.telemetryService, this.logService, this.fileService); + const installed = this.getInstalledExtensionMatchingGallery(gallery); + if (installed) { + return installed; } - - if (maliciousExtensionSet.has(result.identifier.id)) { - result.isMalicious = true; + const extension = new Extension(this.galleryService, ext => this.getExtensionState(ext), undefined, undefined, gallery, this.telemetryService, this.logService, this.fileService); + if (maliciousExtensionSet.has(extension.identifier.id)) { + extension.isMalicious = true; } - - return result; + return extension; } - private getInstalledExtensionMatchingGallery(gallery: IGalleryExtension): Extension | null { - for (const installed of this.installed) { - if (installed.uuid) { // Installed from Gallery - if (installed.uuid === gallery.identifier.uuid) { + private getInstalledExtensionMatchingGallery(gallery: IGalleryExtension): IExtension | null { + for (const installed of this.local) { + if (installed.identifier.uuid) { // Installed from Gallery + if (installed.identifier.uuid === gallery.identifier.uuid) { return installed; } } else { @@ -564,19 +733,14 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return null; } - private syncLocalWithGalleryExtension(extension: Extension, gallery: IGalleryExtension) { - // Sync the local extension with gallery extension if local extension doesnot has metadata - if (extension.local) { - (extension.local.metadata ? Promise.resolve(extension.local) : this.extensionService.updateMetadata(extension.local, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId })) - .then(local => { - extension.local = local; - extension.gallery = gallery; - this._onChange.fire(extension); - this.eventuallyAutoUpdateExtensions(); - }); - } else { - this._onChange.fire(extension); + private getExtensionState(extension: Extension): ExtensionState { + if (this.remoteExtensions) { + const state = this.remoteExtensions.getExtensionState(extension); + if (state !== ExtensionState.Uninstalled) { + return state; + } } + return this.localExtensions.getExtensionState(extension); } checkForUpdates(): Promise { @@ -602,10 +766,10 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, private syncWithGallery(): Promise { const ids: string[] = [], names: string[] = []; - for (const installed of this.installed) { + for (const installed of this.local) { if (installed.type === ExtensionType.User) { - if (installed.uuid) { - ids.push(installed.uuid); + if (installed.identifier.uuid) { + ids.push(installed.identifier.uuid); } else { names.push(installed.identifier.id); } @@ -634,9 +798,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } // {{SQL CARBON EDIT}} - Add && !e.downloadPage condition - const toUpdate = this.local.filter(e => - e.outdated && e.state !== ExtensionState.Installing - && e.local && !this.isAutoUpdateIgnored(new ExtensionIdentifierWithVersion(e.identifier, e.version)) && !e.downloadPage); + const toUpdate = this.outdated.filter(e => !this.isAutoUpdateIgnored(new ExtensionIdentifierWithVersion(e.identifier, e.version)) && !e.downloadPage); return Promise.all(toUpdate.map(e => this.install(e))); } @@ -689,7 +851,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, // The check is added here because we want to fail fast instead of downloading the VSIX and then fail. if (gallery.properties.engine && (!isEngineValid(gallery.properties.engine, product.vscodeVersion) || (gallery.properties.azDataEngine && !isEngineValid(gallery.properties.azDataEngine, pkg.version)))) { - return Promise.reject(new ExtensionManagementError(nls.localize('incompatible', "Unable to install version '{2}' of extension '{0}' as it is not compatible with Azure Data Studio '{1}'.", extension.gallery!.identifier.id, pkg.version, gallery.version), INSTALL_ERROR_INCOMPATIBLE)); + return Promise.reject(new ExtensionManagementError(nls.localize('incompatible2', "Unable to install version '{2}' of extension '{0}' as it is not compatible with Azure Data Studio '{1}'.", extension.gallery!.identifier.id, pkg.version, gallery.version), INSTALL_ERROR_INCOMPATIBLE)); } return this.installWithProgress(async () => { @@ -723,14 +885,14 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } uninstall(extension: IExtension): Promise { - const ext = extension.local ? extension : this.installed.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; + const ext = extension.local ? extension : this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; const toUninstall: ILocalExtension | null = ext && ext.local ? ext.local : null; if (!toUninstall) { return Promise.reject(new Error('Missing local')); } - this.logService.info(`Requested uninstalling the extension ${extension.identifier.id} from window ${this.windowService.getCurrentWindowId()}`); + this.logService.info(`Requested uninstalling the extension ${extension.identifier.id} from window ${this.windowService.windowId}`); return this.progressService.withProgress({ location: ProgressLocation.Extensions, title: nls.localize('uninstallingExtension', 'Uninstalling extension....'), @@ -765,7 +927,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, } reinstall(extension: IExtension): Promise { - const ext = extension.local ? extension : this.installed.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; + const ext = extension.local ? extension : this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; const toReinstall: ILocalExtension | null = ext && ext.local ? ext.local : null; if (!toReinstall) { @@ -925,101 +1087,6 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, return this._extensionAllowedBadgeProviders; } - private onInstallExtension(event: InstallExtensionEvent): void { - const { gallery } = event; - - if (!gallery) { - return; - } - - let extension = this.installed.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0]; - - if (!extension) { - extension = new Extension(this.galleryService, this.stateProvider, undefined, gallery, this.telemetryService, this.logService, this.fileService); - } - - this.installing.push(extension); - - this._onChange.fire(extension); - } - - private onDidInstallExtension(event: DidInstallExtensionEvent): void { - const { local, zipPath, error, gallery } = event; - const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0] : null; - this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; - - if (local && !this.belongsToWindow(local)) { - return; - } - - let extension: Extension | undefined = installingExtension ? installingExtension : zipPath ? new Extension(this.galleryService, this.stateProvider, local, undefined, this.telemetryService, this.logService, this.fileService) : undefined; - if (extension) { - if (local) { - const installed = this.installed.filter(e => areSameExtensions(e.identifier, extension!.identifier))[0]; - if (installed) { - extension = installed; - } else { - this.installed.push(extension); - } - extension.local = local; - extension.gallery = gallery; - } - } - this._onChange.fire(error ? undefined : extension); - } - - private onUninstallExtension(identifier: IExtensionIdentifier): void { - const extension = this.installed.filter(e => areSameExtensions(e.identifier, identifier))[0]; - if (!extension) { - return; - } - - const uninstalling = this.uninstalling.filter(e => areSameExtensions(e.identifier, identifier))[0] || extension; - this.uninstalling = [uninstalling, ...this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier))]; - - this._onChange.fire(uninstalling); - } - - private onDidUninstallExtension({ identifier, error }: DidUninstallExtensionEvent): void { - if (!error) { - this.installed = this.installed.filter(e => !areSameExtensions(e.identifier, identifier)); - } - - const uninstalling = this.uninstalling.filter(e => areSameExtensions(e.identifier, identifier))[0]; - this.uninstalling = this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier)); - if (!uninstalling) { - return; - } - - this._onChange.fire(uninstalling); - } - - private onEnablementChanged(platformExtensions: IPlatformExtension[]) { - const extensions = this.local.filter(e => platformExtensions.some(p => areSameExtensions(e.identifier, p.identifier))); - for (const extension of extensions) { - if (extension.local) { - const enablementState = this.extensionEnablementService.getEnablementState(extension.local); - if (enablementState !== extension.enablementState) { - extension.enablementState = enablementState; - this._onChange.fire(extension); - } - } - } - } - - private getExtensionState(extension: Extension): ExtensionState { - if (extension.gallery && this.installing.some(e => !!e.gallery && areSameExtensions(e.gallery.identifier, extension.gallery!.identifier))) { - return ExtensionState.Installing; - } - - if (this.uninstalling.some(e => areSameExtensions(e.identifier, extension.identifier))) { - return ExtensionState.Uninstalling; - } - - const local = this.installed.filter(e => e === extension || (e.gallery && extension.gallery && areSameExtensions(e.gallery.identifier, extension.gallery.identifier)))[0]; - return local ? ExtensionState.Installed : ExtensionState.Uninstalled; - } - private onError(err: any): void { if (isPromiseCanceledError(err)) { return; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 1d8e043978..40d7ff931e 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -6,12 +6,12 @@ import * as assert from 'assert'; import { assign } from 'vs/base/common/objects'; import { generateUuid } from 'vs/base/common/uuid'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtensionsWorkbenchService, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; import * as ExtensionsActions from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/node/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, IExtensionEnablementService, IExtensionTipsService, ILocalExtension, IGalleryExtension, - DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, EnablementState, InstallOperation, IExtensionManagementServerService + DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, EnablementState, InstallOperation, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/platform/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -33,12 +33,13 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { URLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/electron-browser/extensionManagementServerService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { ExtensionIdentifier, IExtensionContributions, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/electron-browser/extensionManagementServerService'; suite('ExtensionsActions Test', () => { @@ -74,9 +75,17 @@ suite('ExtensionsActions Test', () => { instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); instantiationService.stub(IRemoteAgentService, RemoteAgentService); - instantiationService.stub(IExtensionManagementServerService, instantiationService.createInstance(ExtensionManagementServerService)); + instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { + private _localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', authority: 'vscode-local' }; + constructor() { + super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService)); + } + get localExtensionManagementServer(): IExtensionManagementServer { return this._localExtensionManagementServer; } + set localExtensionManagementServer(server: IExtensionManagementServer) { } + }()); instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); + instantiationService.stub(ILabelService, { onDidChangeFormatters: new Emitter().event }); instantiationService.set(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); instantiationService.stub(IURLService, URLService); @@ -105,7 +114,7 @@ suite('ExtensionsActions Test', () => { test('Test Install action when state is installed', () => { const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return workbenchService.queryLocal() @@ -124,7 +133,7 @@ suite('ExtensionsActions Test', () => { test('Test Install action when state is installing', () => { const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); return workbenchService.queryGallery(CancellationToken.None) @@ -141,7 +150,7 @@ suite('ExtensionsActions Test', () => { test('Test Install action when state is uninstalled', () => { const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); return workbenchService.queryGallery(CancellationToken.None) @@ -154,7 +163,7 @@ suite('ExtensionsActions Test', () => { test('Test Install action when extension is system action', () => { const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', {}, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -169,7 +178,7 @@ suite('ExtensionsActions Test', () => { test('Test Install action when extension doesnot has gallery', () => { const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -184,14 +193,14 @@ suite('ExtensionsActions Test', () => { test('Uninstall action is disabled when there is no extension', () => { const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); assert.ok(!testObject.enabled); }); test('Test Uninstall action when state is uninstalling', () => { const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -207,7 +216,7 @@ suite('ExtensionsActions Test', () => { test('Test Uninstall action when state is installed and is user extension', () => { const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -222,7 +231,7 @@ suite('ExtensionsActions Test', () => { test('Test Uninstall action when state is installed and is system extension', () => { const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', {}, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -237,7 +246,7 @@ suite('ExtensionsActions Test', () => { test('Test Uninstall action when state is installing and is user extension', () => { const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -254,7 +263,7 @@ suite('ExtensionsActions Test', () => { test('Test Uninstall action after extension is installed', () => { const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -273,7 +282,7 @@ suite('ExtensionsActions Test', () => { test('Test CombinedInstallAction when there is no extension', () => { const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); assert.ok(!testObject.enabled); assert.equal('extension-action prominent install no-extension', testObject.class); @@ -281,7 +290,7 @@ suite('ExtensionsActions Test', () => { test('Test CombinedInstallAction when extension is system extension', () => { const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', {}, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -296,7 +305,7 @@ suite('ExtensionsActions Test', () => { test('Test CombinedInstallAction when installAction is enabled', () => { const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -311,7 +320,7 @@ suite('ExtensionsActions Test', () => { test('Test CombinedInstallAction when unInstallAction is enabled', () => { const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -326,7 +335,7 @@ suite('ExtensionsActions Test', () => { test('Test CombinedInstallAction when state is installing', () => { const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -343,7 +352,7 @@ suite('ExtensionsActions Test', () => { test('Test CombinedInstallAction when state is installing during update', () => { const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -362,7 +371,7 @@ suite('ExtensionsActions Test', () => { test('Test CombinedInstallAction when state is uninstalling', () => { const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -378,14 +387,14 @@ suite('ExtensionsActions Test', () => { test('Test UpdateAction when there is no extension', () => { const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); assert.ok(!testObject.enabled); }); test('Test UpdateAction when extension is uninstalled', () => { const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a', { version: '1.0.0' }); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) @@ -397,7 +406,7 @@ suite('ExtensionsActions Test', () => { test('Test UpdateAction when extension is installed and not outdated', () => { const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', { version: '1.0.0' }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -412,7 +421,7 @@ suite('ExtensionsActions Test', () => { test('Test UpdateAction when extension is installed outdated and system extension', () => { const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', { version: '1.0.0' }, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -427,7 +436,7 @@ suite('ExtensionsActions Test', () => { test('Test UpdateAction when extension is installed outdated and user extension', () => { const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', { version: '1.0.0' }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -450,7 +459,7 @@ suite('ExtensionsActions Test', () => { test('Test UpdateAction when extension is installing and outdated and user extension', () => { const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', { version: '1.0.0' }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -469,14 +478,14 @@ suite('ExtensionsActions Test', () => { test('Test ManageExtensionAction when there is no extension', () => { const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); assert.ok(!testObject.enabled); }); test('Test ManageExtensionAction when extension is installed', () => { const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -491,7 +500,7 @@ suite('ExtensionsActions Test', () => { test('Test ManageExtensionAction when extension is uninstalled', () => { const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -506,7 +515,7 @@ suite('ExtensionsActions Test', () => { test('Test ManageExtensionAction when extension is installing', () => { const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -523,7 +532,7 @@ suite('ExtensionsActions Test', () => { test('Test ManageExtensionAction when extension is queried from gallery and installed', () => { const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -541,7 +550,7 @@ suite('ExtensionsActions Test', () => { test('Test ManageExtensionAction when extension is system extension', () => { const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', {}, { type: ExtensionType.System }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -556,7 +565,7 @@ suite('ExtensionsActions Test', () => { test('Test ManageExtensionAction when extension is uninstalling', () => { const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -767,7 +776,7 @@ suite('ExtensionsActions Test', () => { .then(page => { const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); testObject.extension = page.firstPage[0]; - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); installEvent.fire({ identifier: gallery.identifier, gallery }); assert.ok(!testObject.enabled); @@ -951,7 +960,7 @@ suite('ExtensionsActions Test', () => { .then(page => { const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); testObject.extension = page.firstPage[0]; - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); installEvent.fire({ identifier: gallery.identifier, gallery }); assert.ok(!testObject.enabled); }); @@ -965,7 +974,7 @@ suite('ExtensionsActions Test', () => { .then(extensions => { const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); testObject.extension = extensions[0]; - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); uninstallEvent.fire(local.identifier); assert.ok(!testObject.enabled); }); @@ -1046,14 +1055,14 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when there is no extension', () => { const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); assert.ok(!testObject.enabled); }); test('Test ReloadAction when extension state is installing', () => { const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1068,7 +1077,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when extension state is uninstalling', () => { const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1083,7 +1092,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when extension is newly installed', async () => { instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.b'), extensionLocation: URI.file('pub.b') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1094,7 +1103,7 @@ suite('ExtensionsActions Test', () => { return new Promise(c => { testObject.onDidChange(() => { // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - if (testObject.enabled && testObject.tooltip === 'Please reload Azure Data Studio to complete the installation of this extension.') { + if (testObject.enabled && testObject.tooltip === 'Please reload Azure Data Studio to enable this extension.') { c(); } }); @@ -1106,7 +1115,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when extension is installed and uninstalled', () => { instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.b'), extensionLocation: URI.file('pub.b') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) @@ -1123,9 +1132,9 @@ suite('ExtensionsActions Test', () => { }); test('Test ReloadAction when extension is uninstalled', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); + instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a'), version: '1.0.0' }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); @@ -1146,7 +1155,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when extension is uninstalled and installed', () => { instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.a'), version: '1.0.0', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return instantiationService.get(IExtensionsWorkbenchService).queryLocal() @@ -1167,7 +1176,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when extension is updated while running', async () => { instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.a'), version: '1.0.1', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', { version: '1.0.1' }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1177,7 +1186,7 @@ suite('ExtensionsActions Test', () => { return new Promise(c => { testObject.onDidChange(() => { // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - if (testObject.enabled && testObject.tooltip === 'Please reload Azure Data Studio to complete the updating of this extension.') { + if (testObject.enabled && testObject.tooltip === 'Please reload Azure Data Studio to enable the updated extension.') { c(); } }); @@ -1193,7 +1202,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionEnablementService).setEnablement([local], EnablementState.Disabled) .then(() => { const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return workbenchService.queryLocal() @@ -1212,7 +1221,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when extension is disabled when running', () => { instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1223,7 +1232,7 @@ suite('ExtensionsActions Test', () => { .then(() => { assert.ok(testObject.enabled); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - assert.equal('Please reload Azure Data Studio to complete the disabling of this extension.', testObject.tooltip); + assert.equal('Please reload Azure Data Studio to disable this extension.', testObject.tooltip); }); }); }); @@ -1231,7 +1240,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when extension enablement is toggled when running', () => { instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.a'), version: '1.0.0', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); @@ -1250,7 +1259,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionEnablementService).setEnablement([local], EnablementState.Disabled) .then(() => { const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return workbenchService.queryLocal() @@ -1261,7 +1270,7 @@ suite('ExtensionsActions Test', () => { .then(() => { assert.ok(testObject.enabled); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - assert.equal('Please reload Azure Data Studio to complete the enabling of this extension.', testObject.tooltip); + assert.equal('Please reload Azure Data Studio to enable this extension.', testObject.tooltip); }); }); }); @@ -1273,7 +1282,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionEnablementService).setEnablement([local], EnablementState.Disabled) .then(() => { const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return workbenchService.queryLocal() @@ -1292,7 +1301,7 @@ suite('ExtensionsActions Test', () => { return instantiationService.get(IExtensionEnablementService).setEnablement([local], EnablementState.Disabled) .then(() => { const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return workbenchService.queryLocal() @@ -1307,7 +1316,7 @@ suite('ExtensionsActions Test', () => { .then(() => { assert.ok(testObject.enabled); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - assert.equal('Please reload Azure Data Studio to complete the enabling of this extension.', testObject.tooltip); + assert.equal('Please reload Azure Data Studio to enable this extension.', testObject.tooltip); }); }); @@ -1317,7 +1326,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when a localization extension is newly installed', async () => { instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.b'), extensionLocation: URI.file('pub.b') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1333,7 +1342,7 @@ suite('ExtensionsActions Test', () => { test('Test ReloadAction when a localization extension is updated while running', async () => { instantiationService.stubPromise(IExtensionService, 'getExtensions', [{ identifier: new ExtensionIdentifier('pub.a'), version: '1.0.1', extensionLocation: URI.file('pub.a') }]); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); - instantiationService.get(IExtensionsWorkbenchService).onChange(() => testObject.update()); + instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', { version: '1.0.1', contributes: { localizations: [{ languageId: 'de', translations: [] }] } }); const workbenchService = instantiationService.get(IExtensionsWorkbenchService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index d75db0ac0a..716bbbf968 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -37,7 +37,6 @@ import { SinonStub } from 'sinon'; import { IExperimentService, ExperimentService, ExperimentState, ExperimentActionType } from 'vs/workbench/contrib/experiments/node/experimentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; -import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/electron-browser/extensionManagementServerService'; import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; @@ -90,7 +89,11 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); instantiationService.stub(IRemoteAgentService, RemoteAgentService); - instantiationService.stub(IExtensionManagementServerService, instantiationService.createInstance(ExtensionManagementServerService)); + instantiationService.stub(IExtensionManagementServerService, { + localExtensionManagementServer: { + extensionManagementService: instantiationService.get(IExtensionManagementService) + } + }); instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index b4f042408a..944353dcd8 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -37,7 +37,6 @@ import { URLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/electron-browser/extensionManagementServerService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; @@ -77,7 +76,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { }); instantiationService.stub(IRemoteAgentService, RemoteAgentService); - instantiationService.stub(IExtensionManagementServerService, instantiationService.createInstance(ExtensionManagementServerService)); instantiationService.stub(IExtensionManagementService, ExtensionManagementService); instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); @@ -85,6 +83,12 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionManagementServerService, { + localExtensionManagementServer: { + extensionManagementService: instantiationService.get(IExtensionManagementService) + } + }); + instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); instantiationService.set(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); diff --git a/src/vs/workbench/contrib/feedback/electron-browser/feedback.ts b/src/vs/workbench/contrib/feedback/electron-browser/feedback.ts index 704362a938..e17d5c32e1 100644 --- a/src/vs/workbench/contrib/feedback/electron-browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/electron-browser/feedback.ts @@ -310,6 +310,12 @@ export class FeedbackDropdown extends Dropdown { }; } + private updateFeedbackDescription() { + if (this.feedbackDescriptionInput && this.feedbackDescriptionInput.textLength > this.maxFeedbackCharacters) { + this.feedbackDescriptionInput.value = this.feedbackDescriptionInput.value.substring(0, this.maxFeedbackCharacters); + } + } + private getCharCountText(charCount: number): string { const remaining = this.maxFeedbackCharacters - charCount; const text = (remaining === 1) @@ -349,6 +355,7 @@ export class FeedbackDropdown extends Dropdown { this.sentiment = smile ? 1 : 0; this.maxFeedbackCharacters = this.feedbackDelegate.getCharacterLimit(this.sentiment); + this.updateFeedbackDescription(); this.updateCharCountText(); if (this.feedbackDescriptionInput) { this.feedbackDescriptionInput.maxLength = this.maxFeedbackCharacters; diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 87677fc4de..550d5be5c7 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -186,7 +186,7 @@ export class TextFileEditor extends BaseTextEditor { return Promise.reject(createErrorWithActions(toErrorMessage(error), { actions: [ new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, () => { - return this.fileService.updateContent(input.getResource(), '').then(() => this.editorService.openEditor({ + return this.textFileService.create(input.getResource()).then(() => this.editorService.openEditor({ resource: input.getResource(), options: { pinned: true // new file gets pinned by default diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 17184f2c09..ff63c65566 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -968,6 +968,7 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole const listService = accessor.get(IListService); const explorerService = accessor.get(IExplorerService); const fileService = accessor.get(IFileService); + const textFileService = accessor.get(ITextFileService); const editorService = accessor.get(IEditorService); const viewletService = accessor.get(IViewletService); const activeViewlet = viewletService.getActiveViewlet(); @@ -995,7 +996,7 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole folder.addChild(newStat); const onSuccess = async (value: string) => { - const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : fileService.createFile(resources.joinPath(folder.resource, value)); + const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : textFileService.create(resources.joinPath(folder.resource, value)); return createPromise.then(created => { refreshIfSeparator(value, explorerService); return isFolder ? explorerService.select(created.resource, true) diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 05f0102f86..5317b725a8 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -40,8 +40,9 @@ import { IEditorService, SIDE_GROUP, IResourceEditorReplacement } from 'vs/workb import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { basename } from 'vs/base/common/resources'; +import { basename, toLocalResource } from 'vs/base/common/resources'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; // {{SQL CARBON EDIT}} import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; @@ -104,7 +105,8 @@ function save( untitledEditorService: IUntitledEditorService, textFileService: ITextFileService, editorGroupService: IEditorGroupsService, - queryEditorService: IQueryEditorService + queryEditorService: IQueryEditorService, + environmentService: IWorkbenchEnvironmentService ): Promise { function ensureForcedSave(options?: ISaveOptions): ISaveOptions { @@ -146,9 +148,9 @@ function save( // Special case: an untitled file with associated path gets saved directly unless "saveAs" is true let savePromise: Promise; if (!isSaveAs && resource.scheme === Schemas.untitled && untitledEditorService.hasAssociatedFilePath(resource)) { - savePromise = textFileService.save(resource, options).then((result) => { + savePromise = textFileService.save(resource, options).then(result => { if (result) { - return resource.with({ scheme: Schemas.file }); + return toLocalResource(resource, environmentService.configuration.remoteAuthority); } return undefined; @@ -165,7 +167,7 @@ function save( savePromise = textFileService.saveAs(resource, undefined, options); } - return savePromise.then((target) => { + return savePromise.then(target => { if (!target || target.toString() === resource.toString()) { return false; // save canceled or same resource used } @@ -238,7 +240,7 @@ function saveAll(saveAllArguments: any, editorService: IEditorService, untitledE }); // Save all - return textFileService.saveAll(saveAllArguments).then((result) => { + return textFileService.saveAll(saveAllArguments).then(result => { groupIdToUntitledResourceInput.forEach((inputs, groupId) => { // {{SQL CARBON EDIT}} // Update untitled resources to the saved ones, so we open the proper files @@ -325,6 +327,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const instantiationService = accessor.get(IInstantiationService); const textModelService = accessor.get(ITextModelService); const editorService = accessor.get(IEditorService); + const fileService = accessor.get(IFileService); // Register provider at first as needed let registerEditorListener = false; @@ -336,9 +339,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ providerDisposables.push(textModelService.registerTextModelContentProvider(COMPARE_WITH_SAVED_SCHEMA, provider)); } - // Open editor (only files supported) + // Open editor (only resources that can be handled by file service are supported) const uri = getResourceForCommand(resource, accessor.get(IListService), editorService); - if (uri && uri.scheme === Schemas.file /* only files on disk supported for now */) { + if (uri && fileService.canHandleResource(uri)) { const name = basename(uri); const editorLabel = nls.localize('modifiedLabel', "{0} (on disk) ↔ {1}", name, name); @@ -542,7 +545,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } // {{SQL CARBON EDIT}} - return save(resource, true, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService)); + return save(resource, true, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService), accessor.get(IWorkbenchEnvironmentService)); } }); @@ -558,7 +561,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (resources.length === 1) { // If only one resource is selected explictly call save since the behavior is a bit different than save all #41841 // {{SQL CARBON EDIT}} - return save(resources[0], false, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService)); + return save(resources[0], false, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService), accessor.get(IWorkbenchEnvironmentService)); } return saveAll(resources, editorService, accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); } @@ -576,7 +579,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const resource = toResource(editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); if (resource) { // {{SQL CARBON EDIT}} - return save(resource, false, { skipSaveParticipants: true }, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService)); + return save(resource, false, { skipSaveParticipants: true }, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService), accessor.get(IWorkbenchEnvironmentService)); } return undefined; diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts index 26a1f62d5e..40acccd48a 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts @@ -32,6 +32,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { Event } from 'vs/base/common/event'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { isWindows } from 'vs/base/common/platform'; +import { Schemas } from 'vs/base/common/network'; export const CONFLICT_RESOLUTION_CONTEXT = 'saveConflictResolutionContext'; export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution'; @@ -136,13 +137,13 @@ export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, I const triedToMakeWriteable = isReadonly && fileOperationError.options && fileOperationError.options.overwriteReadonly; const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; - // Save Elevated - if (isPermissionDenied || triedToMakeWriteable) { + // Save Elevated (TODO@remote cannot write elevated https://github.com/Microsoft/vscode/issues/48659) + if (resource.scheme === Schemas.file && (isPermissionDenied || triedToMakeWriteable)) { actions.primary!.push(this.instantiationService.createInstance(SaveElevatedAction, model, triedToMakeWriteable)); } - // Overwrite - else if (isReadonly) { + // Overwrite (TODO@remote cannot overwrite readonly https://github.com/Microsoft/vscode/issues/48659) + else if (resource.scheme === Schemas.file && isReadonly) { actions.primary!.push(this.instantiationService.createInstance(OverwriteReadonlyAction, model)); } diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index 0726da889b..c8b8c2fb75 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { memoize } from 'vs/base/common/decorators'; import { basename } from 'vs/base/common/path'; -import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; +import { dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { EncodingMode, ConfirmResult, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity, IRevertOptions } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; @@ -118,7 +118,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { getName(): string { if (!this.name) { - this.name = basenameOrAuthority(this.resource); + this.name = basename(this.labelService.getUriLabel(this.resource)); } return this.decorateLabel(this.name); @@ -195,6 +195,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { if (model && model.hasState(ModelState.ORPHAN)) { return localize('orphanedFile', "{0} (deleted from disk)", label); } + if (model && model.isReadonly()) { return localize('readonlyFile', "{0} (read-only)", label); } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 03783ec381..553874e531 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -23,6 +23,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { once } from 'vs/base/common/functional'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { toLocalResource } from 'vs/base/common/resources'; /** * Explorer viewlet id. @@ -138,12 +140,13 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { @ITextFileService private readonly textFileService: ITextFileService, @IFileService private readonly fileService: IFileService, @IModeService private readonly modeService: IModeService, - @IModelService private readonly modelService: IModelService + @IModelService private readonly modelService: IModelService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { } provideTextContent(resource: URI): Promise { - const fileOnDiskResource = resource.with({ scheme: Schemas.file }); + const savedFileResource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); // Make sure our file from disk is resolved up to date return this.resolveEditorModel(resource).then(codeEditorModel => { @@ -151,7 +154,7 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { // Make sure to keep contents on disk up to date when it changes if (!this.fileWatcherDisposable) { this.fileWatcherDisposable = this.fileService.onFileChanges(changes => { - if (changes.contains(fileOnDiskResource, FileChangeType.UPDATED)) { + if (changes.contains(savedFileResource, FileChangeType.UPDATED)) { this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes } }); @@ -171,20 +174,20 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { private resolveEditorModel(resource: URI, createAsNeeded?: true): Promise; private resolveEditorModel(resource: URI, createAsNeeded?: boolean): Promise; private resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise { - const fileOnDiskResource = resource.with({ scheme: Schemas.file }); + const savedFileResource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); - return this.textFileService.resolveTextContent(fileOnDiskResource).then(content => { + return this.textFileService.resolve(savedFileResource).then(content => { let codeEditorModel = this.modelService.getModel(resource); if (codeEditorModel) { this.modelService.updateModel(codeEditorModel, content.value); } else if (createAsNeeded) { - const fileOnDiskModel = this.modelService.getModel(fileOnDiskResource); + const fileOnDiskModel = this.modelService.getModel(savedFileResource); let languageSelector: ILanguageSelection; if (fileOnDiskModel) { languageSelector = this.modeService.create(fileOnDiskModel.getModeId()); } else { - languageSelector = this.modeService.createByFilepathOrFirstLine(fileOnDiskResource.fsPath); + languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.fsPath); } codeEditorModel = this.modelService.createModel(content.value, languageSelector, resource); diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 08a664ada5..4963549901 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -36,7 +36,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { static configName = 'editor.defaultFormatter'; - static extensionIds: string[] = []; + static extensionIds: (string | null)[] = []; static extensionDescriptions: string[] = []; constructor( @@ -60,6 +60,10 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { DefaultFormatter.extensionIds.length = 0; DefaultFormatter.extensionDescriptions.length = 0; + + DefaultFormatter.extensionIds.push(null); + DefaultFormatter.extensionDescriptions.push(nls.localize('nullFormatterDescription', "None")); + for (const extension of extensions) { if (extension.main) { DefaultFormatter.extensionIds.push(extension.identifier.value); @@ -127,7 +131,8 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { const picks = formatter.map((formatter, index) => { return { index, - label: formatter.displayName || formatter.extensionId || '?' + label: formatter.displayName || formatter.extensionId || '?', + description: formatter.extensionId && formatter.extensionId.value }; }); const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId(); @@ -156,7 +161,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis properties: { [DefaultFormatter.configName]: { description: nls.localize('formatter.default', "Defines a default formatter which takes precedence over all other formatter settings. Must be the identifier of an extension contributing a formatter."), - type: 'string', + type: ['string', 'null'], default: null, enum: DefaultFormatter.extensionIds, markdownEnumDescriptions: DefaultFormatter.extensionDescriptions @@ -253,8 +258,8 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { const model = editor.getModel(); const provider = getRealAndSyntheticDocumentFormattersOrdered(model); const pick = await instaService.invokeFunction(showFormatterPick, model, provider); - if (pick) { - await instaService.invokeFunction(formatDocumentWithProvider, provider[pick], editor, CancellationToken.None); + if (typeof pick === 'number') { + await instaService.invokeFunction(formatDocumentWithProvider, provider[pick], editor, FormattingMode.Explicit, CancellationToken.None); } logFormatterTelemetry(telemetryService, 'document', provider, typeof pick === 'number' && provider[pick] || undefined); } @@ -291,7 +296,7 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model); const pick = await instaService.invokeFunction(showFormatterPick, model, provider); - if (pick) { + if (typeof pick === 'number') { await instaService.invokeFunction(formatDocumentRangeWithProvider, provider[pick], editor, range, CancellationToken.None); } diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts index d56c9289d7..6b6c9a675c 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts @@ -11,8 +11,8 @@ import { IExtensionManagementService, IExtensionEnablementService } from 'vs/pla import { webFrame } from 'electron'; import { assign } from 'vs/base/common/objects'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; -import { IWindowService } from 'vs/platform/windows/common/windows'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class WorkbenchIssueService implements IWorkbenchIssueService { _serviceBrand: any; @@ -22,7 +22,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IThemeService private readonly themeService: IThemeService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService, - @IWindowService private readonly windowService: IWindowService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { } openReporter(dataOverrides: Partial = {}): Promise { @@ -60,7 +60,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { openProcessExplorer(): Promise { const theme = this.themeService.getTheme(); const data: ProcessExplorerData = { - pid: this.windowService.getConfiguration().mainPid, + pid: this.environmentService.configuration.mainPid, zoomLevel: webFrame.getZoomLevel(), styles: { backgroundColor: getColor(theme, editorBackground), diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 77d624ed67..b98d27561a 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -9,8 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExt, } from 'vs/workbench/contrib/output/common/output'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; @@ -22,15 +21,14 @@ import { ILogService, LogLevel } from 'vs/platform/log/common/log'; class LogOutputChannels extends Disposable implements IWorkbenchContribution { constructor( - @IWindowService windowService: IWindowService, - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @ILogService logService: ILogService ) { super(); let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); outputChannelRegistry.registerChannel({ id: Constants.mainLogChannelId, label: nls.localize('mainLog', "Main"), file: URI.file(join(environmentService.logsPath, `main.log`)), log: true }); outputChannelRegistry.registerChannel({ id: Constants.sharedLogChannelId, label: nls.localize('sharedLog', "Shared"), file: URI.file(join(environmentService.logsPath, `sharedprocess.log`)), log: true }); - outputChannelRegistry.registerChannel({ id: Constants.rendererLogChannelId, label: nls.localize('rendererLog', "Window"), file: URI.file(join(environmentService.logsPath, `renderer${windowService.getCurrentWindowId()}.log`)), log: true }); + outputChannelRegistry.registerChannel({ id: Constants.rendererLogChannelId, label: nls.localize('rendererLog', "Window"), file: URI.file(join(environmentService.logsPath, `renderer${environmentService.configuration.windowId}.log`)), log: true }); // {{SQL CARBON EDIT}} let toolsServiceLogFile : string = join(environmentService.logsPath, '..', '..', 'mssql', `sqltools_${Date.now()}.log`); diff --git a/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts index 9e5d40b36f..e1c96e7bb7 100644 --- a/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts @@ -13,8 +13,8 @@ import * as nls from 'vs/nls'; import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { WorkbenchStateContext, IsRemoteContext } from 'vs/workbench/common/contextkeys'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { WorkbenchStateContext, RemoteAuthorityContext } from 'vs/workbench/common/contextkeys'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -40,7 +40,6 @@ import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEdito import { ExplorerRootContext, ExplorerFolderContext } from 'vs/workbench/contrib/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWindowService } from 'vs/platform/windows/common/windows'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; registerSingleton(IPreferencesSearchService, PreferencesSearchService, true); @@ -387,12 +386,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ class PreferencesActionsContribution extends Disposable implements IWorkbenchContribution { constructor( - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IWorkspaceContextService private readonly workpsaceContextService: IWorkspaceContextService, @ILabelService labelService: ILabelService, @IExtensionService extensionService: IExtensionService, - @IWindowService windowService: IWindowService ) { super(); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { @@ -431,7 +429,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon extensionService.whenInstalledExtensionsRegistered() .then(() => { - const remoteAuthority = windowService.getConfiguration().remoteAuthority; + const remoteAuthority = environmentService.configuration.remoteAuthority; const hostLabel = labelService.getHostLabel(REMOTE_HOST_SCHEME, remoteAuthority) || remoteAuthority; const label = nls.localize('openRemoteSettings', "Open User Settings ({0})", hostLabel); CommandsRegistry.registerCommand(OpenRemoteSettingsAction.ID, serviceAccessor => { @@ -443,7 +441,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon title: { value: label, original: `Preferences: Open User Settings (${hostLabel})` }, category: nls.localize('preferencesCategory', "Preferences") }, - when: IsRemoteContext + when: RemoteAuthorityContext.notEqualsTo('') }); }); } diff --git a/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts index 4adb3abd2f..44168d3924 100644 --- a/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts @@ -1211,7 +1211,8 @@ export class SettingsEditor2 extends BaseEditor { } private renderResultCountMessages() { - if (!this.currentSettingsModel) { + if (!this.currentSettingsModel || !this.searchResultModel) { + this.countElement.style.display = 'none'; return; } diff --git a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts index ee2adb178c..894c8ec196 100644 --- a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts @@ -377,7 +377,8 @@ export class CommandsHandler extends QuickOpenHandler { private commandHistoryEnabled: boolean; private commandsHistory: CommandsHistory; - private extensionsRegistered: boolean; + + private waitedForExtensionsRegistered: boolean; constructor( @IEditorService private readonly editorService: IEditorService, @@ -391,7 +392,7 @@ export class CommandsHandler extends QuickOpenHandler { this.commandsHistory = this.instantiationService.createInstance(CommandsHistory); - this.extensionService.whenInstalledExtensionsRegistered().then(() => this.extensionsRegistered = true); + this.extensionService.whenInstalledExtensionsRegistered().then(() => this.waitedForExtensionsRegistered = true); this.configurationService.onDidChangeConfiguration(e => this.updateConfiguration()); this.updateConfiguration(); @@ -402,7 +403,7 @@ export class CommandsHandler extends QuickOpenHandler { } getResults(searchValue: string, token: CancellationToken): Promise { - if (this.extensionsRegistered) { + if (this.waitedForExtensionsRegistered) { return this.doGetResults(searchValue, token); } @@ -410,7 +411,11 @@ export class CommandsHandler extends QuickOpenHandler { // a chance to register so that the complete set of commands shows up as result // We do not want to delay functionality beyond that time though to keep the commands // functional. - return Promise.race([timeout(800), this.extensionService.whenInstalledExtensionsRegistered().then(() => undefined)]).then(() => this.doGetResults(searchValue, token)); + return Promise.race([timeout(800), this.extensionService.whenInstalledExtensionsRegistered().then(() => undefined)]).then(() => { + this.waitedForExtensionsRegistered = true; + + return this.doGetResults(searchValue, token); + }); } private doGetResults(searchValue: string, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/relauncher/electron-browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/electron-browser/relauncher.contribution.ts index f60c2f0d86..3da9444617 100644 --- a/src/vs/workbench/contrib/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/electron-browser/relauncher.contribution.ts @@ -18,6 +18,7 @@ import { isEqual } from 'vs/base/common/resources'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; interface IConfiguration extends IWindowsConfiguration { update: { mode: string; }; @@ -151,7 +152,7 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IExtensionService extensionService: IExtensionService, @IWindowService windowSevice: IWindowService, - @IEnvironmentService environmentService: IEnvironmentService + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { super(); @@ -159,7 +160,7 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor if (!!environmentService.extensionTestsLocationURI) { return; // no restart when in tests: see https://github.com/Microsoft/vscode/issues/66936 } - if (windowSevice.getConfiguration().remoteAuthority) { + if (environmentService.configuration.remoteAuthority) { windowSevice.reloadWindow(); // TODO aeschli, workaround } else { extensionService.restartExtensionHost(); diff --git a/src/vs/workbench/contrib/search/browser/openAnythingHandler.ts b/src/vs/workbench/contrib/search/browser/openAnythingHandler.ts index 18612dd172..94a7f0b8de 100644 --- a/src/vs/workbench/contrib/search/browser/openAnythingHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openAnythingHandler.ts @@ -32,7 +32,7 @@ export class OpenAnythingHandler extends QuickOpenHandler { static readonly ID = 'workbench.picker.anything'; - private static readonly LINE_COLON_PATTERN = /[#:\(](\d*)([#:,](\d*))?\)?$/; + private static readonly LINE_COLON_PATTERN = /[#:\(](\d*)([#:,](\d*))?\)?\s*$/; private static readonly TYPING_SEARCH_DELAY = 200; // This delay accommodates for the user typing a word and then stops typing to start searching diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 66a5013973..5ce3be26db 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -41,7 +41,7 @@ import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition import { OpenAnythingHandler } from 'vs/workbench/contrib/search/browser/openAnythingHandler'; import { OpenSymbolHandler } from 'vs/workbench/contrib/search/browser/openSymbolHandler'; import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions'; -import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FindInFilesAction, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand } from 'vs/workbench/contrib/search/browser/searchActions'; +import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand } from 'vs/workbench/contrib/search/browser/searchActions'; import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet'; @@ -523,7 +523,14 @@ const registry = Registry.as(ActionExtensions.Workbenc // Show Search and Find in Files are redundant, but we can't break keybindings by removing one. So it's the same action, same keybinding, registered to different IDs. // Show Search 'when' is redundant but if the two conflict with exactly the same keybinding and 'when' clause, then they can show up as "unbound" - #51780 registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSearchViewletAction, VIEWLET_ID, OpenSearchViewletAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }, Constants.SearchViewVisibleKey.toNegated()), 'View: Show Search', nls.localize('view', "View")); -registry.registerWorkbenchAction(new SyncActionDescriptor(FindInFilesAction, Constants.FindInFilesActionId, nls.localize('findInFiles', "Find in Files"), { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F }), 'Find in Files', category); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: Constants.FindInFilesActionId, + weight: KeybindingWeight.WorkbenchContrib, + when: null, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F, + handler: FindInFilesCommand +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: Constants.FindInFilesActionId, title: { value: nls.localize('findInFiles', "Find in Files"), original: 'Find in Files' }, category } }); MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { group: '4_find_global', command: { diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 14a99235e3..00fa60abb8 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -160,19 +160,35 @@ export abstract class FindOrReplaceInFilesAction extends Action { }); } } - -export class FindInFilesAction extends FindOrReplaceInFilesAction { - - static readonly LABEL = nls.localize('findInFiles', "Find in Files"); - - constructor(id: string, label: string, - @IViewletService viewletService: IViewletService, - @IPanelService panelService: IPanelService, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, viewletService, panelService, configurationService, /*expandSearchReplaceWidget=*/false); - } +export interface IFindInFilesArgs { + query?: string; + replace?: string; + triggerSearch?: boolean; + filesToInclude?: string; + filesToExclude?: string; + isRegex?: boolean; + isCaseSensitive?: boolean; + matchWholeWord?: boolean; } +export const FindInFilesCommand: ICommandHandler = (accessor, args: IFindInFilesArgs = {}) => { + + const viewletService = accessor.get(IViewletService); + const panelService = accessor.get(IPanelService); + const configurationService = accessor.get(IConfigurationService); + openSearchView(viewletService, panelService, configurationService, false).then(openedView => { + if (openedView) { + const searchAndReplaceWidget = openedView.searchAndReplaceWidget; + searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string'); + let updatedText = false; + if (typeof args.query === 'string') { + openedView.setSearchParameters(args); + } else { + updatedText = openedView.updateTextFromSelection((typeof args.replace !== 'string')); + } + openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); + } + }); +}; export class OpenSearchViewletAction extends FindOrReplaceInFilesAction { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 58761037f7..c82152c377 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -43,7 +43,7 @@ import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/act import { ResourceLabels } from 'vs/workbench/browser/labels'; import { IEditor } from 'vs/workbench/common/editor'; import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; -import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction } from 'vs/workbench/contrib/search/browser/searchActions'; +import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate, SearchDND } from 'vs/workbench/contrib/search/browser/searchResultsView'; import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; @@ -452,8 +452,12 @@ export class SearchView extends ViewletPanel { return; } - const root = element instanceof SearchResult ? null : element; - this.tree.setChildren(root, this.createIterator(element, collapseResults)); + if (element instanceof SearchResult) { + this.tree.setChildren(null, this.createIterator(element, collapseResults)); + } else { + this.tree.setChildren(element, this.createIterator(element, collapseResults)); + this.tree.rerender(element); + } }); } } @@ -642,7 +646,7 @@ export class SearchView extends ViewletPanel { })); this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); - const resourceNavigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: true })); + const resourceNavigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: true, openOnSelection: false })); this._register(Event.debounce(resourceNavigator.onDidOpenResource, (last, event) => event, 75, true)(options => { if (options.element instanceof Match) { const selectedMatch: Match = options.element; @@ -1043,6 +1047,37 @@ export class SearchView extends ViewletPanel { this.onQueryChanged(true); } + setSearchParameters(args: IFindInFilesArgs = {}): void { + if (typeof args.isCaseSensitive === 'boolean') { + this.searchWidget.searchInput.setCaseSensitive(args.isCaseSensitive); + } + if (typeof args.matchWholeWord === 'boolean') { + this.searchWidget.searchInput.setWholeWords(args.matchWholeWord); + } + if (typeof args.isRegex === 'boolean') { + this.searchWidget.searchInput.setRegex(args.isRegex); + } + if (typeof args.filesToInclude === 'string') { + this.searchIncludePattern.setValue(String(args.filesToInclude)); + } + if (typeof args.filesToExclude === 'string') { + this.searchExcludePattern.setValue(String(args.filesToExclude)); + } + if (typeof args.query === 'string') { + this.searchWidget.searchInput.setValue(args.query); + } + if (typeof args.replace === 'string') { + this.searchWidget.replaceInput.value = args.replace; + } else { + if (this.searchWidget.replaceInput.value !== '') { + this.searchWidget.replaceInput.value = ''; + } + } + if (typeof args.triggerSearch === 'boolean' && args.triggerSearch) { + this.onQueryChanged(true); + } + } + toggleQueryDetails(moveFocus = true, show?: boolean, skipLayout?: boolean, reverse?: boolean): void { const cls = 'more'; show = typeof show === 'undefined' ? !dom.hasClass(this.queryDetails, cls) : Boolean(show); diff --git a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts index 3057dd8ed0..38c1d8928a 100644 --- a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts @@ -20,6 +20,7 @@ import { SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFil import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IFileService } from 'vs/platform/files/common/files'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; const id = 'workbench.action.openSnippets'; @@ -120,7 +121,7 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir return { existing, future }; } -async function createSnippetFile(scope: string, defaultPath: URI, windowService: IWindowService, notificationService: INotificationService, fileService: IFileService, opener: IOpenerService) { +async function createSnippetFile(scope: string, defaultPath: URI, windowService: IWindowService, notificationService: INotificationService, fileService: IFileService, textFileService: ITextFileService, opener: IOpenerService) { await fileService.createFolder(defaultPath); await timeout(100); // ensure quick pick closes... @@ -138,7 +139,7 @@ async function createSnippetFile(scope: string, defaultPath: URI, windowService: return undefined; } - await fileService.updateContent(resource, [ + await textFileService.write(resource, [ '{', '\t// Place your ' + scope + ' snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and ', '\t// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope ', @@ -163,7 +164,7 @@ async function createSnippetFile(scope: string, defaultPath: URI, windowService: return undefined; } -async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileService) { +async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileService, textFileService: ITextFileService) { if (await fileService.exists(URI.file(pick.filepath))) { return; } @@ -184,7 +185,7 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS '\t// }', '}' ].join('\n'); - await fileService.updateContent(URI.file(pick.filepath), contents); + await textFileService.write(URI.file(pick.filepath), contents); } CommandsRegistry.registerCommand(id, async (accessor): Promise => { @@ -198,6 +199,7 @@ CommandsRegistry.registerCommand(id, async (accessor): Promise => { const notificationService = accessor.get(INotificationService); const workspaceService = accessor.get(IWorkspaceContextService); const fileService = accessor.get(IFileService); + const textFileService = accessor.get(ITextFileService); const picks = await computePicks(snippetService, envService, modeService); const existing: QuickPickInput[] = picks.existing; @@ -231,12 +233,12 @@ CommandsRegistry.registerCommand(id, async (accessor): Promise => { }); if (globalSnippetPicks.indexOf(pick as SnippetPick) >= 0) { - return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, windowService, notificationService, fileService, opener); + return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, windowService, notificationService, fileService, textFileService, opener); } else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) { - return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, windowService, notificationService, fileService, opener); + return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, windowService, notificationService, fileService, textFileService, opener); } else if (ISnippetPick.is(pick)) { if (pick.hint) { - await createLanguageSnippetFile(pick, fileService); + await createLanguageSnippetFile(pick, fileService, textFileService); } return opener.open(URI.file(pick.filepath)); } diff --git a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts index 5d2fa3d2c7..8ab4b4b628 100644 --- a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts +++ b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts @@ -19,7 +19,7 @@ import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common import * as themes from 'vs/workbench/common/theme'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IFileService } from 'vs/platform/files/common/files'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -37,7 +37,7 @@ class PartsSplash { constructor( @IThemeService private readonly _themeService: IThemeService, @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, - @IFileService private readonly _fileService: IFileService, + @ITextFileService private readonly _textFileService: ITextFileService, @IEnvironmentService private readonly _envService: IEnvironmentService, @IBroadcastService private readonly _broadcastService: IBroadcastService, @ILifecycleService lifecycleService: ILifecycleService, @@ -78,15 +78,14 @@ class PartsSplash { sideBarWidth: getTotalWidth(this._layoutService.getContainer(Parts.SIDEBAR_PART)), statusBarHeight: getTotalHeight(this._layoutService.getContainer(Parts.STATUSBAR_PART)), }; - this._fileService.updateContent( + this._textFileService.write( URI.file(join(this._envService.userDataPath, 'rapid_render.json')), JSON.stringify({ id: PartsSplash._splashElementId, colorInfo, layoutInfo, baseTheme - }), - { encoding: 'utf8' } + }) ); if (baseTheme !== this._lastBaseTheme || colorInfo.editorBackground !== this._lastBackground) { diff --git a/src/vs/workbench/contrib/stats/node/workspaceStats.ts b/src/vs/workbench/contrib/stats/node/workspaceStats.ts index 98ddf9c7ab..43f48abed9 100644 --- a/src/vs/workbench/contrib/stats/node/workspaceStats.ts +++ b/src/vs/workbench/contrib/stats/node/workspaceStats.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { IFileService, IFileStat, IResolveFileResult, IContent } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWindowConfiguration, IWindowService } from 'vs/platform/windows/common/windows'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { endsWith } from 'vs/base/common/strings'; @@ -217,7 +217,7 @@ export class WorkspaceStats implements IWorkbenchContribution { @IFileService private readonly fileService: IFileService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWindowService private readonly windowService: IWindowService, @INotificationService private readonly notificationService: INotificationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @@ -229,7 +229,7 @@ export class WorkspaceStats implements IWorkbenchContribution { private report(): void { // Workspace Stats - this.resolveWorkspaceTags(this.windowService.getConfiguration(), rootFiles => this.handleWorkspaceFiles(rootFiles)) + this.resolveWorkspaceTags(this.environmentService.configuration, rootFiles => this.handleWorkspaceFiles(rootFiles)) .then(tags => this.reportWorkspaceTags(tags), error => onUnexpectedError(error)); // Cloud Stats diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index 4def955e7a..b14d2a24af 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -100,16 +100,27 @@ const presentation: IJSONSchema = { default: false, description: nls.localize('JsonSchema.tasks.presentation.focus', 'Controls whether the panel takes focus. Default is false. If set to true the panel is revealed as well.') }, + revealProblem: { + type: 'string', + enum: ['always', 'onProblem', 'never'], + enumDescriptions: [ + nls.localize('JsonSchema.tasks.presentation.revealProblem.always', 'Always reveals the problems panel when this task is executed.'), + nls.localize('JsonSchema.tasks.presentation.revealProblem.onProblem', 'Only reveals the problems panel if a problem is found.'), + nls.localize('JsonSchema.tasks.presentation.revealProblem.never', 'Never reveals the problems panel when this task is executed.'), + ], + default: 'never', + description: nls.localize('JsonSchema.tasks.presentation.revealProblem', 'Controls whether the problems panel is revealed when running this task or not. Takes precedence over option \"reveal\". Default is \"never\".') + }, reveal: { type: 'string', enum: ['always', 'silent', 'never'], enumDescriptions: [ nls.localize('JsonSchema.tasks.presentation.reveal.always', 'Always reveals the terminal when this task is executed.'), - nls.localize('JsonSchema.tasks.presentation.reveal.silent', 'Only reveals the terminal if the task exits with an error.'), + nls.localize('JsonSchema.tasks.presentation.reveal.silent', 'Only reveals the terminal if the task exits with an error or the problem matcher finds an error.'), nls.localize('JsonSchema.tasks.presentation.reveal.never', 'Never reveals the terminal when this task is executed.'), ], default: 'always', - description: nls.localize('JsonSchema.tasks.presentation.reveals', 'Controls whether the panel running the task is revealed or not. Default is \"always\".') + description: nls.localize('JsonSchema.tasks.presentation.reveals', 'Controls whether the panel running the task is revealed or not. May be overridden by option \"revealProblem\". Default is \"always\".') }, panel: { type: 'string', diff --git a/src/vs/workbench/contrib/tasks/common/media/task.contribution.css b/src/vs/workbench/contrib/tasks/common/media/task.contribution.css index 8f11b24c34..5ffde87453 100644 --- a/src/vs/workbench/contrib/tasks/common/media/task.contribution.css +++ b/src/vs/workbench/contrib/tasks/common/media/task.contribution.css @@ -43,6 +43,7 @@ .task-statusbar-item-label { display: inline-block; cursor: pointer; + padding: 0 5px 0 5px; } .task-statusbar-item-label > .task-statusbar-item-label-counter { diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 0012a92f5d..7f4d551c2b 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -91,6 +91,12 @@ export interface PresentationOptionsConfig { */ reveal?: string; + /** + * Controls whether the problems panel is revealed when running this task or not. + * Defaults to `RevealKind.Never`. + */ + revealProblem?: string; + /** * Controls whether the executed command is printed to the output window or terminal as well. */ @@ -796,7 +802,7 @@ namespace CommandOptions { namespace CommandConfiguration { export namespace PresentationOptions { - const properties: MetaData[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }, { property: 'group' }]; + const properties: MetaData[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'revealProblem' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }, { property: 'group' }]; interface PresentationOptionsShape extends LegacyCommandProperties { presentation?: PresentationOptionsConfig; @@ -805,6 +811,7 @@ namespace CommandConfiguration { export function from(this: void, config: PresentationOptionsShape, context: ParseContext): Tasks.PresentationOptions | undefined { let echo: boolean; let reveal: Tasks.RevealKind; + let revealProblem: Tasks.RevealProblemKind; let focus: boolean; let panel: Tasks.PanelKind; let showReuseMessage: boolean; @@ -827,6 +834,9 @@ namespace CommandConfiguration { if (Types.isString(presentation.reveal)) { reveal = Tasks.RevealKind.fromString(presentation.reveal); } + if (Types.isString(presentation.revealProblem)) { + revealProblem = Tasks.RevealProblemKind.fromString(presentation.revealProblem); + } if (Types.isBoolean(presentation.focus)) { focus = presentation.focus; } @@ -847,7 +857,7 @@ namespace CommandConfiguration { if (!hasProps) { return undefined; } - return { echo: echo!, reveal: reveal!, focus: focus!, panel: panel!, showReuseMessage: showReuseMessage!, clear: clear!, group }; + return { echo: echo!, reveal: reveal!, revealProblem: revealProblem!, focus: focus!, panel: panel!, showReuseMessage: showReuseMessage!, clear: clear!, group }; } export function assignProperties(target: Tasks.PresentationOptions, source: Tasks.PresentationOptions | undefined): Tasks.PresentationOptions | undefined { @@ -860,7 +870,7 @@ namespace CommandConfiguration { export function fillDefaults(value: Tasks.PresentationOptions, context: ParseContext): Tasks.PresentationOptions | undefined { let defaultEcho = context.engine === Tasks.ExecutionEngine.Terminal ? true : false; - return _fillDefaults(value, { echo: defaultEcho, reveal: Tasks.RevealKind.Always, focus: false, panel: Tasks.PanelKind.Shared, showReuseMessage: true, clear: false }, properties, context); + return _fillDefaults(value, { echo: defaultEcho, reveal: Tasks.RevealKind.Always, revealProblem: Tasks.RevealProblemKind.Never, focus: false, panel: Tasks.PanelKind.Shared, showReuseMessage: true, clear: false }, properties, context); } export function freeze(value: Tasks.PresentationOptions): Readonly | undefined { diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index ee12d2b0fc..d73b497da1 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -12,8 +12,8 @@ import { UriComponents } from 'vs/base/common/uri'; import { ProblemMatcher } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export const TASK_RUNNING_STATE = new RawContextKey('taskRunning', false); @@ -123,7 +123,9 @@ export enum RevealKind { /** * Only brings the terminal to front if a problem is detected executing the task - * (e.g. the task couldn't be started because). + * e.g. the task couldn't be started, + * the task ended with an exit code other than zero, + * or the problem matcher found an error. */ Silent = 2, @@ -148,6 +150,39 @@ export namespace RevealKind { } } +export enum RevealProblemKind { + /** + * Never reveals the problems panel when this task is executed. + */ + Never = 1, + + + /** + * Only reveals the problems panel if a problem is found. + */ + OnProblem = 2, + + /** + * Never reveals the problems panel when this task is executed. + */ + Always = 3 +} + +export namespace RevealProblemKind { + export function fromString(this: void, value: string): RevealProblemKind { + switch (value.toLowerCase()) { + case 'always': + return RevealProblemKind.Always; + case 'never': + return RevealProblemKind.Never; + case 'onproblem': + return RevealProblemKind.OnProblem; + default: + return RevealProblemKind.OnProblem; + } + } +} + export enum PanelKind { /** @@ -189,6 +224,12 @@ export interface PresentationOptions { */ reveal: RevealKind; + /** + * Controls whether the problems pane is revealed when running this task or not. + * Defaults to `RevealProblemKind.Never`. + */ + revealProblem: RevealProblemKind; + /** * Controls whether the command associated with the task is echoed * in the user interface. @@ -225,7 +266,7 @@ export interface PresentationOptions { export namespace PresentationOptions { export const defaults: PresentationOptions = { - echo: true, reveal: RevealKind.Always, focus: false, panel: PanelKind.Shared, showReuseMessage: true, clear: false + echo: true, reveal: RevealKind.Always, revealProblem: RevealProblemKind.Never, focus: false, panel: PanelKind.Shared, showReuseMessage: true, clear: false }; } diff --git a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts index 44875a99b4..e51010ba3a 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts @@ -95,6 +95,8 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } fr import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { RunAutomaticTasks, AllowAutomaticTaskRunning, DisallowAutomaticTaskRunning } from 'vs/workbench/contrib/tasks/electron-browser/runAutomaticTasks'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + let tasksCategory = nls.localize('tasksCategory', "Tasks"); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); @@ -385,7 +387,7 @@ interface WorkspaceFolderConfigurationResult { hasErrors: boolean; } -interface TaskCustomizationTelementryEvent { +interface TaskCustomizationTelemetryEvent { properties: string[]; } @@ -467,6 +469,7 @@ class TaskService extends Disposable implements ITaskService { @IConfigurationService private readonly configurationService: IConfigurationService, @IMarkerService private readonly markerService: IMarkerService, @IOutputService private readonly outputService: IOutputService, + @IPanelService private readonly panelService: IPanelService, @IEditorService private readonly editorService: IEditorService, @IFileService private readonly fileService: IFileService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -485,6 +488,7 @@ class TaskService extends Disposable implements ITaskService { @IDialogService private readonly dialogService: IDialogService, @INotificationService private readonly notificationService: INotificationService, @IContextKeyService contextKeyService: IContextKeyService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService ) { super(); @@ -1077,7 +1081,7 @@ class TaskService extends Disposable implements ITaskService { if (editorConfig.editor.insertSpaces) { content = content.replace(/(\n)(\t+)/g, (_, s1, s2) => s1 + strings.repeat(' ', s2.length * editorConfig.editor.tabSize)); } - promise = this.fileService.createFile(workspaceFolder.toResource('.vscode/tasks.json'), content).then(() => { }); + promise = this.textFileService.create(workspaceFolder.toResource('.vscode/tasks.json'), content).then(() => { }); } else { // We have a global task configuration if ((index === -1) && properties) { @@ -1104,7 +1108,7 @@ class TaskService extends Disposable implements ITaskService { return Promise.resolve(undefined); } return promise.then(() => { - let event: TaskCustomizationTelementryEvent = { + let event: TaskCustomizationTelemetryEvent = { properties: properties ? Object.getOwnPropertyNames(properties) : [] }; /* __GDPR__ @@ -1352,9 +1356,9 @@ class TaskService extends Disposable implements ITaskService { } if (this.executionEngine === ExecutionEngine.Terminal) { this._taskSystem = new TerminalTaskSystem( - this.terminalService, this.outputService, this.markerService, + this.terminalService, this.outputService, this.panelService, this.markerService, this.modelService, this.configurationResolverService, this.telemetryService, - this.contextService, this._windowService, + this.contextService, this._environmentService, TaskService.OutputChannelId, (workspaceFolder: IWorkspaceFolder) => { if (!workspaceFolder) { @@ -1877,7 +1881,14 @@ class TaskService extends Disposable implements ITaskService { private canRunCommand(): boolean { if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.notificationService.info(nls.localize('TaskService.noWorkspace', 'Tasks are only available on a workspace folder.')); + this.notificationService.prompt( + Severity.Info, + nls.localize('TaskService.noWorkspace', "Tasks are only available on a workspace folder."), + [{ + label: nls.localize('TaskService.learnMore', "Learn More"), + run: () => window.open('https://code.visualstudio.com/docs/editor/tasks') + }] + ); return false; } return true; @@ -2042,7 +2053,7 @@ class TaskService extends Disposable implements ITaskService { this.showQuickPick(tasks ? tasks : this.tasks(), nls.localize('TaskService.pickRunTask', 'Select the task to run'), { - label: nls.localize('TaslService.noEntryToRun', 'No task to run found. Configure Tasks...'), + label: nls.localize('TaskService.noEntryToRun', 'No task to run found. Configure Tasks...'), task: null }, true). @@ -2197,7 +2208,7 @@ class TaskService extends Disposable implements ITaskService { } let runQuickPick = (promise?: Promise) => { this.showQuickPick(promise || this.getActiveTasks(), - nls.localize('TaskService.tastToTerminate', 'Select task to terminate'), + nls.localize('TaskService.taskToTerminate', 'Select task to terminate'), { label: nls.localize('TaskService.noTaskRunning', 'No task is currently running'), task: null @@ -2253,7 +2264,7 @@ class TaskService extends Disposable implements ITaskService { } let runQuickPick = (promise?: Promise) => { this.showQuickPick(promise || this.getActiveTasks(), - nls.localize('TaskService.tastToRestart', 'Select the task to restart'), + nls.localize('TaskService.taskToRestart', 'Select the task to restart'), { label: nls.localize('TaskService.noTaskToRestart', 'No task to restart'), task: null @@ -2338,7 +2349,7 @@ class TaskService extends Disposable implements ITaskService { "autoDetect" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ - return this.fileService.createFile(resource, content).then((result): URI => { + return this.textFileService.create(resource, content).then((result): URI => { this.telemetryService.publicLog(TaskService.TemplateTelemetryEventName, { templateId: selection.id, autoDetect: selection.autoDetect diff --git a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts index 982077f427..5e9885322c 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts @@ -22,6 +22,7 @@ import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/marke import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ProblemMatcher, ProblemMatcherRegistry /*, ProblemPattern, getResource */ } from 'vs/workbench/contrib/tasks/common/problemMatcher'; +import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -31,7 +32,7 @@ import { IOutputService } from 'vs/workbench/contrib/output/common/output'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind, - TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration, ExtensionTaskSource, TaskScope + TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration, ExtensionTaskSource, TaskScope, RevealProblemKind } from 'vs/workbench/contrib/tasks/common/tasks'; import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver, @@ -39,9 +40,10 @@ import { } from 'vs/workbench/contrib/tasks/common/taskSystem'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { URI } from 'vs/base/common/uri'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Schemas } from 'vs/base/common/network'; import { getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; interface TerminalData { terminal: ITerminalInstance; @@ -159,14 +161,18 @@ export class TerminalTaskSystem implements ITaskSystem { private readonly _onDidStateChange: Emitter; - constructor(private terminalService: ITerminalService, private outputService: IOutputService, + constructor( + private terminalService: ITerminalService, + private outputService: IOutputService, + private panelService: IPanelService, private markerService: IMarkerService, private modelService: IModelService, private configurationResolverService: IConfigurationResolverService, private telemetryService: ITelemetryService, private contextService: IWorkspaceContextService, - private windowService: IWindowService, + private environmentService: IWorkbenchEnvironmentService, private outputChannelId: string, - taskSystemInfoResolver: TaskSystemInfoResovler) { + taskSystemInfoResolver: TaskSystemInfoResovler + ) { this.activeTasks = Object.create(null); this.terminals = Object.create(null); @@ -514,11 +520,16 @@ export class TerminalTaskSystem implements ITaskSystem { eventCounter--; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Inactive, task)); if (eventCounter === 0) { - let reveal = task.command.presentation!.reveal; - if ((reveal === RevealKind.Silent) && (watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && + if ((watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && (watchingProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error)) { - this.terminalService.setActiveInstance(terminal!); - this.terminalService.showPanel(false); + let reveal = task.command.presentation!.reveal; + let revealProblem = task.command.presentation!.revealProblem; + if (revealProblem === RevealProblemKind.OnProblem) { + this.panelService.openPanel(Constants.MARKERS_PANEL_ID, true); + } else if (reveal === RevealKind.Silent) { + this.terminalService.setActiveInstance(terminal!); + this.terminalService.showPanel(false); + } } } } @@ -642,7 +653,11 @@ export class TerminalTaskSystem implements ITaskSystem { } } let reveal = task.command.presentation!.reveal; - if (terminal && (reveal === RevealKind.Silent) && ((exitCode !== 0) || (startStopProblemMatcher.numberOfMatches > 0) && startStopProblemMatcher.maxMarkerSeverity && + let revealProblem = task.command.presentation!.revealProblem; + let revealProblemPanel = terminal && (revealProblem === RevealProblemKind.OnProblem) && (startStopProblemMatcher.numberOfMatches > 0); + if (revealProblemPanel) { + this.panelService.openPanel(Constants.MARKERS_PANEL_ID); + } else if (terminal && (reveal === RevealKind.Silent) && ((exitCode !== 0) || (startStopProblemMatcher.numberOfMatches > 0) && startStopProblemMatcher.maxMarkerSeverity && (startStopProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error))) { this.terminalService.setActiveInstance(terminal); this.terminalService.showPanel(false); @@ -673,7 +688,10 @@ export class TerminalTaskSystem implements ITaskSystem { if (!terminal) { return Promise.reject(new Error(`Failed to create terminal for task ${task._label}`)); } - if (task.command.presentation && (task.command.presentation.reveal === RevealKind.Always)) { + let showProblemPanel = task.command.presentation && (task.command.presentation.revealProblem === RevealProblemKind.Always); + if (showProblemPanel) { + this.panelService.openPanel(Constants.MARKERS_PANEL_ID); + } else if (task.command.presentation && (task.command.presentation.reveal === RevealKind.Always)) { this.terminalService.setActiveInstance(terminal); this.terminalService.showPanel(task.command.presentation.focus); } @@ -853,7 +871,7 @@ export class TerminalTaskSystem implements ITaskSystem { } } // This must be normalized to the OS - const authority = this.windowService.getConfiguration().remoteAuthority; + const authority = this.environmentService.configuration.remoteAuthority; shellLaunchConfig.cwd = URI.from({ scheme: authority ? REMOTE_HOST_SCHEME : Schemas.file, authority: authority, path: cwd }); } if (options.env) { diff --git a/src/vs/workbench/contrib/tasks/test/electron-browser/configuration.test.ts b/src/vs/workbench/contrib/tasks/test/electron-browser/configuration.test.ts index dec9c72a44..0d1df8797f 100644 --- a/src/vs/workbench/contrib/tasks/test/electron-browser/configuration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/electron-browser/configuration.test.ts @@ -83,7 +83,7 @@ class PresentationBuilder { public result: Tasks.PresentationOptions; constructor(public parent: CommandConfigurationBuilder) { - this.result = { echo: false, reveal: Tasks.RevealKind.Always, focus: false, panel: Tasks.PanelKind.Shared, showReuseMessage: true, clear: false }; + this.result = { echo: false, reveal: Tasks.RevealKind.Always, revealProblem: Tasks.RevealProblemKind.Never, focus: false, panel: Tasks.PanelKind.Shared, showReuseMessage: true, clear: false }; } public echo(value: boolean): PresentationBuilder { diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 02cb825a16..a8caad21e1 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -12,7 +12,7 @@ import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/a import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { language } from 'vs/base/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; @@ -30,13 +30,13 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr @IEditorService editorService: IEditorService, @IKeybindingService keybindingsService: IKeybindingService, @IWorkbenchThemeService themeService: IWorkbenchThemeService, - @IWindowService windowService: IWindowService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IConfigurationService configurationService: IConfigurationService, @IViewletService viewletService: IViewletService ) { super(); - const { filesToOpen, filesToCreate, filesToDiff } = windowService.getConfiguration(); + const { filesToOpen, filesToCreate, filesToDiff } = environmentService.configuration; const activeViewlet = viewletService.getActiveViewlet(); /* __GDPR__ diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 2a42147d5a..c0e643a220 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -264,6 +264,11 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, + 'terminal.integrated.enableLatencyMitigation': { + description: nls.localize('terminal.integrated.enableLatencyMitigation', "Whether to enable the latency mitigation feature for high-latency terminals."), + type: 'boolean', + default: false + }, 'terminal.integrated.experimentalRefreshOnResume': { description: nls.localize('terminal.integrated.experimentalRefreshOnResume', "An experimental setting that will refresh the terminal renderer when the system is resumed."), type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index ccd5c4e1f3..759421df4f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -587,6 +587,19 @@ export class TerminalInstance implements ITerminalInstance { if (this._processManager) { this._widgetManager = new TerminalWidgetManager(this._wrapperElement); this._processManager.onProcessReady(() => this._linkHandler.setWidgetManager(this._widgetManager)); + + this._processManager.onProcessReady(() => { + if (this._configHelper.config.enableLatencyMitigation) { + if (!this._processManager) { + return; + } + this._processManager.getLatency().then(latency => { + if (latency > 20 && (this._xterm as any).typeAheadInit) { + (this._xterm as any).typeAheadInit(this._processManager, this._themeService); + } + }); + } + }); } const computedStyle = window.getComputedStyle(this._container); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 5f9a6a3f81..d392e301c2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -6,7 +6,7 @@ import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess, IBeforeProcessDataEvent } from 'vs/workbench/contrib/terminal/common/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -14,11 +14,10 @@ import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/commo import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IWindowService } from 'vs/platform/windows/common/windows'; import { Schemas } from 'vs/base/common/network'; import { REMOTE_HOST_SCHEME, getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProductService } from 'vs/platform/product/common/product'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -57,6 +56,8 @@ export class TerminalProcessManager implements ITerminalProcessManager { private readonly _onProcessReady = new Emitter(); public get onProcessReady(): Event { return this._onProcessReady.event; } + private readonly _onBeforeProcessData = new Emitter(); + public get onBeforeProcessData(): Event { return this._onBeforeProcessData.event; } private readonly _onProcessData = new Emitter(); public get onProcessData(): Event { return this._onProcessData.event; } private readonly _onProcessTitle = new Emitter(); @@ -72,9 +73,8 @@ export class TerminalProcessManager implements ITerminalProcessManager { @ILogService private readonly _logService: ILogService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, - @IWindowService private readonly _windowService: IWindowService, @IConfigurationService private readonly _workspaceConfigurationService: IConfigurationService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IProductService private readonly _productService: IProductService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService @@ -114,7 +114,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { if (shellLaunchConfig.cwd && typeof shellLaunchConfig.cwd === 'object') { this.remoteAuthority = getRemoteAuthority(shellLaunchConfig.cwd); } else { - this.remoteAuthority = this._windowService.getConfiguration().remoteAuthority; + this.remoteAuthority = this._environmentService.configuration.remoteAuthority; } const hasRemoteAuthority = !!this.remoteAuthority; let launchRemotely = hasRemoteAuthority || forceExtHostProcess; @@ -183,7 +183,11 @@ export class TerminalProcessManager implements ITerminalProcessManager { const p = this._process!; p.onProcessData(data => { - this._onProcessData.fire(data); + const beforeProcessDataEvent: IBeforeProcessDataEvent = { data }; + this._onBeforeProcessData.fire(beforeProcessDataEvent); + if (beforeProcessDataEvent.data && beforeProcessDataEvent.data.length > 0) { + this._onProcessData.fire(beforeProcessDataEvent.data); + } }); p.onProcessIdReady(pid => { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index fcb8106525..bbdcbc410c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -17,7 +17,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; @@ -35,7 +35,7 @@ export abstract class TerminalService extends CommonTerminalService implements I @INotificationService notificationService: INotificationService, @IDialogService dialogService: IDialogService, @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @IWindowService private _windowService: IWindowService, + @IWorkbenchEnvironmentService private _environmentService: IWorkbenchEnvironmentService, @IExtensionService extensionService: IExtensionService, @IFileService fileService: IFileService, ) { @@ -76,7 +76,7 @@ export abstract class TerminalService extends CommonTerminalService implements I return; } - if (this._windowService.getConfiguration().remoteAuthority) { + if (this._environmentService.configuration.remoteAuthority) { // Don't suggest if the opened workspace is remote return; } @@ -87,7 +87,7 @@ export abstract class TerminalService extends CommonTerminalService implements I return; } - if (this._windowService.getConfiguration().remoteAuthority) { + if (this._environmentService.configuration.remoteAuthority) { // Don't suggest if the opened workspace is remote return; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts new file mode 100644 index 0000000000..03eb766019 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Terminal as XTermTerminal } from 'vscode-xterm'; +import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export interface ITerminalCore { + buffer: any; +} + +export interface ITypeAheadAddonTerminal { + _core: ITerminalCore; + on: any; + write(data: string): void; + cols: number; + + __typeAheadQueue: string[]; + __typeAheadState: TypeAheadState; + __typeAheadCurrentYBase: number; + __typeAheadCurrentY: number; +} + +enum TypeAheadState { + /** + * The normal state, ready to type if it starts. + */ + Normal, + /** + * Something happens such that we cannot make a good guess on what to print, + * wait until the cursor row changes before proceeding. + */ + AwaitingRowChange +} + +function isCharPrintable(data: string): boolean { + const code = data.charCodeAt(0); + return data.length === 1 && code >= 32 && code <= 126; +} + +function init(terminal: any, processManager: ITerminalProcessManager, themeService: IThemeService): void { + const t = terminal as ITypeAheadAddonTerminal; + + t.__typeAheadQueue = []; + t.__typeAheadState = TypeAheadState.Normal; + t.__typeAheadCurrentYBase = 0; + t.__typeAheadCurrentY = 0; + + function typeAhead(data: string): void { + for (let i = 0; i < data.length; i++) { + t.__typeAheadQueue.push(data[i]); + } + t.write(data); + } + + t.on('cursormove', () => { + // Reset if the cursor row changed + if (t._core.buffer.ybase !== t.__typeAheadCurrentYBase || t._core.buffer.y !== t.__typeAheadCurrentY) { + t.__typeAheadCurrentYBase = t._core.buffer.ybase; + t.__typeAheadCurrentY = t._core.buffer.y; + t.__typeAheadState = TypeAheadState.Normal; + } + }); + + t.on('data', (data: string) => { + // Exit if we're waiting for a row change + if (t.__typeAheadState === TypeAheadState.AwaitingRowChange) { + return; + } + + // Only enable in the normal buffer + if (!t._core.buffer._hasScrollback) { + return; + } + + // // Handle enter + // if (data === '\r') { + // typeAhead('\r\n'); + // return; + // } + + // // Left arrow + // if (data === '\x1b[D') { + // // TODO: How to stop it from going beyond prompt? + // typeAhead(String.fromCharCode(8)); + // } + + // // Right arrow + // if (data === '\x1b[C') { + // // TODO: How to stop it from going beyond prompt? + // typeAhead('\x1b[C'); + // } + + // // Backspace (DEL) + // if (data.charCodeAt(0) === 127) { + // // TODO: This would require knowing the prompt length to be able to shift everything + // } + + if (!isCharPrintable(data)) { + t.__typeAheadState = TypeAheadState.AwaitingRowChange; + return; + } + + if (t._core.buffer.x === t.cols - 1) { + // TODO: Does the space get added on Windows/Linux too? + data += ' \r'; + } + typeAhead(data); + }); + + processManager.onBeforeProcessData(event => { + let consumeCount = 0; + for (let i = 0; i < event.data.length; i++) { + if (t.__typeAheadQueue[0] === event.data[i]) { + t.__typeAheadQueue.shift(); + consumeCount++; + } else { + t.__typeAheadQueue.length = 0; + break; + } + } + if (consumeCount === event.data.length) { + event.data = ''; + } else if (consumeCount > 0) { + event.data = event.data.substr(consumeCount); + } + }); +} + +export function apply(terminalConstructor: typeof XTermTerminal) { + (terminalConstructor.prototype).typeAheadInit = function (processManager: ITerminalProcessManager, themeService: IThemeService): void { + init(this, processManager, themeService); + }; +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 44158db7cb..d0caa558bb 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -101,6 +101,7 @@ export interface ITerminalConfiguration { experimentalBufferImpl: 'JsArray' | 'TypedArray'; splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; windowsEnableConpty: boolean; + enableLatencyMitigation: boolean; experimentalRefreshOnResume: boolean; } @@ -634,6 +635,14 @@ export interface ITerminalCommandTracker { selectToNextLine(): void; } +export interface IBeforeProcessDataEvent { + /** + * The data of the event, this can be modified by the event listener to change what gets sent + * to the terminal. + */ + data: string; +} + export interface ITerminalProcessManager extends IDisposable { readonly processState: ProcessState; readonly ptyProcessReady: Promise; @@ -643,6 +652,7 @@ export interface ITerminalProcessManager extends IDisposable { readonly userHome: string | undefined; readonly onProcessReady: Event; + readonly onBeforeProcessData: Event; readonly onProcessData: Event; readonly onProcessTitle: Event; readonly onProcessExit: Event; diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts index a090438b31..728a676329 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts @@ -12,6 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; +import * as typeAheadAddon from 'vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon'; let Terminal: typeof XTermTerminal; @@ -35,6 +36,7 @@ export class TerminalInstanceService implements ITerminalInstanceService { Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/search/search')); Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/webLinks/webLinks')); Terminal.applyAddon(require.__$__nodeRequire('vscode-xterm/lib/addons/winptyCompat/winptyCompat')); + Terminal.applyAddon(typeAheadAddon); // Localize strings Terminal.strings.blankLine = nls.localize('terminal.integrated.a11yBlankLine', 'Blank line'); Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input'); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalService.ts index c2a021fdbd..39a60adf28 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalService.ts @@ -19,7 +19,8 @@ import { getDefaultShell, linuxDistro, getWindowsBuildNumber } from 'vs/workbenc import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ipcRenderer as ipc } from 'electron'; -import { IOpenFileRequest, IWindowService } from 'vs/platform/windows/common/windows'; +import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; import { coalesce } from 'vs/base/common/arrays'; @@ -43,10 +44,10 @@ export class TerminalService extends BrowserTerminalService implements ITerminal @INotificationService notificationService: INotificationService, @IDialogService dialogService: IDialogService, @IExtensionService extensionService: IExtensionService, - @IWindowService windowService: IWindowService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IFileService fileService: IFileService ) { - super(contextKeyService, panelService, layoutService, lifecycleService, storageService, notificationService, dialogService, instantiationService, windowService, extensionService, fileService); + super(contextKeyService, panelService, layoutService, lifecycleService, storageService, notificationService, dialogService, instantiationService, environmentService, extensionService, fileService); this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, linuxDistro); ipc.on('vscode:openFiles', (_event: any, request: IOpenFileRequest) => { diff --git a/src/vs/workbench/contrib/update/electron-browser/update.ts b/src/vs/workbench/contrib/update/electron-browser/update.ts index fc1aa979d3..fccc71833b 100644 --- a/src/vs/workbench/contrib/update/electron-browser/update.ts +++ b/src/vs/workbench/contrib/update/electron-browser/update.ts @@ -23,7 +23,7 @@ import * as semver from 'semver'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { INotificationService, INotificationHandle, Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ReleaseNotesManager } from './releaseNotesEditor'; import { isWindows } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -300,7 +300,7 @@ export class UpdateContribution implements IGlobalActivity { @IDialogService private readonly dialogService: IDialogService, @IUpdateService private readonly updateService: IUpdateService, @IActivityService private readonly activityService: IActivityService, - @IWindowService private readonly windowService: IWindowService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { this.state = updateService.state; @@ -330,7 +330,7 @@ export class UpdateContribution implements IGlobalActivity { case StateType.Idle: if (state.error) { this.onError(state.error); - } else if (this.state.type === StateType.CheckingForUpdates && this.state.context && this.state.context.windowId === this.windowService.getCurrentWindowId()) { + } else if (this.state.type === StateType.CheckingForUpdates && this.state.context && this.state.context.windowId === this.environmentService.configuration.windowId) { this.onUpdateNotAvailable(); } break; @@ -555,7 +555,7 @@ export class UpdateContribution implements IGlobalActivity { return null; case StateType.Idle: - const windowId = this.windowService.getCurrentWindowId(); + const windowId = this.environmentService.configuration.windowId; return new Action('update.check', nls.localize('checkForUpdates', "Check for Updates..."), undefined, true, () => this.updateService.checkForUpdates({ windowId })); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 580c60220c..5af54121ed 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -17,14 +17,36 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols'; -import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService'; -import { WebviewFindWidget } from '../browser/webviewFindWidget'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService'; +import { WebviewFindWidget } from '../browser/webviewFindWidget'; import { WebviewContentOptions, WebviewPortMapping, WebviewOptions, Webview } from 'vs/workbench/contrib/webview/common/webview'; +import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorOptions, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; +export interface WebviewPortMapping { + readonly port: number; + readonly resolvedPort: number; +} + +export interface WebviewOptions { + readonly allowSvgs?: boolean; + readonly extension?: { + readonly location: URI; + readonly id?: ExtensionIdentifier; + }; + readonly enableFindWidget?: boolean; +} + +export interface WebviewContentOptions { + readonly allowScripts?: boolean; + readonly svgWhiteList?: string[]; + readonly localResourceRoots?: ReadonlyArray; + readonly portMappings?: ReadonlyArray; +} interface IKeydownEvent { key: string; @@ -126,11 +148,15 @@ class WebviewProtocolProvider extends Disposable { class WebviewPortMappingProvider extends Disposable { + private readonly _tunnels = new Map>(); + constructor( session: WebviewSession, + extensionLocation: URI | undefined, mappings: () => ReadonlyArray, + private readonly tunnelService: ITunnelService, extensionId: ExtensionIdentifier | undefined, - @ITelemetryService telemetryService: ITelemetryService, + @ITelemetryService telemetryService: ITelemetryService ) { super(); @@ -148,7 +174,7 @@ class WebviewPortMappingProvider extends Disposable { hasLogged = true; /* __GDPR__ - "webview.accessLocalhost" : { + "webview.accessLocalhost" : { "extension" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -157,12 +183,25 @@ class WebviewPortMappingProvider extends Disposable { const port = +localhostMatch[1]; for (const mapping of mappings()) { - if (mapping.port === port && mapping.port !== mapping.resolvedPort) { - return { - redirectURL: details.url.replace( - new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`), - `${uri.scheme}://localhost:${mapping.resolvedPort}/`) - }; + if (mapping.port === port) { + if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) { + const tunnel = await this.getOrCreateTunnel(mapping.resolvedPort); + if (tunnel) { + return { + redirectURL: details.url.replace( + new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`), + `${uri.scheme}://localhost:${tunnel.tunnelLocalPort}/`) + }; + } + } + + if (mapping.port !== mapping.resolvedPort) { + return { + redirectURL: details.url.replace( + new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`), + `${uri.scheme}://localhost:${mapping.resolvedPort}/`) + }; + } } } } @@ -170,6 +209,27 @@ class WebviewPortMappingProvider extends Disposable { return undefined; }); } + + dispose() { + super.dispose(); + + for (const tunnel of this._tunnels.values()) { + tunnel.then(tunnel => tunnel.dispose()); + } + this._tunnels.clear(); + } + + private getOrCreateTunnel(remotePort: number): Promise | undefined { + const existing = this._tunnels.get(remotePort); + if (existing) { + return existing; + } + const tunnel = this.tunnelService.openTunnel(remotePort); + if (tunnel) { + this._tunnels.set(remotePort, tunnel); + } + return tunnel; + } } class SvgBlocker extends Disposable { @@ -314,6 +374,7 @@ export class WebviewElement extends Disposable implements Webview { @IThemeService themeService: IThemeService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, + @ITunnelService tunnelService: ITunnelService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService private readonly _configurationService: IConfigurationService, ) { @@ -354,10 +415,12 @@ export class WebviewElement extends Disposable implements Webview { this._register(new WebviewPortMappingProvider( session, - () => (this._contentOptions.portMappings || []), + _options.extension ? _options.extension.location : undefined, + () => (this._contentOptions.portMappings || [{ port: 3000, resolvedPort: 4000 }]), + tunnelService, _options.extension ? _options.extension.id : undefined, - telemetryService)); - + telemetryService + )); if (!this._options.allowSvgs) { const svgBlocker = this._register(new SvgBlocker(session, this._contentOptions)); diff --git a/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider.ts b/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider.ts index 229eaf029d..916876bb84 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider.ts @@ -35,7 +35,7 @@ export class WalkThroughContentProvider implements ITextModelContentProvider, IW reject(err); } }); - }) : this.textFileService.resolveTextContent(URI.file(resource.fsPath)).then(content => content.value)); + }) : this.textFileService.resolve(URI.file(resource.fsPath)).then(content => content.value)); return content.then(content => { let codeEditorModel = this.modelService.getModel(resource); if (!codeEditorModel) { @@ -61,7 +61,7 @@ export class WalkThroughSnippetContentProvider implements ITextModelContentProvi } public provideTextContent(resource: URI): Promise { - return this.textFileService.resolveTextContent(URI.file(resource.fsPath)).then(content => { + return this.textFileService.resolve(URI.file(resource.fsPath)).then(content => { let codeEditorModel = this.modelService.getModel(resource); if (!codeEditorModel) { const j = parseInt(resource.fragment); diff --git a/src/vs/workbench/electron-browser/actions/developerActions.ts b/src/vs/workbench/electron-browser/actions/developerActions.ts index 116d44142f..c22cca3e4a 100644 --- a/src/vs/workbench/electron-browser/actions/developerActions.ts +++ b/src/vs/workbench/electron-browser/actions/developerActions.ts @@ -112,7 +112,7 @@ export class InspectContextKeysAction extends Action { export class ToggleScreencastModeAction extends Action { static readonly ID = 'workbench.action.toggleScreencastMode'; - static LABEL = nls.localize('toggle mouse clicks', "Toggle Screencast Mode"); + static LABEL = nls.localize('toggle screencast mode', "Toggle Screencast Mode"); static disposable: IDisposable | undefined; diff --git a/src/vs/workbench/electron-browser/actions/windowActions.ts b/src/vs/workbench/electron-browser/actions/windowActions.ts index a3ee23ad90..65ecbac249 100644 --- a/src/vs/workbench/electron-browser/actions/windowActions.ts +++ b/src/vs/workbench/electron-browser/actions/windowActions.ts @@ -222,7 +222,7 @@ export abstract class BaseSwitchWindow extends Action { protected abstract isQuickNavigate(): boolean; run(): Promise { - const currentWindowId = this.windowService.getCurrentWindowId(); + const currentWindowId = this.windowService.windowId; return this.windowsService.getWindows().then(windows => { const placeHolder = nls.localize('switchWindowPlaceHolder', "Select a window to switch to"); diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 2d644437d4..158e117120 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -165,7 +165,7 @@ import { InstallVSIXAction } from 'vs/workbench/contrib/extensions/electron-brow const developerCategory = nls.localize('developer', "Developer"); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSharedProcessAction, ToggleSharedProcessAction.ID, ToggleSharedProcessAction.LABEL), 'Developer: Toggle Shared Process', developerCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(InspectContextKeysAction, InspectContextKeysAction.ID, InspectContextKeysAction.LABEL), 'Developer: Inspect Context Keys', developerCategory); - registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleScreencastModeAction, ToggleScreencastModeAction.ID, ToggleScreencastModeAction.LABEL), 'Developer: Toggle Mouse Clicks', developerCategory); + registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleScreencastModeAction, ToggleScreencastModeAction.ID, ToggleScreencastModeAction.LABEL), 'Developer: Toggle Screencast Mode', developerCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(ReloadWindowWithExtensionsDisabledAction, ReloadWindowWithExtensionsDisabledAction.ID, ReloadWindowWithExtensionsDisabledAction.LABEL), 'Developer: Reload Window Without Extensions', developerCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(LogStorageAction, LogStorageAction.ID, LogStorageAction.LABEL), 'Developer: Log Storage', developerCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL), 'Developer: Reload Window', developerCategory); diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 0f0eca3784..a558140fe6 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -15,14 +15,12 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { stat } from 'vs/base/node/pfs'; -import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; -import { IWindowConfiguration, IWindowService } from 'vs/platform/windows/common/windows'; -import { WindowService } from 'vs/platform/windows/electron-browser/windowService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { webFrame } from 'electron'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; @@ -48,7 +46,6 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/workbench/services/files2/electron-browser/diskFileSystemProvider'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME, RemoteExtensionsFileSystemProvider } from 'vs/platform/remote/common/remoteAgentFileSystemChannel'; -import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { DefaultConfigurationExportHelper } from 'vs/workbench/services/configuration/node/configurationExportHelper'; import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache'; import { ConfigurationFileService } from 'vs/workbench/services/configuration/node/configurationFileService'; @@ -177,12 +174,9 @@ class CodeRendererMain extends Disposable { const mainProcessService = this._register(new MainProcessService(this.configuration.windowId)); serviceCollection.set(IMainProcessService, mainProcessService); - // Window - serviceCollection.set(IWindowService, new SyncDescriptor(WindowService, [this.configuration])); - // Environment - const environmentService = new EnvironmentService(this.configuration, this.configuration.execPath); - serviceCollection.set(IEnvironmentService, environmentService); + const environmentService = new WorkbenchEnvironmentService(this.configuration, this.configuration.execPath); + serviceCollection.set(IWorkbenchEnvironmentService, environmentService); // Log const logService = this._register(this.createLogService(mainProcessService, environmentService)); @@ -206,7 +200,7 @@ class CodeRendererMain extends Disposable { if (connection) { const channel = connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); - fileService.registerProvider(REMOTE_HOST_SCHEME, remoteFileSystemProvider); + fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); } return this.resolveWorkspaceInitializationPayload(environmentService).then(payload => Promise.all([ @@ -231,7 +225,7 @@ class CodeRendererMain extends Disposable { ]).then(services => ({ serviceCollection, logService, storageService: services[1] }))); } - private resolveWorkspaceInitializationPayload(environmentService: EnvironmentService): Promise { + private resolveWorkspaceInitializationPayload(environmentService: IWorkbenchEnvironmentService): Promise { // Multi-root workspace if (this.configuration.workspace) { @@ -301,7 +295,7 @@ class CodeRendererMain extends Disposable { }, error => onUnexpectedError(error)); } - private createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IEnvironmentService, fileService: FileService2, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { + private createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService2, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { const configurationFileService = new ConfigurationFileService(); fileService.whenReady.then(() => configurationFileService.fileService = fileService); @@ -315,7 +309,7 @@ class CodeRendererMain extends Disposable { }); } - private createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IEnvironmentService, logService: ILogService, mainProcessService: IMainProcessService): Promise { + private createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, logService: ILogService, mainProcessService: IMainProcessService): Promise { const globalStorageDatabase = new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')); const storageService = new StorageService(globalStorageDatabase, logService, environmentService); @@ -327,7 +321,7 @@ class CodeRendererMain extends Disposable { }); } - private createLogService(mainProcessService: IMainProcessService, environmentService: IEnvironmentService): ILogService { + private createLogService(mainProcessService: IMainProcessService, environmentService: IWorkbenchEnvironmentService): ILogService { const spdlogService = createSpdLogService(`renderer${this.configuration.windowId}`, this.configuration.logLevel, environmentService.logsPath); const consoleLogService = new ConsoleLogService(this.configuration.logLevel); const logService = new MultiplexLogService([consoleLogService, spdlogService]); diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 00ca4f27e5..ff69daf130 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -38,7 +38,7 @@ import pkg from 'vs/platform/product/node/package'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { coalesce } from 'vs/base/common/arrays'; @@ -86,7 +86,7 @@ export class ElectronWindow extends Disposable { @IMenuService private readonly menuService: IMenuService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IIntegrityService private readonly integrityService: IIntegrityService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { @@ -226,7 +226,7 @@ export class ElectronWindow extends Disposable { this._register(this.editorService.onDidVisibleEditorsChange(() => this.onDidVisibleEditorsChange())); // Listen to editor closing (if we run with --wait) - const filesToWait = this.windowService.getConfiguration().filesToWait; + const filesToWait = this.environmentService.configuration.filesToWait; if (filesToWait) { const resourcesToWaitFor = coalesce(filesToWait.paths.map(p => p.fileUri)); const waitMarkerFile = filesToWait.waitMarkerFileUri; @@ -320,7 +320,7 @@ export class ElectronWindow extends Disposable { // Emit event when vscode is ready this.lifecycleService.when(LifecyclePhase.Ready).then(() => { - ipc.send('vscode:workbenchReady', this.windowService.getCurrentWindowId()); + ipc.send('vscode:workbenchReady', this.windowService.windowId); }); // Integrity warning diff --git a/src/vs/platform/accessibility/node/accessibilityService.ts b/src/vs/workbench/services/accessibility/node/accessibilityService.ts similarity index 87% rename from src/vs/platform/accessibility/node/accessibilityService.ts rename to src/vs/workbench/services/accessibility/node/accessibilityService.ts index 78120934b6..97a9767487 100644 --- a/src/vs/platform/accessibility/node/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/node/accessibilityService.ts @@ -6,7 +6,7 @@ import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { isWindows } from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class AccessibilityService implements IAccessibilityService { _serviceBrand: any; @@ -16,7 +16,7 @@ export class AccessibilityService implements IAccessibilityService { readonly onDidChangeAccessibilitySupport: Event = this._onDidChangeAccessibilitySupport.event; constructor( - @IWindowService private readonly windowService: IWindowService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { } alwaysUnderlineAccessKeys(): Promise { @@ -49,9 +49,8 @@ export class AccessibilityService implements IAccessibilityService { getAccessibilitySupport(): AccessibilitySupport { if (this._accessibilitySupport === AccessibilitySupport.Unknown) { - const config = this.windowService.getConfiguration(); + const config = this.environmentService.configuration; this._accessibilitySupport = (config && config.accessibilitySupport) ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled; - } return this._accessibilitySupport; diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 8a2ae60583..8b9b005b19 100644 --- a/src/vs/workbench/services/backup/common/backup.ts +++ b/src/vs/workbench/services/backup/common/backup.ts @@ -5,13 +5,12 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IResolveContentOptions, IUpdateContentOptions, ITextSnapshot } from 'vs/platform/files/common/files'; +import { IResolveContentOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { ITextBufferFactory } from 'vs/editor/common/model'; export const IBackupFileService = createDecorator('backupFileService'); export const BACKUP_FILE_RESOLVE_OPTIONS: IResolveContentOptions = { acceptTextOnly: true, encoding: 'utf8' }; -export const BACKUP_FILE_UPDATE_OPTIONS: IUpdateContentOptions = { encoding: 'utf8' }; /** * A service that handles any I/O and state associated with the backup system. diff --git a/src/vs/workbench/services/backup/node/backupFileService.ts b/src/vs/workbench/services/backup/node/backupFileService.ts index 35173eb19b..390072562d 100644 --- a/src/vs/workbench/services/backup/node/backupFileService.ts +++ b/src/vs/workbench/services/backup/node/backupFileService.ts @@ -8,14 +8,14 @@ import * as crypto from 'crypto'; import * as pfs from 'vs/base/node/pfs'; import { URI as Uri } from 'vs/base/common/uri'; import { ResourceQueue } from 'vs/base/common/async'; -import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup'; -import { IFileService, ITextSnapshot } from 'vs/platform/files/common/files'; +import { IBackupFileService, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup'; +import { IFileService, ITextSnapshot, TextSnapshotReadable } from 'vs/platform/files/common/files'; import { readToMatchingString } from 'vs/base/node/stream'; import { ITextBufferFactory } from 'vs/editor/common/model'; import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { keys } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export interface IBackupFilesModel { @@ -29,27 +29,6 @@ export interface IBackupFilesModel { clear(): void; } -export class BackupSnapshot implements ITextSnapshot { - private preambleHandled: boolean; - - constructor(private snapshot: ITextSnapshot, private preamble: string) { } - - read(): string | null { - let value = this.snapshot.read(); - if (!this.preambleHandled) { - this.preambleHandled = true; - - if (typeof value === 'string') { - value = this.preamble + value; - } else { - value = this.preamble; - } - } - - return value; - } -} - export class BackupFilesModel implements IBackupFilesModel { private cache: { [resource: string]: number /* version ID */ } = Object.create(null); @@ -114,10 +93,10 @@ export class BackupFileService implements IBackupFileService { private impl: IBackupFileService; constructor( - @IWindowService windowService: IWindowService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IFileService fileService: IFileService ) { - const backupWorkspacePath = windowService.getConfiguration().backupPath; + const backupWorkspacePath = environmentService.configuration.backupPath; if (backupWorkspacePath) { this.impl = new BackupFileServiceImpl(backupWorkspacePath, fileService); } else { @@ -232,7 +211,7 @@ class BackupFileServiceImpl implements IBackupFileService { const preamble = `${resource.toString()}${BackupFileServiceImpl.META_MARKER}`; // Update content with value - return this.fileService.updateContent(backupResource, new BackupSnapshot(content, preamble), BACKUP_FILE_UPDATE_OPTIONS).then(() => model.add(backupResource, versionId)); + return this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble)).then(() => model.add(backupResource, versionId)); }); }); } diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index 9c9f2cdc4b..9619604071 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -14,7 +14,7 @@ import { URI as Uri } from 'vs/base/common/uri'; import { BackupFileService, BackupFilesModel, hashPath } from 'vs/workbench/services/backup/node/backupFileService'; import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { TestContextService, TestTextResourceConfigurationService, TestEnvironmentService, TestWindowService } from 'vs/workbench/test/workbenchTestServices'; +import { TestContextService, TestTextResourceConfigurationService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { DefaultEndOfLine } from 'vs/editor/common/model'; @@ -24,6 +24,8 @@ import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; import { NullLogService } from 'vs/platform/log/common/log'; import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider'; +import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; +import { parseArgs } from 'vs/platform/environment/node/argv'; const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice'); const backupHome = path.join(parentDir, 'Backups'); @@ -38,18 +40,18 @@ const fooBackupPath = path.join(workspaceBackupPath, 'file', hashPath(fooFile)); const barBackupPath = path.join(workspaceBackupPath, 'file', hashPath(barFile)); const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(untitledFile)); -class TestBackupWindowService extends TestWindowService { +class TestBackupEnvironmentService extends WorkbenchEnvironmentService { private config: IWindowConfiguration; constructor(workspaceBackupPath: string) { - super(); + super(parseArgs(process.argv) as IWindowConfiguration, process.execPath); this.config = Object.create(null); this.config.backupPath = workspaceBackupPath; } - getConfiguration(): IWindowConfiguration { + get configuration(): IWindowConfiguration { return this.config; } } @@ -64,9 +66,9 @@ class TestBackupFileService extends BackupFileService { TestEnvironmentService, new TestTextResourceConfigurationService(), )); - const windowService = new TestBackupWindowService(workspaceBackupPath); + const environmentService = new TestBackupEnvironmentService(workspaceBackupPath); - super(windowService, fileService); + super(environmentService, fileService); } public toBackupResource(resource: Uri): Uri { diff --git a/src/vs/workbench/services/broadcast/electron-browser/broadcastService.ts b/src/vs/workbench/services/broadcast/electron-browser/broadcastService.ts index d0fb21fa85..d278ceba6a 100644 --- a/src/vs/workbench/services/broadcast/electron-browser/broadcastService.ts +++ b/src/vs/workbench/services/broadcast/electron-browser/broadcastService.ts @@ -25,7 +25,7 @@ class BroadcastService extends Disposable implements IBroadcastService { ) { super(); - this.windowId = windowService.getCurrentWindowId(); + this.windowId = windowService.windowId; this.registerListeners(); } diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 8a4a16d6d7..10bf4765f5 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -11,8 +11,8 @@ import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/l import { RunOnceScheduler } from 'vs/base/common/async'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; -import { WorkspaceConfigurationModelParser, FolderSettingsModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, IConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration'; +import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; +import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, IConfigurationFileService, MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspaceFolder, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -47,7 +47,7 @@ export class RemoteUserConfiguration extends Disposable { if (environment) { this._userConfiguration.dispose(); this._userConfigurationDisposable.dispose(); - this._userConfiguration = this._register(new UserConfiguration(environment.appSettingsPath, this._configurationFileService)); + this._userConfiguration = this._register(new UserConfiguration(environment.settingsPath, MACHINE_SCOPES, this._configurationFileService)); this._userConfigurationDisposable = this._register(this._userConfiguration.onDidChangeConfiguration(configurationModel => this.onDidUserConfigurationChange(configurationModel))); this._userConfiguration.initialize().then(configurationModel => this.onDidUserConfigurationChange(configurationModel)); } @@ -62,6 +62,10 @@ export class RemoteUserConfiguration extends Disposable { return this._userConfiguration.reload(); } + reprocess(): ConfigurationModel { + return this._userConfiguration.reprocess(); + } + private onDidUserConfigurationChange(configurationModel: ConfigurationModel): void { this.updateCache(configurationModel); this._onDidChangeConfiguration.fire(configurationModel); @@ -74,6 +78,7 @@ export class RemoteUserConfiguration extends Disposable { export class UserConfiguration extends Disposable { + private readonly parser: ConfigurationModelParser; private readonly reloadConfigurationScheduler: RunOnceScheduler; protected readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; @@ -83,10 +88,12 @@ export class UserConfiguration extends Disposable { constructor( private readonly configurationResource: URI, + private readonly scopes: ConfigurationScope[] | undefined, private readonly configurationFileService: IConfigurationFileService ) { super(); + this.parser = new ConfigurationModelParser(this.configurationResource.toString(), this.scopes); this._register(configurationFileService.onFileChanges(e => this.handleFileEvents(e))); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); this._register(toDisposable(() => { @@ -127,14 +134,18 @@ export class UserConfiguration extends Disposable { async reload(): Promise { try { const content = await this.configurationFileService.resolveContent(this.configurationResource); - const parser = new ConfigurationModelParser(this.configurationResource.toString()); - parser.parse(content); - return parser.configurationModel; + this.parser.parseContent(content); + return this.parser.configurationModel; } catch (e) { return new ConfigurationModel(); } } + reprocess(): ConfigurationModel { + this.parser.parse(); + return this.parser.configurationModel; + } + private async onWatchStarted(currentModel: ConfigurationModel): Promise { const configuraitonModel = await this.reload(); const { added, removed, updated } = compare(currentModel, configuraitonModel); @@ -202,6 +213,10 @@ class CachedUserConfiguration extends Disposable { return this.reload(); } + reprocess(): ConfigurationModel { + return this.configurationModel; + } + async reload(): Promise { const content = await this.configurationCache.read(this.key); try { @@ -371,7 +386,7 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable implements IWork errors.onUnexpectedError(error); } } - this.workspaceConfigurationModelParser.parse(contents); + this.workspaceConfigurationModelParser.parseContent(contents); this.consolidate(); } @@ -449,7 +464,7 @@ class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfi const key = this.getKey(workspaceIdentifier); const contents = await this.configurationCache.read(key); this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(key.key); - this.workspaceConfigurationModelParser.parse(contents); + this.workspaceConfigurationModelParser.parseContent(contents); this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel); } catch (e) { } @@ -503,7 +518,7 @@ export interface IFolderConfiguration extends IDisposable { class FileServiceBasedFolderConfiguration extends Disposable implements IFolderConfiguration { - private _folderSettingsModelParser: FolderSettingsModelParser; + private _folderSettingsModelParser: ConfigurationModelParser; private _standAloneConfigurations: ConfigurationModel[]; private _cache: ConfigurationModel; @@ -518,7 +533,7 @@ class FileServiceBasedFolderConfiguration extends Disposable implements IFolderC this.configurationNames = [FOLDER_SETTINGS_NAME /*First one should be settings */, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY]; this.configurationResources = this.configurationNames.map(name => resources.joinPath(this.configurationFolder, `${name}.json`)); - this._folderSettingsModelParser = new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? [ConfigurationScope.RESOURCE] : [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]); + this._folderSettingsModelParser = new ConfigurationModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? FOLDER_SCOPES : WORKSPACE_SCOPES); this._standAloneConfigurations = []; this._cache = new ConfigurationModel(); @@ -544,17 +559,17 @@ class FileServiceBasedFolderConfiguration extends Disposable implements IFolderC // reset this._standAloneConfigurations = []; - this._folderSettingsModelParser.parse(''); + this._folderSettingsModelParser.parseContent(''); // parse if (configurationContents[0]) { - this._folderSettingsModelParser.parse(configurationContents[0]); + this._folderSettingsModelParser.parseContent(configurationContents[0]); } for (let index = 1; index < configurationContents.length; index++) { const contents = configurationContents[index]; if (contents) { const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(this.configurationResources[index].toString(), this.configurationNames[index]); - standAloneConfigurationModelParser.parse(contents); + standAloneConfigurationModelParser.parseContent(contents); this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel); } } @@ -567,7 +582,7 @@ class FileServiceBasedFolderConfiguration extends Disposable implements IFolderC reprocess(): ConfigurationModel { const oldContents = this._folderSettingsModelParser.configurationModel.contents; - this._folderSettingsModelParser.reprocess(); + this._folderSettingsModelParser.parse(); if (!equals(oldContents, this._folderSettingsModelParser.configurationModel.contents)) { this.consolidate(); } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 96eb1792e7..9b89d24745 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -16,7 +16,7 @@ import { isLinux } from 'vs/base/common/platform'; import { ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Configuration, WorkspaceConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, IConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, IConfigurationFileService, machineSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, isSingleFolderWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, useSlashForPath, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; @@ -72,7 +72,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic this.defaultConfiguration = new DefaultConfigurationModel(); this.configurationCache = configurationCache; if (userSettingsResource) { - this.localUserConfiguration = this._register(new UserConfiguration(userSettingsResource, configurationFileService)); + this.localUserConfiguration = this._register(new UserConfiguration(userSettingsResource, undefined, configurationFileService)); this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); } if (remoteAuthority) { @@ -80,7 +80,12 @@ export class WorkspaceService extends Disposable implements IConfigurationServic this._register(this.remoteUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onRemoteUserConfigurationChanged(userConfiguration))); } this.workspaceConfiguration = this._register(new WorkspaceConfiguration(configurationCache, this.configurationFileService)); - this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged())); + this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => { + this.onWorkspaceConfigurationChanged(); + if (this.workspaceConfiguration.loaded) { + this.releaseWorkspaceBarrier(); + } + })); this._register(Registry.as(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas())); this._register(Registry.as(Extensions.Configuration).onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties))); @@ -473,6 +478,9 @@ export class WorkspaceService extends Disposable implements IConfigurationServic this.registerConfigurationSchemas(); if (this.workspace && this._configuration) { this._configuration.updateDefaultConfiguration(this.defaultConfiguration); + if (this.remoteUserConfiguration) { + this._configuration.updateRemoteUserConfiguration(this.remoteUserConfiguration.reprocess()); + } if (this.getWorkbenchState() === WorkbenchState.FOLDER) { this._configuration.updateWorkspaceConfiguration(this.cachedFolderConfigs.get(this.workspace.folders[0].uri)!.reprocess()); } else { @@ -500,6 +508,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic jsonRegistry.registerSchema(defaultSettingsSchemaId, allSettingsSchema); jsonRegistry.registerSchema(userSettingsSchemaId, allSettingsSchema); + jsonRegistry.registerSchema(machineSettingsSchemaId, workspaceSettingsSchema); if (WorkbenchState.WORKSPACE === this.getWorkbenchState()) { const unsupportedWindowSettings = convertToNotSuggestedProperties(windowSettings.properties, localize('unsupportedWindowSetting', "This setting cannot be applied now. It will be applied when you open this folder directly.")); @@ -539,9 +548,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic this.triggerConfigurationChange(workspaceConfigurationChangeEvent, ConfigurationTarget.WORKSPACE); } } - if (this.workspaceConfiguration.loaded) { - this.releaseWorkspaceBarrier(); - } return Promise.resolve(undefined); } diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index d83c61b662..444c20cbea 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -7,6 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; export const FOLDER_CONFIG_FOLDER_NAME = '.azuredatastudio'; export const FOLDER_SETTINGS_NAME = 'settings'; @@ -14,10 +15,15 @@ export const FOLDER_SETTINGS_PATH = `${FOLDER_CONFIG_FOLDER_NAME}/${FOLDER_SETTI export const defaultSettingsSchemaId = 'vscode://schemas/settings/default'; export const userSettingsSchemaId = 'vscode://schemas/settings/user'; +export const machineSettingsSchemaId = 'vscode://schemas/settings/machine'; export const workspaceSettingsSchemaId = 'vscode://schemas/settings/workspace'; export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; export const launchSchemaId = 'vscode://schemas/launch'; +export const MACHINE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; +export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; +export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE]; + export const TASKS_CONFIGURATION_KEY = 'tasks'; export const LAUNCH_CONFIGURATION_KEY = 'launch'; @@ -25,7 +31,6 @@ export const WORKSPACE_STANDALONE_CONFIGURATIONS = Object.create(null); WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${TASKS_CONFIGURATION_KEY}.json`; WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${LAUNCH_CONFIGURATION_KEY}.json`; - export type ConfigurationKey = { type: 'user' | 'workspaces' | 'folder', key: string }; export interface IConfigurationCache { diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 8ae68c3db0..1c7846da15 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -138,7 +138,7 @@ export class ConfigurationEditingService { this.queue = new Queue(); remoteAgentService.getEnvironment().then(environment => { if (environment) { - this.remoteSettingsResource = environment.appSettingsPath; + this.remoteSettingsResource = environment.settingsPath; } }); } @@ -375,7 +375,7 @@ export class ConfigurationEditingService { private async resolveModelReference(resource: URI): Promise> { const exists = await this.fileService.exists(resource); if (!exists) { - await this.fileService.updateContent(resource, '{}', { encoding: 'utf8' }); + await this.textFileService.write(resource, '{}'); } return this.textModelResolverService.createModelReference(resource); } diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 2a1df3e357..cf35ba7c66 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -6,22 +6,21 @@ import { equals } from 'vs/base/common/objects'; import { compare, toValuesTree, IConfigurationChangeEvent, ConfigurationTarget, IConfigurationModel, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { Configuration as BaseConfiguration, ConfigurationModelParser, ConfigurationChangeEvent, ConfigurationModel, AbstractConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { Workspace } from 'vs/platform/workspace/common/workspace'; import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; +import { WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; export class WorkspaceConfigurationModelParser extends ConfigurationModelParser { private _folders: IStoredWorkspaceFolder[] = []; - private _settingsModelParser: FolderSettingsModelParser; + private _settingsModelParser: ConfigurationModelParser; private _launchModel: ConfigurationModel; constructor(name: string) { super(name); - this._settingsModelParser = new FolderSettingsModelParser(name, [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]); + this._settingsModelParser = new ConfigurationModelParser(name, WORKSPACE_SCOPES); this._launchModel = new ConfigurationModel(); } @@ -38,14 +37,14 @@ export class WorkspaceConfigurationModelParser extends ConfigurationModelParser } reprocessWorkspaceSettings(): void { - this._settingsModelParser.reprocess(); + this._settingsModelParser.parse(); } - protected parseRaw(raw: any): IConfigurationModel { + protected doParseRaw(raw: any): IConfigurationModel { this._folders = (raw['folders'] || []) as IStoredWorkspaceFolder[]; - this._settingsModelParser.parse(raw['settings']); + this._settingsModelParser.parseRaw(raw['settings']); this._launchModel = this.createConfigurationModelFrom(raw, 'launch'); - return super.parseRaw(raw); + return super.doParseRaw(raw); } private createConfigurationModelFrom(raw: any, key: string): ConfigurationModel { @@ -67,7 +66,7 @@ export class StandaloneConfigurationModelParser extends ConfigurationModelParser super(name); } - protected parseRaw(raw: any): IConfigurationModel { + protected doParseRaw(raw: any): IConfigurationModel { const contents = toValuesTree(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`)); const scopedContents = Object.create(null); scopedContents[this.scope] = contents; @@ -77,56 +76,6 @@ export class StandaloneConfigurationModelParser extends ConfigurationModelParser } -export class FolderSettingsModelParser extends ConfigurationModelParser { - - private _raw: any; - private _settingsModel: ConfigurationModel; - - constructor(name: string, private scopes: ConfigurationScope[]) { - super(name); - } - - parse(content: string | any): void { - this._raw = typeof content === 'string' ? this.parseContent(content) : content; - this.parseWorkspaceSettings(this._raw); - } - - get configurationModel(): ConfigurationModel { - return this._settingsModel || new ConfigurationModel(); - } - - reprocess(): void { - this.parse(this._raw); - } - - private parseWorkspaceSettings(rawSettings: any): void { - const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const rawWorkspaceSettings = this.filterByScope(rawSettings, configurationProperties, true); - const configurationModel = this.parseRaw(rawWorkspaceSettings); - this._settingsModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides); - } - - private filterByScope(properties: {}, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }, filterOverriddenProperties: boolean): {} { - const result = {}; - for (let key in properties) { - if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) { - result[key] = this.filterByScope(properties[key], configurationProperties, false); - } else { - const scope = this.getScope(key, configurationProperties); - if (this.scopes.indexOf(scope) !== -1) { - result[key] = properties[key]; - } - } - } - return result; - } - - private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope { - const propertySchema = configurationProperties[key]; - return propertySchema && typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW; - } -} - export class Configuration extends BaseConfiguration { constructor( diff --git a/src/vs/workbench/services/configuration/common/jsonEditingService.ts b/src/vs/workbench/services/configuration/common/jsonEditingService.ts index 486b106de5..29b46137a5 100644 --- a/src/vs/workbench/services/configuration/common/jsonEditingService.ts +++ b/src/vs/workbench/services/configuration/common/jsonEditingService.ts @@ -86,7 +86,7 @@ export class JSONEditingService implements IJSONEditingService { private async resolveModelReference(resource: URI): Promise> { const exists = await this.fileService.exists(resource); if (!exists) { - await this.fileService.updateContent(resource, '{}', { encoding: 'utf8' }); + await this.textFileService.write(resource, '{}'); } return this.textModelResolverService.createModelReference(resource); } diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index c567d0d1e7..7c7a5faecf 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -5,10 +5,10 @@ import * as assert from 'assert'; import { join } from 'vs/base/common/path'; import { Registry } from 'vs/platform/registry/common/platform'; -import { FolderSettingsModelParser, WorkspaceConfigurationChangeEvent, StandaloneConfigurationModelParser, AllKeysConfigurationChangeEvent, Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; +import { WorkspaceConfigurationChangeEvent, StandaloneConfigurationModelParser, AllKeysConfigurationChangeEvent, Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; -import { ConfigurationChangeEvent, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; +import { ConfigurationChangeEvent, ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ResourceMap } from 'vs/base/common/map'; @@ -41,33 +41,33 @@ suite('FolderSettingsModelParser', () => { }); test('parse all folder settings', () => { - const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); + const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); - testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' })); + testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' })); assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'window': 'window', 'resource': 'resource' } }); }); test('parse resource folder settings', () => { - const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE]); + const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE]); - testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' })); + testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' })); assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } }); }); test('parse overridable resource settings', () => { - const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE]); + const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE]); - testObject.parse(JSON.stringify({ '[json]': { 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' } })); + testObject.parseContent(JSON.stringify({ '[json]': { 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' } })); assert.deepEqual(testObject.configurationModel.overrides, [{ 'contents': { 'FolderSettingsModelParser': { 'resource': 'resource' } }, 'identifiers': ['json'] }]); }); test('reprocess folder settings excludes application setting', () => { - const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); + const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]); - testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.anotherApplicationSetting': 'executable' })); + testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.anotherApplicationSetting': 'executable' })); assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource', 'anotherApplicationSetting': 'executable' } }); @@ -84,7 +84,7 @@ suite('FolderSettingsModelParser', () => { } }); - testObject.reprocess(); + testObject.parse(); assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } }); }); @@ -95,7 +95,7 @@ suite('StandaloneConfigurationModelParser', () => { test('parse tasks stand alone configuration model', () => { const testObject = new StandaloneConfigurationModelParser('tasks', 'tasks'); - testObject.parse(JSON.stringify({ 'version': '1.1.1', 'tasks': [] })); + testObject.parseContent(JSON.stringify({ 'version': '1.1.1', 'tasks': [] })); assert.deepEqual(testObject.configurationModel.contents, { 'tasks': { 'version': '1.1.1', 'tasks': [] } }); }); diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index 23c2e85568..0d2e5c32b6 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -10,7 +10,7 @@ import * as Types from 'vs/base/common/types'; import { Schemas } from 'vs/base/common/network'; import { toResource } from 'vs/workbench/common/editor'; import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -19,22 +19,21 @@ import { AbstractVariableResolverService } from 'vs/workbench/services/configura import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; -import { ConfiguredInput, IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IWindowService } from 'vs/platform/windows/common/windows'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IProcessEnvironment } from 'vs/base/common/platform'; -export class ConfigurationResolverService extends AbstractVariableResolverService { +export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { static INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g; constructor( - @IWindowService windowService: IWindowService, - @IEditorService editorService: IEditorService, - @IEnvironmentService environmentService: IEnvironmentService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @ICommandService private readonly commandService: ICommandService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IQuickInputService private readonly quickInputService: IQuickInputService + envVariables: IProcessEnvironment, + editorService: IEditorService, + environmentService: IWorkbenchEnvironmentService, + private readonly configurationService: IConfigurationService, + private readonly commandService: ICommandService, + private readonly workspaceContextService: IWorkspaceContextService, + private readonly quickInputService: IQuickInputService ) { super({ getFolderUri: (folderName: string): uri | undefined => { @@ -83,7 +82,7 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic } return undefined; } - }, windowService.getConfiguration().userEnv); + }, envVariables); } public resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary): Promise { @@ -200,7 +199,7 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic private findVariables(object: any, variables: string[]) { if (typeof object === 'string') { let matches; - while ((matches = ConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) { + while ((matches = BaseConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) { if (matches.length === 4) { const command = matches[1]; if (variables.indexOf(command) < 0) { @@ -293,4 +292,16 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic } } -registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); +export class ConfigurationResolverService extends BaseConfigurationResolverService { + + constructor( + @IEditorService editorService: IEditorService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IConfigurationService configurationService: IConfigurationService, + @ICommandService commandService: ICommandService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IQuickInputService quickInputService: IQuickInputService + ) { + super(environmentService.configuration.userEnv, editorService, environmentService, configurationService, commandService, workspaceContextService, quickInputService); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts new file mode 100644 index 0000000000..6f434fa44a --- /dev/null +++ b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IProcessEnvironment } from 'vs/base/common/platform'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; + +export class ConfigurationResolverService extends BaseConfigurationResolverService { + + constructor( + @IEditorService editorService: IEditorService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IConfigurationService configurationService: IConfigurationService, + @ICommandService commandService: ICommandService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IQuickInputService quickInputService: IQuickInputService + ) { + super(process.env as IProcessEnvironment, editorService, environmentService, configurationService, commandService, workspaceContextService, quickInputService); + } +} + +registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); \ No newline at end of file diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index ab396a8ce9..54a1712549 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -11,15 +11,18 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { TestEnvironmentService, TestEditorService, TestContextService, TestWindowService } from 'vs/workbench/test/workbenchTestServices'; +import { TestEditorService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IQuickInputService, IQuickPickItem, QuickPickInput, IPickOptions, Omit, IInputOptions, IQuickInputButton, IQuickPick, IInputBox, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import * as Types from 'vs/base/common/types'; -import { IWindowService, IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { EditorType } from 'vs/editor/common/editorCommon'; import { Selection } from 'vs/editor/common/core/selection'; +import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; +import { parseArgs } from 'vs/platform/environment/node/argv'; +import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; const mockLineNumber = 10; class TestEditorServiceWithActiveEditor extends TestEditorService { @@ -38,7 +41,7 @@ class TestEditorServiceWithActiveEditor extends TestEditorService { suite('Configuration Resolver Service', () => { let configurationResolverService: IConfigurationResolverService | null; let envVariables: { [key: string]: string } = { key1: 'Value for key1', key2: 'Value for key2' }; - let windowService: IWindowService; + let environmentService: IWorkbenchEnvironmentService; let mockCommandService: MockCommandService; let editorService: TestEditorServiceWithActiveEditor; let workspace: IWorkspaceFolder; @@ -48,14 +51,14 @@ suite('Configuration Resolver Service', () => { mockCommandService = new MockCommandService(); editorService = new TestEditorServiceWithActiveEditor(); quickInputService = new MockQuickInputService(); - windowService = new MockWindowService(envVariables); + environmentService = new MockWorkbenchEnvironmentService(envVariables); workspace = { uri: uri.parse('file:///VSCode/workspaceLocation'), name: 'hey', index: 0, toResource: (path: string) => uri.file(path) }; - configurationResolverService = new ConfigurationResolverService(windowService, editorService, TestEnvironmentService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(), quickInputService); + configurationResolverService = new ConfigurationResolverService(editorService, environmentService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(), quickInputService); }); teardown(() => { @@ -134,7 +137,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -151,7 +154,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz'); }); @@ -168,7 +171,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); if (platform.isWindows) { assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz'); } else { @@ -189,7 +192,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); if (platform.isWindows) { assert.strictEqual(service.resolve(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2'); } else { @@ -223,7 +226,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); }); @@ -233,7 +236,7 @@ suite('Configuration Resolver Service', () => { editor: {} }); - let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); }); @@ -246,7 +249,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.throws(() => service.resolve(workspace, 'abc ${env} xyz')); assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz')); @@ -626,13 +629,13 @@ class MockInputsConfigurationService extends TestConfigurationService { } } -class MockWindowService extends TestWindowService { +class MockWorkbenchEnvironmentService extends WorkbenchEnvironmentService { constructor(private env: platform.IProcessEnvironment) { - super(); + super(parseArgs(process.argv) as IWindowConfiguration, process.execPath); } - getConfiguration(): IWindowConfiguration { + get configuration(): IWindowConfiguration { return { userEnv: this.env } as IWindowConfiguration; } } diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index f60cf09465..85ab8dd3d2 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -8,7 +8,7 @@ import { IWindowService, INativeOpenDialogOptions, OpenDialogOptions, IURIToOpen import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; @@ -28,7 +28,7 @@ export class FileDialogService implements IFileDialogService { @IWindowService private readonly windowService: IWindowService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IHistoryService private readonly historyService: IHistoryService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService @@ -257,7 +257,7 @@ export class FileDialogService implements IFileDialogService { } private getSchemeFilterForWindow() { - return !this.windowService.getConfiguration().remoteAuthority ? Schemas.file : REMOTE_HOST_SCHEME; + return !this.environmentService.configuration.remoteAuthority ? Schemas.file : REMOTE_HOST_SCHEME; } private getFileSystemSchema(options: { availableFileSystems?: string[], defaultUri?: URI }): string { @@ -266,7 +266,7 @@ export class FileDialogService implements IFileDialogService { } -function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean { +function isUntitledWorkspace(path: URI, environmentService: IWorkbenchEnvironmentService): boolean { return resources.isEqualOrParent(path, environmentService.untitledWorkspacesHome); } diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index 3a2fa7057b..4298b53bb5 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -12,7 +12,6 @@ import { URI } from 'vs/base/common/uri'; import { isWindows } from 'vs/base/common/platform'; import { ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; -import { IWindowService } from 'vs/platform/windows/common/windows'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -20,10 +19,11 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { Schemas } from 'vs/base/common/network'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { RemoteFileDialogContext } from 'vs/workbench/common/contextkeys'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; interface FileQuickPickItem extends IQuickPickItem { uri: URI; @@ -45,28 +45,29 @@ export class RemoteFileDialog { private allowFolderSelection: boolean; private remoteAuthority: string | undefined; private requiresTrailing: boolean; - private userValue: string; private scheme: string = REMOTE_HOST_SCHEME; private shouldOverwriteFile: boolean = false; - private autoComplete: string; private contextKey: IContextKey; + private userEnteredPathSegment: string; + private autoCompletePathSegment: string; + private activeItem: FileQuickPickItem; + constructor( @IFileService private readonly fileService: IFileService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IWindowService private readonly windowService: IWindowService, @ILabelService private readonly labelService: ILabelService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @INotificationService private readonly notificationService: INotificationService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IModelService private readonly modelService: IModelService, @IModeService private readonly modeService: IModeService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IContextKeyService contextKeyService: IContextKeyService ) { - this.remoteAuthority = this.windowService.getConfiguration().remoteAuthority; + this.remoteAuthority = this.environmentService.configuration.remoteAuthority; this.contextKey = RemoteFileDialogContext.bindTo(contextKeyService); } @@ -134,7 +135,7 @@ export class RemoteFileDialog { private remoteUriFrom(path: string): URI { path = path.replace(/\\/g, '/'); - return URI.from({ scheme: this.scheme, authority: this.remoteAuthority, path }); + return resources.toLocalResource(URI.from({ scheme: this.scheme, path }), this.remoteAuthority); } private getScheme(defaultUri: URI | undefined, available: string[] | undefined): string { @@ -179,7 +180,7 @@ export class RemoteFileDialog { } this.acceptButton = { iconPath: this.getDialogIcons('accept'), tooltip: this.options.title }; - return new Promise((resolve) => { + return new Promise(async (resolve) => { this.filePickBox = this.quickInputService.createQuickPick(); this.filePickBox.matchOnLabel = false; this.filePickBox.autoFocusOnList = false; @@ -187,6 +188,8 @@ export class RemoteFileDialog { let isResolving = false; let isAcceptHandled = false; this.currentFolder = homedir; + this.userEnteredPathSegment = ''; + this.autoCompletePathSegment = ''; this.filePickBox.buttons = [this.acceptButton]; this.filePickBox.onDidTriggerButton(_ => { // accept button @@ -232,22 +235,25 @@ export class RemoteFileDialog { this.filePickBox.onDidChangeActive(i => { isAcceptHandled = false; // update input box to match the first selected item - if (i.length === 1) { - this.setAutoComplete(this.userValue, resources.basename(this.remoteUriFrom(this.userValue)), i[0], true); + if ((i.length === 1) && this.isChangeFromUser()) { + this.setAutoComplete(this.constructFullUserPath(), this.userEnteredPathSegment, i[0], true); } }); this.filePickBox.onDidChangeValue(async value => { // onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything - if (!this.autoComplete || (value !== this.autoComplete)) { - if (value !== this.userValue) { + if (this.isChangeFromUser()) { + if (value !== this.constructFullUserPath()) { this.filePickBox.validationMessage = undefined; this.shouldOverwriteFile = false; const valueUri = this.remoteUriFrom(this.trimTrailingSlash(this.filePickBox.value)); + let isUpdate = false; if (!resources.isEqual(this.remoteUriFrom(this.trimTrailingSlash(this.pathFromUri(this.currentFolder))), valueUri, true)) { - await this.tryUpdateItems(value, this.remoteUriFrom(this.filePickBox.value)); + isUpdate = await this.tryUpdateItems(value, this.remoteUriFrom(this.filePickBox.value)); + } + if (!isUpdate) { + this.setActiveItems(value); } - this.setActiveItems(value); } else { this.filePickBox.activeItems = []; } @@ -262,16 +268,27 @@ export class RemoteFileDialog { this.filePickBox.show(); this.contextKey.set(true); - this.updateItems(homedir, trailing); + await this.updateItems(homedir, trailing); if (trailing) { this.filePickBox.valueSelection = [this.filePickBox.value.length - trailing.length, this.filePickBox.value.length - ext.length]; } else { this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; } - this.userValue = this.filePickBox.value; }); } + private isChangeFromUser(): boolean { + if ((this.filePickBox.value === this.pathAppend(this.currentFolder, this.userEnteredPathSegment + this.autoCompletePathSegment)) + && (this.activeItem === (this.filePickBox.activeItems ? this.filePickBox.activeItems[0] : undefined))) { + return false; + } + return true; + } + + private constructFullUserPath(): string { + return this.pathAppend(this.currentFolder, this.userEnteredPathSegment); + } + private async onDidAccept(): Promise { // Check if Open Local has been selected const selectedItems: ReadonlyArray = this.filePickBox.selectedItems; @@ -279,6 +296,7 @@ export class RemoteFileDialog { if (this.options.availableFileSystems && (this.options.availableFileSystems.length > 1)) { this.options.availableFileSystems.shift(); } + this.options.defaultUri = undefined; if (this.requiresTrailing) { return this.fileDialogService.showSaveDialog(this.options).then(result => { return result; @@ -331,14 +349,14 @@ export class RemoteFileDialog { } } else if (navigateValue) { // Try to navigate into the folder - this.updateItems(navigateValue); + await this.updateItems(navigateValue); } else { // validation error. Path does not exist. } return Promise.resolve(undefined); } - private async tryUpdateItems(value: string, valueUri: URI) { + private async tryUpdateItems(value: string, valueUri: URI): Promise { if (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true))) { let stat: IFileStat | undefined; try { @@ -346,11 +364,12 @@ export class RemoteFileDialog { } catch (e) { // do nothing } - if (stat && stat.isDirectory && (resources.basename(valueUri) !== '.')) { - this.updateItems(valueUri); + if (stat && stat.isDirectory && (resources.basename(valueUri) !== '.') && this.endsWithSlash(value)) { + await this.updateItems(valueUri); + return true; } else { const inputUriDirname = resources.dirname(valueUri); - if (!resources.isEqual(this.currentFolder, inputUriDirname, true)) { + if (!resources.isEqual(this.remoteUriFrom(this.trimTrailingSlash(this.pathFromUri(this.currentFolder))), inputUriDirname, true)) { let statWithoutTrailing: IFileStat | undefined; try { statWithoutTrailing = await this.fileService.resolve(inputUriDirname); @@ -358,59 +377,70 @@ export class RemoteFileDialog { // do nothing } if (statWithoutTrailing && statWithoutTrailing.isDirectory && (resources.basename(valueUri) !== '.')) { - this.updateItems(inputUriDirname, resources.basename(valueUri)); + await this.updateItems(inputUriDirname, resources.basename(valueUri)); + return true; } } } } + return false; } private setActiveItems(value: string) { - if (!this.userValue || (value !== this.userValue.substring(0, value.length))) { - const inputBasename = resources.basename(this.remoteUriFrom(value)); + const inputBasename = resources.basename(this.remoteUriFrom(value)); + // Make sure that the folder whose children we are currently viewing matches the path in the input + const userPath = this.constructFullUserPath(); + if (userPath === value.substring(0, userPath.length)) { let hasMatch = false; for (let i = 0; i < this.filePickBox.items.length; i++) { const item = this.filePickBox.items[i]; if (this.setAutoComplete(value, inputBasename, item)) { - this.filePickBox.activeItems = [item]; hasMatch = true; break; } } if (!hasMatch) { + this.userEnteredPathSegment = inputBasename; + this.autoCompletePathSegment = ''; this.filePickBox.activeItems = []; } } else { - this.userValue = value; - this.autoComplete = ''; + if (inputBasename !== resources.basename(this.currentFolder)) { + this.userEnteredPathSegment = inputBasename; + } else { + this.userEnteredPathSegment = ''; + } + this.autoCompletePathSegment = ''; } } private setAutoComplete(startingValue: string, startingBasename: string, quickPickItem: FileQuickPickItem, force: boolean = false): boolean { const itemBasename = (quickPickItem.label === '..') ? quickPickItem.label : resources.basename(quickPickItem.uri); - const itemPathLabel = (itemBasename === '..') ? this.pathAppend(this.currentFolder, itemBasename) : this.pathFromUri(quickPickItem.uri); - if (this.trimTrailingSlash(this.filePickBox.value) !== itemPathLabel) { - // Either foce the autocomplete, or the old value should be one smaller than the new value and match the new value. - if (!force && (itemBasename.length >= startingBasename.length) && (itemBasename.substr(0, startingBasename.length).toLowerCase() === startingBasename.toLowerCase())) { - this.userValue = startingValue; - const autoCompleteValue = itemBasename.substr(startingBasename.length); - this.autoComplete = startingValue + autoCompleteValue; - this.insertText(this.autoComplete, autoCompleteValue); - this.filePickBox.valueSelection = [startingValue.length, this.filePickBox.value.length]; - return true; - } else if (force) { - this.userValue = this.pathFromUri(this.currentFolder, true); - this.autoComplete = this.pathAppend(this.currentFolder, itemBasename); - this.filePickBox.valueSelection = [this.userValue.length, this.filePickBox.value.length]; - // use insert text to preserve undo buffer - this.insertText(this.autoComplete, itemBasename); - this.filePickBox.valueSelection = [this.filePickBox.value.length - itemBasename.length, this.filePickBox.value.length]; - return true; - } + // Either force the autocomplete, or the old value should be one smaller than the new value and match the new value. + if (!force && (itemBasename.length >= startingBasename.length) && equalsIgnoreCase(itemBasename.substr(0, startingBasename.length), startingBasename)) { + this.userEnteredPathSegment = startingBasename; + this.activeItem = quickPickItem; + // Changing the active items will trigger the onDidActiveItemsChanged. Clear the autocomplete first, then set it after. + this.autoCompletePathSegment = ''; + this.filePickBox.activeItems = [quickPickItem]; + this.autoCompletePathSegment = itemBasename.substr(startingBasename.length); + this.insertText(startingValue + this.autoCompletePathSegment, this.autoCompletePathSegment); + this.filePickBox.valueSelection = [startingValue.length, this.filePickBox.value.length]; + return true; + } else if (force && (quickPickItem.label !== (this.userEnteredPathSegment + this.autoCompletePathSegment))) { + this.userEnteredPathSegment = ''; + this.autoCompletePathSegment = itemBasename; + this.activeItem = quickPickItem; + this.filePickBox.valueSelection = [this.pathFromUri(this.currentFolder, true).length, this.filePickBox.value.length]; + // use insert text to preserve undo buffer + this.insertText(this.pathAppend(this.currentFolder, itemBasename), itemBasename); + this.filePickBox.valueSelection = [this.filePickBox.value.length - itemBasename.length, this.filePickBox.value.length]; + return true; + } else { + this.userEnteredPathSegment = startingBasename; + this.autoCompletePathSegment = ''; + return false; } - this.userValue = startingValue; - this.autoComplete = ''; - return false; } private insertText(wholeValue: string, insertText: string) { @@ -528,15 +558,15 @@ export class RemoteFileDialog { return Promise.resolve(true); } - private updateItems(newFolder: URI, trailing?: string) { - this.currentFolder = newFolder; - this.userValue = this.pathFromUri(newFolder); - this.autoComplete = ''; + private async updateItems(newFolder: URI, trailing?: string) { + this.userEnteredPathSegment = trailing ? trailing : ''; + this.autoCompletePathSegment = ''; this.filePickBox.valueSelection = [0, this.filePickBox.value.length]; const newValue = trailing ? this.pathFromUri(resources.joinPath(newFolder, trailing)) : this.pathFromUri(newFolder, true); + this.currentFolder = this.remoteUriFrom(this.pathFromUri(newFolder, true)); this.insertText(newValue, newValue); this.filePickBox.busy = true; - this.createItems(this.currentFolder).then(items => { + return this.createItems(this.currentFolder).then(items => { this.filePickBox.items = items; if (this.allowFolderSelection) { this.filePickBox.activeItems = []; @@ -560,8 +590,9 @@ export class RemoteFileDialog { } private pathAppend(uri: URI, additional: string): string { - if (additional === '..') { - return this.pathFromUri(uri) + this.labelService.getSeparator(uri.scheme, uri.authority) + additional; + if ((additional === '..') || (additional === '.')) { + const basePath = this.pathFromUri(uri); + return basePath + (this.endsWithSlash(basePath) ? '' : this.labelService.getSeparator(uri.scheme, uri.authority)) + additional; } else { return this.pathFromUri(resources.joinPath(uri, additional)); } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index de20e16dad..d646c19c6d 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -207,6 +207,11 @@ export interface IEditorGroupsService { */ readonly whenRestored: Promise; + /** + * Find out if the editor group service has editors to restore from a previous session. + */ + readonly willRestoreEditors: boolean; + /** * Get all groups that are currently visible in the editor area optionally * sorted by being most recent active or grid order. Will sort by creation diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts new file mode 100644 index 0000000000..84c501ee1e --- /dev/null +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; + +export const IWorkbenchEnvironmentService = createDecorator('environmentService'); + +export interface IWorkbenchEnvironmentService extends IEnvironmentService { + _serviceBrand: any; + + readonly configuration: IWindowConfiguration; +} diff --git a/src/vs/workbench/services/environment/node/environmentService.ts b/src/vs/workbench/services/environment/node/environmentService.ts new file mode 100644 index 0000000000..db2a229c03 --- /dev/null +++ b/src/vs/workbench/services/environment/node/environmentService.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + +export class WorkbenchEnvironmentService extends EnvironmentService implements IWorkbenchEnvironmentService { + + _serviceBrand: any; + + constructor( + private _configuration: IWindowConfiguration, + execPath: string + ) { + super(_configuration, execPath); + } + + get configuration(): IWindowConfiguration { + return this._configuration; + } +} diff --git a/src/vs/workbench/services/extensionManagement/node/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/node/extensionEnablementService.ts index 74c47865b0..8db1f8421d 100644 --- a/src/vs/workbench/services/extensionManagement/node/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/node/extensionEnablementService.ts @@ -10,11 +10,10 @@ import { IExtensionManagementService, DidUninstallExtensionEvent, IExtensionEnab import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWindowService } from 'vs/platform/windows/common/windows'; import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -33,9 +32,8 @@ export class ExtensionEnablementService extends Disposable implements IExtension constructor( @IStorageService storageService: IStorageService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IWindowService private readonly windowService: IWindowService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, ) { @@ -138,7 +136,7 @@ export class ExtensionEnablementService extends Disposable implements IExtension if (Array.isArray(disabledExtensions)) { return disabledExtensions.some(id => areSameExtensions({ id }, extension.identifier)); } - if (this.windowService.getConfiguration().remoteAuthority) { + if (this.environmentService.configuration.remoteAuthority) { const server = isUIExtension(extension.manifest, this.configurationService) ? this.extensionManagementServerService.localExtensionManagementServer : this.extensionManagementServerService.remoteExtensionManagementServer; return this.extensionManagementServerService.getExtensionManagementServer(extension.location) !== server; } diff --git a/src/vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test.ts index f15cedb706..aa1f950c78 100644 --- a/src/vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/electron-browser/extensionEnablementService.test.ts @@ -9,14 +9,12 @@ import { ExtensionEnablementService } from 'vs/workbench/services/extensionManag import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Emitter } from 'vs/base/common/event'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IStorageService, InMemoryStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IExtensionContributions, ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWindowService } from 'vs/platform/windows/common/windows'; -import { TestWindowService } from 'vs/workbench/test/workbenchTestServices'; function storageService(instantiationService: TestInstantiationService): IStorageService { let service = instantiationService.get(IStorageService); @@ -34,11 +32,13 @@ function storageService(instantiationService: TestInstantiationService): IStorag export class TestExtensionEnablementService extends ExtensionEnablementService { constructor(instantiationService: TestInstantiationService) { - super(storageService(instantiationService), instantiationService.get(IWorkspaceContextService), - instantiationService.get(IEnvironmentService) || instantiationService.stub(IEnvironmentService, {} as IEnvironmentService), + super( + storageService(instantiationService), + instantiationService.get(IWorkspaceContextService), + instantiationService.get(IWorkbenchEnvironmentService) || instantiationService.stub(IWorkbenchEnvironmentService, { configuration: Object.create(null) } as IWorkbenchEnvironmentService), instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidInstallExtension: new Emitter().event, onDidUninstallExtension: new Emitter().event } as IExtensionManagementService), - instantiationService.stub(IWindowService, new TestWindowService()), instantiationService.get(IConfigurationService), instantiationService.get(IExtensionManagementServerService)); + instantiationService.get(IConfigurationService), instantiationService.get(IExtensionManagementServerService)); } public reset(): void { @@ -437,26 +437,26 @@ suite('ExtensionEnablementService Test', () => { }); test('test canChangeEnablement return false when extensions are disabled in environment', () => { - instantiationService.stub(IEnvironmentService, { disableExtensions: true } as IEnvironmentService); + instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); }); test('test canChangeEnablement return false when the extension is disabled in environment', () => { - instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false); }); test('test canChangeEnablement return true for system extensions when extensions are disabled in environment', () => { - instantiationService.stub(IEnvironmentService, { disableExtensions: true } as IEnvironmentService); + instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); const extension = aLocalExtension('pub.a', undefined, ExtensionType.System); assert.equal(testObject.canChangeEnablement(extension), true); }); test('test canChangeEnablement return false for system extension when extension is disabled in environment', () => { - instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); const extension = aLocalExtension('pub.a', undefined, ExtensionType.System); assert.ok(!testObject.canChangeEnablement(extension)); @@ -464,7 +464,7 @@ suite('ExtensionEnablementService Test', () => { test('test extension is disabled when disabled in enviroment', async () => { const extension = aLocalExtension('pub.a'); - instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService); + instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, onDidInstallExtension: didInstallEvent.event, getInstalled: () => Promise.resolve([extension, aLocalExtension('pub.b')]) } as IExtensionManagementService); testObject = new TestExtensionEnablementService(instantiationService); assert.ok(!testObject.isEnabled(extension)); diff --git a/src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts similarity index 100% rename from src/vs/workbench/services/extensions/node/extensionDescriptionRegistry.ts rename to src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts diff --git a/src/vs/workbench/services/extensions/common/extensionDevOptions.ts b/src/vs/workbench/services/extensions/common/extensionDevOptions.ts index c3416a958d..b4902eb1f6 100644 --- a/src/vs/workbench/services/extensions/common/extensionDevOptions.ts +++ b/src/vs/workbench/services/extensions/common/extensionDevOptions.ts @@ -18,15 +18,13 @@ export function parseExtensionDevOptions(environmentService: IEnvironmentService let isExtensionDevHost = environmentService.isExtensionDevelopment; let debugOk = true; - let extDevLoc = environmentService.extensionDevelopmentLocationURI; - if (Array.isArray(extDevLoc)) { - for (let x of extDevLoc) { + let extDevLocs = environmentService.extensionDevelopmentLocationURI; + if (extDevLocs) { + for (let x of extDevLocs) { if (x.scheme !== Schemas.file) { debugOk = false; } } - } else if (extDevLoc && extDevLoc.scheme !== Schemas.file) { - debugOk = false; } let isExtensionDevDebug = debugOk && typeof environmentService.debugExtensionHost.port === 'number'; diff --git a/src/vs/workbench/services/extensions/common/extensionHostDebug.ts b/src/vs/workbench/services/extensions/common/extensionHostDebug.ts index 60d48ac5b8..de19f9a2d9 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostDebug.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostDebug.ts @@ -10,6 +10,22 @@ import { IRemoteConsoleLog } from 'vs/base/common/console'; export const IExtensionHostDebugService = createDecorator('extensionHostDebugService'); +export interface IAttachSessionEvent { + sessionId: string; + subId?: string; + port: number; +} + +export interface ILogToSessionEvent { + sessionId: string; + log: IRemoteConsoleLog; +} + +export interface ITerminateSessionEvent { + sessionId: string; + subId?: string; +} + export interface IExtensionHostDebugService { _serviceBrand: any; @@ -19,12 +35,12 @@ export interface IExtensionHostDebugService { close(resource: URI): void; onClose: Event; - attachSession(id: string, port: number): void; - onAttachSession: Event<{ id: string, port: number }>; + attachSession(sessionId: string, port: number, subId?: string): void; + onAttachSession: Event; - logToSession(id: string, log: IRemoteConsoleLog): void; - onLogToSession: Event<{ id: string, log: IRemoteConsoleLog }>; + logToSession(sessionId: string, log: IRemoteConsoleLog): void; + onLogToSession: Event; - terminateSession(id: string): void; - onTerminateSession: Event; + terminateSession(sessionId: string, subId?: string): void; + onTerminateSession: Event; } \ No newline at end of file diff --git a/src/vs/workbench/services/extensions/node/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts similarity index 100% rename from src/vs/workbench/services/extensions/node/extensionHostProtocol.ts rename to src/vs/workbench/services/extensions/common/extensionHostProtocol.ts diff --git a/src/vs/workbench/services/extensions/node/lazyPromise.ts b/src/vs/workbench/services/extensions/common/lazyPromise.ts similarity index 100% rename from src/vs/workbench/services/extensions/node/lazyPromise.ts rename to src/vs/workbench/services/extensions/common/lazyPromise.ts diff --git a/src/vs/workbench/services/extensions/node/rpcProtocol.ts b/src/vs/workbench/services/extensions/common/rpcProtocol.ts similarity index 99% rename from src/vs/workbench/services/extensions/node/rpcProtocol.ts rename to src/vs/workbench/services/extensions/common/rpcProtocol.ts index d683d104a2..5ff035cab0 100644 --- a/src/vs/workbench/services/extensions/node/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/common/rpcProtocol.ts @@ -11,7 +11,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IURITransformer, transformIncomingURIs } from 'vs/base/common/uriIpc'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise'; +import { LazyPromise } from 'vs/workbench/services/extensions/common/lazyPromise'; import { IRPCProtocol, ProxyIdentifier, getStringIdentifierForProxy } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -739,7 +739,6 @@ class MessageIO { } private static _serializeReplyOKVSBuffer(req: number, res: VSBuffer): VSBuffer { - let len = 0; len += MessageBuffer.sizeVSBuffer(res); @@ -809,10 +808,10 @@ const enum MessageType { Cancel = 6, ReplyOKEmpty = 7, ReplyOKBuffer = 8, - ReplyOKVSBuffer = 8, - ReplyOKJSON = 9, - ReplyErrError = 10, - ReplyErrEmpty = 11, + ReplyOKVSBuffer = 9, + ReplyOKJSON = 10, + ReplyErrError = 11, + ReplyErrEmpty = 12, } const enum ArgType { diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index dcbe8ca21a..399a45d821 100644 --- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -294,30 +294,19 @@ export class CachedExtensionScanner { // Always load developed extensions while extensions development let developedExtensions: Promise = Promise.resolve([]); - if (environmentService.isExtensionDevelopment) { - - if (Array.isArray(environmentService.extensionDevelopmentLocationURI)) { - - const extDescsP = environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { - return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), log - ); - }); - developedExtensions = Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { - let extDesc: IExtensionDescription[] = []; - for (let eds of extDescArrays) { - extDesc = extDesc.concat(eds); - } - return extDesc; - }); - - } else if (environmentService.extensionDevelopmentLocationURI) { - if (environmentService.extensionDevelopmentLocationURI.scheme === Schemas.file) { - developedExtensions = ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, commit, locale, devMode, originalFSPath(environmentService.extensionDevelopmentLocationURI), false, true, translations), log - ); + if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI) { + const extDescsP = environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { + return ExtensionScanner.scanOneOrMultipleExtensions( + new ExtensionScannerInput(version, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), log + ); + }); + developedExtensions = Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { + let extDesc: IExtensionDescription[] = []; + for (let eds of extDescArrays) { + extDesc = extDesc.concat(eds); } - } + return extDesc; + }); } return Promise.all([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index af0c8bfd8d..ff1c3153a2 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -22,7 +22,7 @@ import { findFreePort, randomPort } from 'vs/base/node/ports'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; import { generateRandomPipeName, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; @@ -32,7 +32,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; -import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/node/extensionHostProtocol'; +import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { parseExtensionDevOptions } from '../common/extensionDevOptions'; @@ -78,7 +78,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { @IWindowsService private readonly _windowsService: IWindowsService, @IWindowService private readonly _windowService: IWindowService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILogService private readonly _logService: ILogService, @ILabelService private readonly _labelService: ILabelService, @@ -124,10 +124,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { private matchesExtDevLocations(resource: URI): boolean { const extDevLocs = this._environmentService.extensionDevelopmentLocationURI; - if (Array.isArray(extDevLocs)) { + if (extDevLocs) { return extDevLocs.some(extDevLoc => isEqual(extDevLoc, resource)); - } else if (extDevLocs) { - return isEqual(extDevLocs, resource); } return false; } @@ -240,7 +238,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { // Help in case we fail to start it let startupTimeoutHandle: any; - if (!this._environmentService.isBuilt && !this._windowService.getConfiguration().remoteAuthority || this._isExtensionDevHost) { + if (!this._environmentService.isBuilt && !this._environmentService.configuration.remoteAuthority || this._isExtensionDevHost) { startupTimeoutHandle = setTimeout(() => { const msg = this._isExtensionDevDebugBrk ? nls.localize('extensionHostProcess.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts index c92bd39062..a5e7a2dffd 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts @@ -6,7 +6,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug'; +import { IExtensionHostDebugService, IAttachSessionEvent, ITerminateSessionEvent, ILogToSessionEvent } from 'vs/workbench/services/extensions/common/extensionHostDebug'; import { URI } from 'vs/base/common/uri'; import { IRemoteConsoleLog } from 'vs/base/common/console'; import { ipcRenderer as ipc } from 'electron'; @@ -16,10 +16,8 @@ interface IReloadBroadcast { resource: string; } -interface IAttachSessionBroadcast { +interface IAttachSessionBroadcast extends IAttachSessionEvent { type: 'vscode:extensionAttach'; - id: string; - port: number; } interface ICloseBroadcast { @@ -27,15 +25,12 @@ interface ICloseBroadcast { resource: string; } -interface ILogToSessionBroadcast { +interface ILogToSessionBroadcast extends ILogToSessionEvent { type: 'vscode:extensionLog'; - id: string; - log: IRemoteConsoleLog; } -interface ITerminateSessionBroadcast { +interface ITerminateSessionBroadcast extends ITerminateSessionEvent { type: 'vscode:extensionTerminate'; - id: string; } const CHANNEL = 'vscode:extensionHostDebug'; @@ -46,30 +41,32 @@ class ExtensionHostDebugService implements IExtensionHostDebugService { private windowId: number; private readonly _onReload = new Emitter(); private readonly _onClose = new Emitter(); - private readonly _onAttachSession = new Emitter<{ id: string, port: number }>(); - private readonly _onLogToSession = new Emitter<{ id: string, log: IRemoteConsoleLog }>(); - private readonly _onTerminateSession = new Emitter(); + private readonly _onAttachSession = new Emitter(); + private readonly _onLogToSession = new Emitter(); + private readonly _onTerminateSession = new Emitter(); constructor( @IWindowService readonly windowService: IWindowService, ) { - this.windowId = windowService.getCurrentWindowId(); + this.windowId = windowService.windowId; ipc.on(CHANNEL, (_: unknown, broadcast: IReloadBroadcast | ICloseBroadcast | IAttachSessionBroadcast | ILogToSessionBroadcast | ITerminateSessionBroadcast) => { - if (broadcast.type === 'vscode:extensionReload') { - this._onReload.fire(URI.parse(broadcast.resource)); - } - if (broadcast.type === 'vscode:extensionCloseExtensionHost') { - this._onClose.fire(URI.parse(broadcast.resource)); - } - if (broadcast.type === 'vscode:extensionAttach') { - this._onAttachSession.fire({ id: broadcast.id, port: broadcast.port }); - } - if (broadcast.type === 'vscode:extensionLog') { - this._onLogToSession.fire({ id: broadcast.id, log: broadcast.log }); - } - if (broadcast.type === 'vscode:extensionTerminate') { - this._onTerminateSession.fire(broadcast.id); + switch (broadcast.type) { + case 'vscode:extensionReload': + this._onReload.fire(URI.parse(broadcast.resource)); + break; + case 'vscode:extensionCloseExtensionHost': + this._onClose.fire(URI.parse(broadcast.resource)); + break; + case 'vscode:extensionAttach': + this._onAttachSession.fire(broadcast); + break; + case 'vscode:extensionLog': + this._onLogToSession.fire(broadcast); + break; + case 'vscode:extensionTerminate': + this._onTerminateSession.fire(broadcast); + break; } }); } @@ -96,38 +93,40 @@ class ExtensionHostDebugService implements IExtensionHostDebugService { return this._onClose.event; } - attachSession(id: string, port: number): void { + attachSession(sessionId: string, port: number, subId?: string): void { ipc.send(CHANNEL, this.windowId, { type: 'vscode:extensionAttach', - id, - port + sessionId, + port, + subId }); } - get onAttachSession(): Event<{ id: string, port: number }> { + get onAttachSession(): Event { return this._onAttachSession.event; } - logToSession(id: string, log: IRemoteConsoleLog): void { + logToSession(sessionId: string, log: IRemoteConsoleLog): void { ipc.send(CHANNEL, this.windowId, { type: 'vscode:extensionLog', - id, + sessionId, log }); } - get onLogToSession(): Event<{ id: string, log: IRemoteConsoleLog }> { + get onLogToSession(): Event { return this._onLogToSession.event; } - terminateSession(id: string): void { + terminateSession(sessionId: string, subId?: string): void { ipc.send(CHANNEL, this.windowId, { type: 'vscode:extensionTerminate', - id + sessionId, + subId }); } - get onTerminateSession(): Event { + get onTerminateSession(): Event { return this._onTerminateSession.event; } } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts index 6e2b318141..61301d81e9 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts @@ -16,7 +16,7 @@ import { ProfileSession } from 'vs/workbench/services/extensions/common/extensio import { IExtensionHostStarter } from 'vs/workbench/services/extensions/electron-browser/extensionHost'; import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/node/rpcProtocol'; +import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as nls from 'vs/nls'; @@ -27,6 +27,7 @@ import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/wor import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUntitledResourceInput } from 'vs/workbench/common/editor'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { VSBuffer } from 'vs/base/common/buffer'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -154,7 +155,7 @@ export class ExtensionHostProcessManager extends Disposable { let b = Buffer.alloc(SIZE, Math.random() % 256); const sw = StopWatch.create(true); - await proxy.$test_up(b); + await proxy.$test_up(VSBuffer.wrap(b)); sw.stop(); return ExtensionHostProcessManager._convert(SIZE, sw.elapsed()); } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionManagementServerService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionManagementServerService.ts index 2cb6b5c0d9..f2bb48b6b6 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionManagementServerService.ts @@ -33,7 +33,7 @@ export class ExtensionManagementServerService implements IExtensionManagementSer const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { const extensionManagementService = new ExtensionManagementChannelClient(remoteAgentConnection.getChannel('extensions')); - this.remoteExtensionManagementServer = { authority: remoteAgentConnection.remoteAuthority, extensionManagementService, label: remoteAgentConnection.remoteAuthority }; + this.remoteExtensionManagementServer = { authority: remoteAgentConnection.remoteAuthority, extensionManagementService, label: localize('remote', "Remote") }; } } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index f82a06d930..92c4c05010 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -13,7 +13,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import * as perf from 'vs/base/common/performance'; import { isEqualOrParent } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { EnablementState, IExtensionEnablementService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { BetterMergeId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -26,8 +26,8 @@ import { IWindowService, IWindowsService } from 'vs/platform/windows/common/wind import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, ProfileSession, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser, schema } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; -import { ResponsiveState } from 'vs/workbench/services/extensions/node/rpcProtocol'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { CachedExtensionScanner, Logger } from 'vs/workbench/services/extensions/electron-browser/cachedExtensionScanner'; import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/electron-browser/extensionHostProcessManager'; import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -97,7 +97,7 @@ export class ExtensionService extends Disposable implements IExtensionService { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotificationService private readonly _notificationService: INotificationService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService, @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService, @@ -112,7 +112,7 @@ export class ExtensionService extends Disposable implements IExtensionService { e.join(this.activateByEvent(`onFileSystem:${e.scheme}`)); })); - this._extensionHostLogsLocation = URI.file(path.join(this._environmentService.logsPath, `exthost${this._windowService.getCurrentWindowId()}`)); + this._extensionHostLogsLocation = URI.file(path.join(this._environmentService.logsPath, `exthost${this._windowService.windowId}`)); this._registry = new ExtensionDescriptionRegistry([]); this._installedExtensionsReady = new Barrier(); this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment; @@ -189,7 +189,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[]): Promise { - if (this._windowService.getConfiguration().remoteAuthority) { + if (this._environmentService.configuration.remoteAuthority) { return; } @@ -283,7 +283,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } public canAddExtension(extension: IExtensionDescription): boolean { - if (this._windowService.getConfiguration().remoteAuthority) { + if (this._environmentService.configuration.remoteAuthority) { return false; } @@ -303,7 +303,7 @@ export class ExtensionService extends Disposable implements IExtensionService { } public canRemoveExtension(extension: IExtensionDescription): boolean { - if (this._windowService.getConfiguration().remoteAuthority) { + if (this._environmentService.configuration.remoteAuthority) { return false; } @@ -647,16 +647,14 @@ export class ExtensionService extends Disposable implements IExtensionService { private isExtensionUnderDevelopment(extension: IExtensionDescription): boolean { if (this._environmentService.isExtensionDevelopment) { - const extDevLoc = this._environmentService.extensionDevelopmentLocationURI; - const extLocation = extension.extensionLocation; - if (Array.isArray(extDevLoc)) { - for (let p of extDevLoc) { + const extDevLocs = this._environmentService.extensionDevelopmentLocationURI; + if (extDevLocs) { + const extLocation = extension.extensionLocation; + for (let p of extDevLocs) { if (isEqualOrParent(extLocation, p)) { return true; } } - } else if (extDevLoc) { - return isEqualOrParent(extLocation, extDevLoc); } } return false; diff --git a/src/vs/workbench/services/extensions/node/extensionHostMain.ts b/src/vs/workbench/services/extensions/node/extensionHostMain.ts index c50c2dbda3..18775d01b5 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostMain.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostMain.ts @@ -10,67 +10,56 @@ import { Counter } from 'vs/base/common/numbers'; import { URI, setUriThrowOnMissingScheme } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { IEnvironment, IInitData, MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; +import { IInitData, MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; -import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; -import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; +import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; +import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { ILogService } from 'vs/platform/log/common/log'; // we don't (yet) throw when extensions parse // uris that have no scheme setUriThrowOnMissingScheme(false); -const nativeExit = process.exit.bind(process); -function patchProcess(allowExit: boolean) { - process.exit = function (code?: number) { - if (allowExit) { - exit(code); - } else { - const err = new Error('An extension called process.exit() and this was prevented.'); - console.warn(err.stack); - } - } as (code?: number) => never; - - process.crash = function () { - const err = new Error('An extension called process.crash() and this was prevented.'); - console.warn(err.stack); - }; +export interface IExitFn { + (code?: number): any; } -export function exit(code?: number) { - nativeExit(code); +export interface IConsolePatchFn { + (mainThreadConsole: MainThreadConsoleShape): any; +} + +export interface ILogServiceFn { + (initData: IInitData): ILogService; } export class ExtensionHostMain { - private _isTerminating: boolean; - private readonly _environment: IEnvironment; + private readonly _exitFn: IExitFn; private readonly _extensionService: ExtHostExtensionService; private readonly _extHostLogService: ExtHostLogService; private disposables: IDisposable[] = []; private _searchRequestIdProvider: Counter; - constructor(protocol: IMessagePassingProtocol, initData: IInitData) { + constructor(protocol: IMessagePassingProtocol, initData: IInitData, exitFn: IExitFn, consolePatchFn: IConsolePatchFn, logServiceFn: ILogServiceFn) { this._isTerminating = false; + this._exitFn = exitFn; const uriTransformer: IURITransformer | null = null; const rpcProtocol = new RPCProtocol(protocol, null, uriTransformer); // ensure URIs are transformed and revived initData = this.transform(initData, rpcProtocol); - this._environment = initData.environment; - const allowExit = !!this._environment.extensionTestsLocationURI; // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708) - patchProcess(allowExit); - - this._patchPatchedConsole(rpcProtocol.getProxy(MainContext.MainThreadConsole)); + // allow to patch console + consolePatchFn(rpcProtocol.getProxy(MainContext.MainThreadConsole)); // services - this._extHostLogService = new ExtHostLogService(initData.logLevel, initData.logsLocation.fsPath); + this._extHostLogService = new ExtHostLogService(logServiceFn(initData), initData.logsLocation.fsPath); this.disposables.push(this._extHostLogService); this._searchRequestIdProvider = new Counter(); @@ -80,7 +69,7 @@ export class ExtensionHostMain { this._extHostLogService.trace('initData', initData); const extHostConfiguraiton = new ExtHostConfiguration(rpcProtocol.getProxy(MainContext.MainThreadConfiguration), extHostWorkspace); - this._extensionService = new ExtHostExtensionService(nativeExit, initData, rpcProtocol, extHostWorkspace, extHostConfiguraiton, this._extHostLogService); + this._extensionService = new ExtHostExtensionService(exitFn, initData, rpcProtocol, extHostWorkspace, extHostConfiguraiton, initData.environment, this._extHostLogService); // error forwarding and stack trace scanning Error.stackTraceLimit = 100; // increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) @@ -116,18 +105,6 @@ export class ExtensionHostMain { }); } - private _patchPatchedConsole(mainThreadConsole: MainThreadConsoleShape): void { - // The console is already patched to use `process.send()` - const nativeProcessSend = process.send!; - process.send = (...args: any[]) => { - if (args.length === 0 || !args[0] || args[0].type !== '__$console') { - return nativeProcessSend.apply(process, args); - } - - mainThreadConsole.$logExtensionHostMessage(args[0]); - }; - } - terminate(): void { if (this._isTerminating) { // we are already shutting down... @@ -145,7 +122,7 @@ export class ExtensionHostMain { // Give extensions 1 second to wrap up any async dispose, then exit in at most 4 seconds setTimeout(() => { - Promise.race([timeout(4000), extensionsDeactivated]).then(() => exit(), () => exit()); + Promise.race([timeout(4000), extensionsDeactivated]).finally(() => this._exitFn()); }, 1000); } @@ -153,7 +130,10 @@ export class ExtensionHostMain { initData.extensions.forEach((ext) => (ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation))); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); initData.environment.appSettingsHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appSettingsHome)); - initData.environment.extensionDevelopmentLocationURI = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.extensionDevelopmentLocationURI)); + const extDevLocs = initData.environment.extensionDevelopmentLocationURI; + if (extDevLocs) { + initData.environment.extensionDevelopmentLocationURI = extDevLocs.map(url => URI.revive(rpcProtocol.transformIncomingURIs(url))); + } initData.environment.extensionTestsLocationURI = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.extensionTestsLocationURI)); initData.environment.globalStorageHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.globalStorageHome)); initData.environment.userHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.userHome)); diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcess.ts b/src/vs/workbench/services/extensions/node/extensionHostProcess.ts index 44c33f5d00..6009d6f9db 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcess.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcess.ts @@ -11,10 +11,12 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import product from 'vs/platform/product/node/product'; -import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; -import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage } from 'vs/workbench/services/extensions/node/extensionHostProtocol'; -import { exit, ExtensionHostMain } from 'vs/workbench/services/extensions/node/extensionHostMain'; +import { IInitData, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; +import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { ExtensionHostMain, IExitFn, ILogServiceFn } from 'vs/workbench/services/extensions/node/extensionHostMain'; import { VSBuffer } from 'vs/base/common/buffer'; +import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; +import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; // With Electron 2.x and node.js 8.x the "natives" module // can cause a native crash (see https://github.com/nodejs/node/issues/19891 and @@ -34,6 +36,39 @@ import { VSBuffer } from 'vs/base/common/buffer'; }; })(); +// custom process.exit logic... +const nativeExit: IExitFn = process.exit.bind(process); +function patchProcess(allowExit: boolean) { + process.exit = function (code?: number) { + if (allowExit) { + nativeExit(code); + } else { + const err = new Error('An extension called process.exit() and this was prevented.'); + console.warn(err.stack); + } + } as (code?: number) => never; + + process.crash = function () { + const err = new Error('An extension called process.crash() and this was prevented.'); + console.warn(err.stack); + }; +} + +// use IPC messages to forward console-calls +function patchPatchedConsole(mainThreadConsole: MainThreadConsoleShape): void { + // The console is already patched to use `process.send()` + const nativeProcessSend = process.send!; + process.send = (...args: any[]) => { + if (args.length === 0 || !args[0] || args[0].type !== '__$console') { + return nativeProcessSend.apply(process, args); + } + + mainThreadConsole.$logExtensionHostMessage(args[0]); + }; +} + +const createLogService: ILogServiceFn = initData => createSpdLogService(ExtensionHostLogFileName, initData.logLevel, initData.logsLocation.fsPath); + interface IRendererConnection { protocol: IMessagePassingProtocol; initData: IInitData; @@ -42,7 +77,7 @@ interface IRendererConnection { // This calls exit directly in case the initialization is not finished and we need to exit // Otherwise, if initialization completed we go to extensionHostMain.terminate() let onTerminate = function () { - exit(); + nativeExit(); }; function _createExtHostProtocol(): Promise { @@ -149,7 +184,7 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise { // connect to main side return connectToRenderer(protocol); }).then(renderer => { + const { initData } = renderer; // setup things - const extensionHostMain = new ExtensionHostMain(renderer.protocol, renderer.initData); + patchProcess(!!initData.environment.extensionTestsLocationURI); // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708) + + const extensionHostMain = new ExtensionHostMain(renderer.protocol, initData, nativeExit, patchPatchedConsole, createLogService); + + // rewrite onTerminate-function to be a proper shutdown onTerminate = () => extensionHostMain.terminate(); }).catch(err => console.error(err)); diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 25989b5f94..cb9e94d193 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -13,11 +13,11 @@ import * as cp from 'child_process'; import { assign } from 'vs/base/common/objects'; import { endsWith } from 'vs/base/common/strings'; -import { IExtHostWorkspaceProvider } from 'vs/workbench/api/node/extHostWorkspace'; -import { ExtHostConfigProvider } from 'vs/workbench/api/node/extHostConfiguration'; +import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; +import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { ProxyAgent } from 'vscode-proxy-agent'; import { MainThreadTelemetryShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; +import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts index 201f641202..a7cf7ac082 100644 --- a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts @@ -8,7 +8,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { Emitter, Event } from 'vs/base/common/event'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; +import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { VSBuffer } from 'vs/base/common/buffer'; suite('RPCProtocol', () => { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 7ffee8c1c4..b2b133d77d 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -7,7 +7,7 @@ import * as paths from 'vs/base/common/path'; import * as fs from 'fs'; import * as os from 'os'; import * as assert from 'assert'; -import { FileOperation, FileOperationEvent, IContent, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, ICreateFileOptions, IContentData, ITextSnapshot, ILegacyFileService, IFileStatWithMetadata, IFileService, IFileSystemProvider, etag } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationEvent, IContent, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IWriteTextFileOptions, ICreateFileOptions, IContentData, ITextSnapshot, ILegacyFileService, IFileStatWithMetadata, IFileService, IFileSystemProvider, etag } from 'vs/platform/files/common/files'; import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/fileConstants'; import * as objects from 'vs/base/common/objects'; import { timeout } from 'vs/base/common/async'; @@ -382,7 +382,7 @@ export class LegacyFileService extends Disposable implements ILegacyFileService //#region File Writing - updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Promise { + updateContent(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise { if (options.writeElevated) { return this.doUpdateContentElevated(resource, value, options); } @@ -390,7 +390,7 @@ export class LegacyFileService extends Disposable implements ILegacyFileService return this.doUpdateContent(resource, value, options); } - private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Promise { + private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise { const absolutePath = this.toAbsolutePath(resource); // 1.) check file for writing @@ -501,12 +501,12 @@ export class LegacyFileService extends Disposable implements ILegacyFileService }); } - private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Promise { + private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise { const absolutePath = this.toAbsolutePath(resource); // 1.) check file for writing return this.checkFileBeforeWriting(absolutePath, options, options.overwriteReadonly /* ignore readonly if we overwrite readonly, this is handled via sudo later */).then(exists => { - const writeOptions: IUpdateContentOptions = objects.assign(Object.create(null), options); + const writeOptions: IWriteTextFileOptions = objects.assign(Object.create(null), options); writeOptions.writeElevated = false; writeOptions.encoding = this._encoding.getWriteEncoding(resource, options.encoding).encoding; @@ -602,7 +602,7 @@ export class LegacyFileService extends Disposable implements ILegacyFileService //#region Helpers - private checkFileBeforeWriting(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): Promise { + private checkFileBeforeWriting(absolutePath: string, options: IWriteTextFileOptions = Object.create(null), ignoreReadonly?: boolean): Promise { return pfs.exists(absolutePath).then(exists => { if (exists) { return pfs.stat(absolutePath).then(stat => { @@ -656,7 +656,7 @@ export class LegacyFileService extends Disposable implements ILegacyFileService }); } - private readOnlyError(options: IUpdateContentOptions): Promise { + private readOnlyError(options: IWriteTextFileOptions): Promise { return Promise.reject(new FileOperationError( nls.localize('fileReadOnlyError', "File is Read Only"), FileOperationResult.FILE_READ_ONLY, diff --git a/src/vs/workbench/services/files/node/remoteFileService.ts b/src/vs/workbench/services/files/node/remoteFileService.ts index 8ca55746ae..8d1e3f9061 100644 --- a/src/vs/workbench/services/files/node/remoteFileService.ts +++ b/src/vs/workbench/services/files/node/remoteFileService.ts @@ -11,12 +11,24 @@ import { IDecodeStreamOptions, toDecodeStream, encodeStream } from 'vs/base/node import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileSystemProvider, IResolveContentOptions, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileSystemProvider, IResolveContentOptions, IStreamContent, ITextSnapshot, IWriteTextFileOptions, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; import { createReadableOfProvider, createReadableOfSnapshot, createWritableOfProvider } from 'vs/workbench/services/files/node/streams'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +class StringSnapshot implements ITextSnapshot { + private _value: string | null; + constructor(value: string) { + this._value = value; + } + read(): string | null { + let ret = this._value; + this._value = null; + return ret; + } +} + export class LegacyRemoteFileService extends LegacyFileService { private readonly _provider: Map; @@ -181,7 +193,7 @@ export class LegacyRemoteFileService extends LegacyFileService { } } - updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise { + updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { if (resource.scheme === Schemas.file) { return super.updateContent(resource, value, options); } else { diff --git a/src/vs/workbench/services/files/node/streams.ts b/src/vs/workbench/services/files/node/streams.ts index ed5e82917e..ebce14cc72 100644 --- a/src/vs/workbench/services/files/node/streams.ts +++ b/src/vs/workbench/services/files/node/streams.ts @@ -127,7 +127,7 @@ function createSimpleReadable(provider: IFileSystemProvider, resource: URI, posi return; } this._readOperation = provider.readFile!(resource).then(data => { - this.push(data.slice(position)); + this.push(Buffer.from(data.buffer, data.byteOffset, data.byteLength).slice(position)); this.push(null); }, err => { this.emit('error', err); diff --git a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts index ed368102c5..411454d985 100644 --- a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts @@ -8,7 +8,7 @@ import * as path from 'vs/base/common/path'; import * as os from 'os'; import * as assert from 'assert'; import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; -import { FileOperation, FileOperationEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; +import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { URI as uri } from 'vs/base/common/uri'; import * as uuid from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; @@ -53,118 +53,6 @@ suite('LegacyFileService', () => { return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); - test('createFile', () => { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - const contents = 'Hello World'; - const resource = uri.file(path.join(testDir, 'test.txt')); - return service.createFile(resource, contents).then(s => { - assert.equal(s.name, 'test.txt'); - assert.equal(fs.existsSync(s.resource.fsPath), true); - assert.equal(fs.readFileSync(s.resource.fsPath), contents); - - assert.ok(event); - assert.equal(event.resource.fsPath, resource.fsPath); - assert.equal(event.operation, FileOperation.CREATE); - assert.equal(event.target!.resource.fsPath, resource.fsPath); - toDispose.dispose(); - }); - }); - - test('createFile (does not overwrite by default)', function () { - const contents = 'Hello World'; - const resource = uri.file(path.join(testDir, 'test.txt')); - - fs.writeFileSync(resource.fsPath, ''); // create file - - return service.createFile(resource, contents).then(undefined, error => { - assert.ok(error); - }); - }); - - test('createFile (allows to overwrite existing)', function () { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - const contents = 'Hello World'; - const resource = uri.file(path.join(testDir, 'test.txt')); - - fs.writeFileSync(resource.fsPath, ''); // create file - - return service.createFile(resource, contents, { overwrite: true }).then(s => { - assert.equal(s.name, 'test.txt'); - assert.equal(fs.existsSync(s.resource.fsPath), true); - assert.equal(fs.readFileSync(s.resource.fsPath), contents); - - assert.ok(event); - assert.equal(event.resource.fsPath, resource.fsPath); - assert.equal(event.operation, FileOperation.CREATE); - assert.equal(event.target!.resource.fsPath, resource.fsPath); - toDispose.dispose(); - }); - }); - - test('updateContent', () => { - const resource = uri.file(path.join(testDir, 'small.txt')); - - return service.resolveContent(resource).then(c => { - assert.equal(c.value, 'Small File'); - - c.value = 'Updates to the small file'; - - return service.updateContent(c.resource, c.value).then(c => { - assert.equal(fs.readFileSync(resource.fsPath), 'Updates to the small file'); - }); - }); - }); - - test('updateContent (ITextSnapShot)', function () { - const resource = uri.file(path.join(testDir, 'small.txt')); - - return service.resolveContent(resource).then(c => { - assert.equal(c.value, 'Small File'); - - const model = TextModel.createFromString('Updates to the small file'); - - return service.updateContent(c.resource, model.createSnapshot()).then(c => { - assert.equal(fs.readFileSync(resource.fsPath), 'Updates to the small file'); - - model.dispose(); - }); - }); - }); - - test('updateContent (large file)', function () { - const resource = uri.file(path.join(testDir, 'lorem.txt')); - - return service.resolveContent(resource).then(c => { - const newValue = c.value + c.value; - c.value = newValue; - - return service.updateContent(c.resource, c.value).then(c => { - assert.equal(fs.readFileSync(resource.fsPath), newValue); - }); - }); - }); - - test('updateContent (large file, ITextSnapShot)', function () { - const resource = uri.file(path.join(testDir, 'lorem.txt')); - - return service.resolveContent(resource).then(c => { - const newValue = c.value + c.value; - const model = TextModel.createFromString(newValue); - - return service.updateContent(c.resource, model.createSnapshot()).then(c => { - assert.equal(fs.readFileSync(resource.fsPath), newValue); - }); - }); - }); - test('updateContent - use encoding (UTF 16 BE)', function () { const resource = uri.file(path.join(testDir, 'small.txt')); const encoding = 'utf16be'; @@ -251,6 +139,67 @@ suite('LegacyFileService', () => { }); }); + test('updateContent - UTF 8 BOMs', function () { + + // setup + const _id = uuid.generateUuid(); + const _testDir = path.join(parentDir, _id); + const _sourceDir = getPathFromAmdModule(require, './fixtures/service'); + const resource = uri.file(path.join(testDir, 'index.html')); + + const fileService = new FileService2(new NullLogService()); + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); + + const _service = new LegacyFileService( + fileService, + new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))), + TestEnvironmentService, + new TestTextResourceConfigurationService() + ); + + return pfs.copy(_sourceDir, _testDir).then(() => { + return pfs.readFile(resource.fsPath).then(data => { + assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); + + const model = TextModel.createFromString('Hello Bom'); + + // Update content: UTF_8 => UTF_8_BOM + return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8_with_bom }).then(() => { + return pfs.readFile(resource.fsPath).then(data => { + assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); + + // Update content: PRESERVE BOM when using UTF-8 + model.setValue('Please stay Bom'); + return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).then(() => { + return pfs.readFile(resource.fsPath).then(data => { + assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); + + // Update content: REMOVE BOM + model.setValue('Go away Bom'); + return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8, overwriteEncoding: true }).then(() => { + return pfs.readFile(resource.fsPath).then(data => { + assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); + + // Update content: BOM comes not back + model.setValue('Do not come back Bom'); + return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).then(() => { + return pfs.readFile(resource.fsPath).then(data => { + assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); + + model.dispose(); + _service.dispose(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + test('resolveContent - large file', function () { const resource = uri.file(path.join(testDir, 'lorem.txt')); @@ -259,7 +208,7 @@ suite('LegacyFileService', () => { }); }); - test('Files are intermingled #38331', function () { + test('resolveContent - Files are intermingled #38331', function () { let resource1 = uri.file(path.join(testDir, 'lorem.txt')); let resource2 = uri.file(path.join(testDir, 'some_utf16le.css')); let value1: string; @@ -359,7 +308,7 @@ suite('LegacyFileService', () => { }); }); - test('options - encoding override (parent)', function () { + test('resolveContent - options - encoding override (parent)', function () { // setup const _id = uuid.generateUuid(); @@ -381,7 +330,6 @@ suite('LegacyFileService', () => { const fileService = new FileService2(new NullLogService()); fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - const _service = new LegacyFileService( fileService, new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))), @@ -402,7 +350,7 @@ suite('LegacyFileService', () => { }); }); - test('options - encoding override (extension)', function () { + test('resolveContent - options - encoding override (extension)', function () { // setup const _id = uuid.generateUuid(); @@ -444,67 +392,6 @@ suite('LegacyFileService', () => { }); }); - test('UTF 8 BOMs', function () { - - // setup - const _id = uuid.generateUuid(); - const _testDir = path.join(parentDir, _id); - const _sourceDir = getPathFromAmdModule(require, './fixtures/service'); - const resource = uri.file(path.join(testDir, 'index.html')); - - const fileService = new FileService2(new NullLogService()); - fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - - const _service = new LegacyFileService( - fileService, - new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))), - TestEnvironmentService, - new TestTextResourceConfigurationService() - ); - - return pfs.copy(_sourceDir, _testDir).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); - - const model = TextModel.createFromString('Hello Bom'); - - // Update content: UTF_8 => UTF_8_BOM - return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8_with_bom }).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); - - // Update content: PRESERVE BOM when using UTF-8 - model.setValue('Please stay Bom'); - return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); - - // Update content: REMOVE BOM - model.setValue('Go away Bom'); - return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8, overwriteEncoding: true }).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); - - // Update content: BOM comes not back - model.setValue('Do not come back Bom'); - return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).then(() => { - return pfs.readFile(resource.fsPath).then(data => { - assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); - - model.dispose(); - _service.dispose(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - test('resolveContent - from position (ASCII)', function () { const resource = uri.file(path.join(testDir, 'small.txt')); diff --git a/src/vs/workbench/services/files2/browser/fileService2.ts b/src/vs/workbench/services/files2/browser/fileService2.ts new file mode 100644 index 0000000000..725b40098b --- /dev/null +++ b/src/vs/workbench/services/files2/browser/fileService2.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; +import { URI } from 'vs/base/common/uri'; +import { IResolveContentOptions, IStreamContent, IStringStream, IContent, IFileSystemProvider, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { basename } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { localize } from 'vs/nls'; + +// TODO@ben temporary for testing only +export class FileService3 extends FileService2 { + + async resolveContent(resource: URI, options?: IResolveContentOptions): Promise { + return this.resolveStreamContent(resource, options).then(streamContent => { + return new Promise((resolve, reject) => { + + const result: IContent = { + resource: streamContent.resource, + name: streamContent.name, + mtime: streamContent.mtime, + etag: streamContent.etag, + encoding: streamContent.encoding, + isReadonly: streamContent.isReadonly, + size: streamContent.size, + value: '' + }; + + streamContent.value.on('data', chunk => result.value += chunk); + streamContent.value.on('error', err => reject(err)); + streamContent.value.on('end', () => resolve(result)); + + return result; + }); + }); + } + + async resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise { + const provider = await this.withProvider(resource); + if (provider && provider.readFile) { + const listeners: { [type: string]: any[]; } = Object.create(null); + const stringStream: IStringStream = { + on(event: string, callback: any): void { + listeners[event] = listeners[event] || []; + listeners[event].push(callback); + } + }; + const stat = await this.resolve(resource, { resolveMetadata: true }); + + const r: IStreamContent = { + mtime: stat.mtime, + size: stat.size, + etag: stat.etag, + value: stringStream, + resource: resource, + encoding: 'utf8', + name: basename(resource) + }; + + provider.readFile(resource).then((contents) => { + const str = VSBuffer.wrap(contents).toString(); + listeners['data'].forEach((listener) => listener(str)); + listeners['end'].forEach((listener) => listener()); + }); + + return r; + } + + return super.resolveStreamContent(resource, options); + } + + protected throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { + // we really do not want to allow for changes currently + throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/files2/common/fileService2.ts b/src/vs/workbench/services/files2/common/fileService2.ts index ac572eecb1..453b9162b2 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService } from 'vs/platform/files/common/files'; +import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IWriteTextFileOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService, IWriteFileOptions } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -14,7 +14,7 @@ import { TernarySearchTree } from 'vs/base/common/map'; import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays'; import { getBaseLabel } from 'vs/base/common/labels'; import { ILogService } from 'vs/platform/log/common/log'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable } from 'vs/base/common/buffer'; export class FileService2 extends Disposable implements IFileService { @@ -129,7 +129,7 @@ export class FileService2 extends Disposable implements IFileService { return !!(provider && (provider.capabilities & capability)); } - private async withProvider(resource: URI): Promise { + protected async withProvider(resource: URI): Promise { // Assert path is absolute if (!isAbsolutePath(resource)) { @@ -200,7 +200,7 @@ export class FileService2 extends Disposable implements IFileService { const stat = await provider.stat(resource); - return await this.toFileStat(provider, resource, stat, undefined, resolveMetadata, (stat, siblings) => { + return this.toFileStat(provider, resource, stat, undefined, resolveMetadata, (stat, siblings) => { // check for recursive resolving if (Boolean(trie.findSuperstr(stat.resource.toString()) || trie.get(stat.resource.toString()))) { @@ -295,38 +295,68 @@ export class FileService2 extends Disposable implements IFileService { return this._legacy.encoding; } - async createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise { - const useLegacy = true; // can only disable this when encoding is sorted out - if (useLegacy) { - return this.joinOnLegacy.then(legacy => legacy.createFile(resource, content, options)); - } - - const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); + async createFile2(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise { // validate overwrite const overwrite = !!(options && options.overwrite); - if (await this.exists(resource)) { - if (!overwrite) { - throw new FileOperationError(localize('fileExists', "File to create already exists ({0})", resource.toString(true)), FileOperationResult.FILE_MODIFIED_SINCE, options); + if (!overwrite && await this.exists(resource)) { + throw new FileOperationError(localize('fileExists', "File to create already exists ({0})", resource.toString(true)), FileOperationResult.FILE_MODIFIED_SINCE, options); + } + + // do write into file (this will create it too) + const fileStat = await this.writeFile(resource, bufferOrReadable); + + // events + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); + + return fileStat; + } + + async writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise { + const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); + + // validate write + const exists = await this.exists(resource); + if (exists) { + const stat = await provider.stat(resource); + + // file cannot be directory + if ((stat.type & FileType.Directory) !== 0) { + throw new Error(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", resource.toString())); } - // delete otherwise - await this.del(resource, { recursive: true }); + // Dirty write prevention: if the file on disk has been changed and does not match our expected + // mtime and etag, we bail out to prevent dirty writing. + // + // First, we check for a mtime that is in the future before we do more checks. The assumption is + // that only the mtime is an indicator for a file that has changd on disk. + // + // Second, if the mtime has advanced, we compare the size of the file on disk with our previous + // one using the etag() function. Relying only on the mtime check has prooven to produce false + // positives due to file system weirdness (especially around remote file systems). As such, the + // check for size is a weaker check because it can return a false negative if the file has changed + // but to the same length. This is a compromise we take to avoid having to produce checksums of + // the file content for comparison which would be much slower to compute. + if (options && typeof options.mtime === 'number' && typeof options.etag === 'string' && options.mtime < stat.mtime && options.etag !== etag(stat.size, options.mtime)) { + throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options); + } } try { - // mkdir recursively - await this.mkdirp(provider, dirname(resource)); - - // create file: buffered - if (hasOpenReadWriteCloseCapability(provider)) { - await this.doWriteBuffered(provider, resource, VSBuffer.fromString(content || '')); + // mkdir recursively as needed + if (!exists) { + await this.mkdirp(provider, dirname(resource)); } - // create file: unbuffered + // write file: buffered + if (hasOpenReadWriteCloseCapability(provider)) { + await this.doWriteBuffered(provider, resource, bufferOrReadable instanceof VSBuffer ? bufferToReadable(bufferOrReadable) : bufferOrReadable); + } + + // write file: unbuffered else if (hasReadWriteCapability(provider)) { - await this.doWriteUnbuffered(provider, resource, VSBuffer.fromString(content || ''), overwrite); + await this.doWriteUnbuffered(provider, resource, bufferOrReadable); } // give up if provider has insufficient capabilities @@ -334,25 +364,25 @@ export class FileService2 extends Disposable implements IFileService { return Promise.reject('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed to support creating a file.'); } } catch (error) { - throw new FileOperationError(localize('err.create', "Failed to create file {0}", resource.toString(false)), toFileOperationResult(error), options); + throw new FileOperationError(localize('err.write', "Failed to write file {0}", resource.toString(false)), toFileOperationResult(error), options); } - // events - const fileStat = await this.resolve(resource, { resolveMetadata: true }); - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); + return this.resolve(resource, { resolveMetadata: true }); + } - return fileStat; + createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise { + return this.joinOnLegacy.then(legacy => legacy.createFile(resource, content, options)); } resolveContent(resource: URI, options?: IResolveContentOptions): Promise { return this.joinOnLegacy.then(legacy => legacy.resolveContent(resource, options)); } - resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise { + async resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise { return this.joinOnLegacy.then(legacy => legacy.resolveStreamContent(resource, options)); } - updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise { + updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { return this.joinOnLegacy.then(legacy => legacy.updateContent(resource, value, options)); } @@ -414,16 +444,16 @@ export class FileService2 extends Disposable implements IFileService { !(hasOpenReadWriteCloseCapability(sourceProvider) || hasReadWriteCapability(sourceProvider)) || !(hasOpenReadWriteCloseCapability(targetProvider) || hasReadWriteCapability(targetProvider)) ) { - return Promise.reject('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed to support copy.'); + throw new Error('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed to support copy.'); } // when copying via buffer/unbuffered, we have to manually // traverse the source if it is a folder and not a file const sourceFile = await this.resolve(source); if (sourceFile.isDirectory) { - return this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target, overwrite).then(() => mode); + return this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target).then(() => mode); } else { - return this.doCopyFile(sourceProvider, source, targetProvider, target, overwrite).then(() => mode); + return this.doCopyFile(sourceProvider, source, targetProvider, target).then(() => mode); } } @@ -444,7 +474,7 @@ export class FileService2 extends Disposable implements IFileService { } } - private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, overwrite?: boolean): Promise { + private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI): Promise { // copy: source (buffered) => target (buffered) if (hasOpenReadWriteCloseCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) { @@ -453,7 +483,7 @@ export class FileService2 extends Disposable implements IFileService { // copy: source (buffered) => target (unbuffered) if (hasOpenReadWriteCloseCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) { - return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target, !!overwrite); + return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target); } // copy: source (unbuffered) => target (buffered) @@ -463,11 +493,11 @@ export class FileService2 extends Disposable implements IFileService { // copy: source (unbuffered) => target (unbuffered) if (hasReadWriteCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) { - return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target, !!overwrite); + return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target); } } - private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI, overwrite?: boolean): Promise { + private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI): Promise { // create folder in target await targetProvider.mkdir(targetFolder); @@ -477,9 +507,9 @@ export class FileService2 extends Disposable implements IFileService { await Promise.all(sourceFolder.children.map(async sourceChild => { const targetChild = joinPath(targetFolder, sourceChild.name); if (sourceChild.isDirectory) { - return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild, overwrite); + return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild); } else { - return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild, overwrite); + return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild); } })); } @@ -494,7 +524,7 @@ export class FileService2 extends Disposable implements IFileService { const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); isCaseChange = isPathCaseSensitive ? false : isEqual(source, target, true /* ignore case */); if (!isCaseChange && isEqualOrParent(target, source, !isPathCaseSensitive)) { - return Promise.reject(new Error(localize('unableToMoveCopyError1', "Unable to move/copy when source path is equal or parent of target path"))); + throw new Error(localize('unableToMoveCopyError1', "Unable to move/copy when source path is equal or parent of target path")); } } @@ -510,7 +540,7 @@ export class FileService2 extends Disposable implements IFileService { // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw if (sourceProvider === targetProvider && isEqualOrParent(source, target, !isPathCaseSensitive)) { - return Promise.reject(new Error(localize('unableToMoveCopyError3', "Unable to move/copy. File would replace folder it is contained in."))); + throw new Error(localize('unableToMoveCopyError3', "Unable to move/copy. File would replace folder it is contained in.")); } } @@ -662,14 +692,21 @@ export class FileService2 extends Disposable implements IFileService { //#region Helpers - private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, buffer: VSBuffer): Promise { + private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readable: VSBufferReadable): Promise { // open handle const handle = await provider.open(resource, { create: true }); // write into handle until all bytes from buffer have been written try { - await this.doWriteBuffer(provider, handle, buffer, buffer.byteLength, 0, 0); + let posInFile = 0; + + let chunk: VSBuffer | null; + while (chunk = readable.read()) { + await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0); + + posInFile += chunk.byteLength; + } } catch (error) { throw error; } finally { @@ -685,8 +722,15 @@ export class FileService2 extends Disposable implements IFileService { } } - private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, buffer: VSBuffer, overwrite: boolean): Promise { - return provider.writeFile(resource, buffer.buffer, { create: true, overwrite }); + private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable): Promise { + let buffer: VSBuffer; + if (bufferOrReadable instanceof VSBuffer) { + buffer = bufferOrReadable; + } else { + buffer = readableToBuffer(bufferOrReadable); + } + + return provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true }); } private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { @@ -731,8 +775,8 @@ export class FileService2 extends Disposable implements IFileService { } } - private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI, overwrite: boolean): Promise { - return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite }); + private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise { + return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true }); } private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { @@ -751,7 +795,7 @@ export class FileService2 extends Disposable implements IFileService { } } - private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI, overwrite: boolean): Promise { + private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise { // Open handle const sourceHandle = await sourceProvider.open(source, { create: false }); @@ -784,7 +828,7 @@ export class FileService2 extends Disposable implements IFileService { } while (bytesRead > 0); // Write buffer into target at once - await this.doWriteUnbuffered(targetProvider, target, VSBuffer.concat([...buffers, buffer.slice(0, posInBuffer)], totalBytesRead), overwrite); + await this.doWriteUnbuffered(targetProvider, target, VSBuffer.concat([...buffers, buffer.slice(0, posInBuffer)], totalBytesRead)); } catch (error) { throw error; } finally { @@ -792,7 +836,7 @@ export class FileService2 extends Disposable implements IFileService { } } - private throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { + protected throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED); } diff --git a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts index 0784880efa..eab107bdaa 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -369,36 +369,39 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro // Dispose old dispose(this.recursiveWatcher); - let watcherImpl: { - new( - folders: { path: string, excludes: string[] }[], - onChange: (changes: IDiskFileChange[]) => void, - onError: (msg: string) => void, - verboseLogging: boolean - ): WindowsWatcherService | UnixWatcherService | NsfwWatcherService - }; + // Create new if we actually have folders to watch + if (this.recursiveFoldersToWatch.length > 0) { + let watcherImpl: { + new( + folders: { path: string, excludes: string[] }[], + onChange: (changes: IDiskFileChange[]) => void, + onError: (msg: string) => void, + verboseLogging: boolean + ): WindowsWatcherService | UnixWatcherService | NsfwWatcherService + }; - // Single Folder Watcher - if (this.recursiveFoldersToWatch.length === 1) { - if (isWindows) { - watcherImpl = WindowsWatcherService; - } else { - watcherImpl = UnixWatcherService; + // Single Folder Watcher + if (this.recursiveFoldersToWatch.length === 1) { + if (isWindows) { + watcherImpl = WindowsWatcherService; + } else { + watcherImpl = UnixWatcherService; + } } - } - // Multi Folder Watcher - else { - watcherImpl = NsfwWatcherService; - } + // Multi Folder Watcher + else { + watcherImpl = NsfwWatcherService; + } - // Create and start watching - this.recursiveWatcher = new watcherImpl( - this.recursiveFoldersToWatch, - event => this._onDidChangeFile.fire(toFileChanges(event)), - error => this._onDidWatchErrorOccur.fire(new Error(error)), - this.logService.getLevel() === LogLevel.Trace - ); + // Create and start watching + this.recursiveWatcher = new watcherImpl( + this.recursiveFoldersToWatch, + event => this._onDidChangeFile.fire(toFileChanges(event)), + error => this._onDidWatchErrorOccur.fire(new Error(error)), + this.logService.getLevel() === LogLevel.Trace + ); + } } } diff --git a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts index 18e3fb2c8b..04945d097a 100644 --- a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts @@ -15,11 +15,12 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; import { copy, rimraf, symlink, RimRafMode, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from 'fs'; -import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; +import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; function getByName(root: IFileStat, name: string): IFileStat | null { if (root.children === undefined) { @@ -35,6 +36,28 @@ function getByName(root: IFileStat, name: string): IFileStat | null { return null; } +function toLineByLineReadable(content: string): VSBufferReadable { + let chunks = content.split('\n'); + chunks = chunks.map((chunk, index) => { + if (index === 0) { + return chunk; + } + + return '\n' + chunk; + }); + + return { + read(): VSBuffer | null { + const chunk = chunks.shift(); + if (typeof chunk === 'string') { + return VSBuffer.fromString(chunk); + } + + return null; + } + }; +} + export class TestDiskFileSystemProvider extends DiskFileSystemProvider { private _testCapabilities: FileSystemProviderCapabilities; @@ -777,6 +800,165 @@ suite('Disk File Service', () => { } }); + test('createFile2', async () => { + let event: FileOperationEvent; + disposables.push(service.onAfterOperation(e => event = e)); + + const contents = 'Hello World'; + const resource = URI.file(join(testDir, 'test.txt')); + const fileStat = await service.createFile2(resource, VSBuffer.fromString(contents)); + assert.equal(fileStat.name, 'test.txt'); + assert.equal(existsSync(fileStat.resource.fsPath), true); + assert.equal(readFileSync(fileStat.resource.fsPath), contents); + + assert.ok(event!); + assert.equal(event!.resource.fsPath, resource.fsPath); + assert.equal(event!.operation, FileOperation.CREATE); + assert.equal(event!.target!.resource.fsPath, resource.fsPath); + }); + + test('createFile2 (does not overwrite by default)', async () => { + const contents = 'Hello World'; + const resource = URI.file(join(testDir, 'test.txt')); + + writeFileSync(resource.fsPath, ''); // create file + + try { + await service.createFile2(resource, VSBuffer.fromString(contents)); + } + catch (error) { + assert.ok(error); + } + }); + + test('createFile2 (allows to overwrite existing)', async () => { + let event: FileOperationEvent; + disposables.push(service.onAfterOperation(e => event = e)); + + const contents = 'Hello World'; + const resource = URI.file(join(testDir, 'test.txt')); + + writeFileSync(resource.fsPath, ''); // create file + + const fileStat = await service.createFile2(resource, VSBuffer.fromString(contents), { overwrite: true }); + assert.equal(fileStat.name, 'test.txt'); + assert.equal(existsSync(fileStat.resource.fsPath), true); + assert.equal(readFileSync(fileStat.resource.fsPath), contents); + + assert.ok(event!); + assert.equal(event!.resource.fsPath, resource.fsPath); + assert.equal(event!.operation, FileOperation.CREATE); + assert.equal(event!.target!.resource.fsPath, resource.fsPath); + }); + + test('writeFile', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + const content = readFileSync(resource.fsPath); + assert.equal(content, 'Small File'); + + const newContent = 'Updates to the small file'; + await service.writeFile(resource, VSBuffer.fromString(newContent)); + + assert.equal(readFileSync(resource.fsPath), newContent); + }); + + test('writeFile (large file)', async () => { + const resource = URI.file(join(testDir, 'lorem.txt')); + + const content = readFileSync(resource.fsPath); + const newContent = content.toString() + content.toString(); + + const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent)); + assert.equal(fileStat.name, 'lorem.txt'); + + assert.equal(readFileSync(resource.fsPath), newContent); + }); + + test('writeFile (readable)', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + const content = readFileSync(resource.fsPath); + assert.equal(content, 'Small File'); + + const newContent = 'Updates to the small file'; + await service.writeFile(resource, toLineByLineReadable(newContent)); + + assert.equal(readFileSync(resource.fsPath), newContent); + }); + + test('writeFile (large file - readable)', async () => { + const resource = URI.file(join(testDir, 'lorem.txt')); + + const content = readFileSync(resource.fsPath); + const newContent = content.toString() + content.toString(); + + const fileStat = await service.writeFile(resource, toLineByLineReadable(newContent)); + assert.equal(fileStat.name, 'lorem.txt'); + + assert.equal(readFileSync(resource.fsPath), newContent); + }); + + test('writeFile (file is created including parents)', async () => { + const resource = URI.file(join(testDir, 'other', 'newfile.txt')); + + const content = 'File is created including parent'; + const fileStat = await service.writeFile(resource, VSBuffer.fromString(content)); + assert.equal(fileStat.name, 'newfile.txt'); + + assert.equal(readFileSync(resource.fsPath), content); + }); + + test('writeFile (error when folder is encountered)', async () => { + const resource = URI.file(testDir); + + let error: Error | undefined = undefined; + try { + await service.writeFile(resource, VSBuffer.fromString('File is created including parent')); + } catch (err) { + error = err; + } + + assert.ok(error); + }); + + test('writeFile (no error when providing up to date etag)', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + const stat = await service.resolve(resource); + + const content = readFileSync(resource.fsPath); + assert.equal(content, 'Small File'); + + const newContent = 'Updates to the small file'; + await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); + + assert.equal(readFileSync(resource.fsPath), newContent); + }); + + test('writeFile (error when writing to file that has been updated meanwhile)', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + const stat = await service.resolve(resource); + + const content = readFileSync(resource.fsPath); + assert.equal(content, 'Small File'); + + const newContent = 'Updates to the small file'; + await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); + + let error: FileOperationError | undefined = undefined; + try { + await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: etag(0, 0), mtime: 0 }); + } catch (err) { + error = err; + } + + assert.ok(error); + assert.ok(error instanceof FileOperationError); + assert.equal(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); + }); + test('watch - file', done => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); diff --git a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts index 62d5dc2437..fe8af9dc58 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts @@ -211,7 +211,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding return this.fileService.exists(this.resource) .then(exists => { const EOL = this.configurationService.getValue('files', { overrideIdentifier: 'json' })['eol']; - const result: Promise = exists ? Promise.resolve(null) : this.fileService.updateContent(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' }); + const result: Promise = exists ? Promise.resolve(null) : this.textFileService.write(this.resource, this.getEmptyContent(EOL)); return result.then(() => this.textModelResolverService.createModelReference(this.resource)); }); } diff --git a/src/vs/workbench/services/output/node/outputChannelModelService.ts b/src/vs/workbench/services/output/node/outputChannelModelService.ts index bc4cb3c7de..d63aa0e007 100644 --- a/src/vs/workbench/services/output/node/outputChannelModelService.ts +++ b/src/vs/workbench/services/output/node/outputChannelModelService.ts @@ -16,8 +16,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { IOutputChannelModel, AbstractFileOutputChannelModel, IOutputChannelModelService, AsbtractOutputChannelModelService, BufferredOutputChannel } from 'vs/workbench/services/output/common/outputChannelModel'; import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { toLocalISOString } from 'vs/base/common/date'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -206,8 +205,7 @@ export class OutputChannelModelService extends AsbtractOutputChannelModelService constructor( @IInstantiationService instantiationService: IInstantiationService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IWindowService private readonly windowService: IWindowService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService ) { super(instantiationService); @@ -221,7 +219,7 @@ export class OutputChannelModelService extends AsbtractOutputChannelModelService private _outputDir: Promise | null; private get outputDir(): Promise { if (!this._outputDir) { - const outputDir = URI.file(join(this.environmentService.logsPath, `output_${this.windowService.getCurrentWindowId()}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`)); + const outputDir = URI.file(join(this.environmentService.logsPath, `output_${this.environmentService.configuration.windowId}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`)); this._outputDir = this.fileService.createFolder(outputDir).then(() => outputDir); } return this._outputDir; diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index f576a64593..5cf957b514 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -37,6 +37,7 @@ import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEdito import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel, DefaultRawSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; const emptyEditableSettingsContent = '{\n}'; @@ -59,6 +60,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic @IEditorService private readonly editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IFileService private readonly fileService: IFileService, + @ITextFileService private readonly textFileService: ITextFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @INotificationService private readonly notificationService: INotificationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -215,8 +217,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic async openRemoteSettings(): Promise { const environemnt = await this.remoteAgentService.getEnvironment(); if (environemnt) { - await this.createIfNotExists(environemnt.appSettingsPath, emptyEditableSettingsContent); - return this.editorService.openEditor({ resource: environemnt.appSettingsPath, options: { pinned: true, revealIfOpened: true } }); + await this.createIfNotExists(environemnt.settingsPath, emptyEditableSettingsContent); + return this.editorService.openEditor({ resource: environemnt.settingsPath, options: { pinned: true, revealIfOpened: true } }); } return null; } @@ -555,7 +557,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic private createIfNotExists(resource: URI, contents: string): Promise { return this.fileService.resolveContent(resource, { acceptTextOnly: true }).then(undefined, error => { if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { - return this.fileService.updateContent(resource, contents).then(undefined, error => { + return this.textFileService.write(resource, contents).then(undefined, error => { return Promise.reject(new Error(nls.localize('fail.createSettings', "Unable to create '{0}' ({1}).", this.labelService.getUriLabel(resource, { relative: true }), error))); }); } diff --git a/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts b/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts new file mode 100644 index 0000000000..c2e838965b --- /dev/null +++ b/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IRemoteAgentConnection } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; +import { IProductService } from 'vs/platform/product/common/product'; + +export class RemoteAgentService extends AbstractRemoteAgentService { + + constructor( + @IEnvironmentService environmentService: IEnvironmentService, + @IProductService productService: IProductService, + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService + ) { + super(environmentService); + } + + getConnection(): IRemoteAgentConnection | null { + return null; + } +} diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 23ea5e1d70..19b6202970 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -17,6 +17,8 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } f import { Registry } from 'vs/platform/registry/common/platform'; import { RemoteExtensionEnvironmentChannelClient } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { Emitter } from 'vs/base/common/event'; export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService { @@ -44,10 +46,23 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I } return bail ? this._environment : this._environment.then(undefined, () => null); } + + getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { + const connection = this.getConnection(); + if (connection) { + const client = new RemoteExtensionEnvironmentChannelClient(connection.getChannel('remoteextensionsenvironment')); + return client.getDiagnosticInfo(options); + } + + return Promise.resolve(undefined); + } } export class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection { + private readonly _onReconnecting = this._register(new Emitter()); + public readonly onReconnecting = this._onReconnecting.event; + readonly remoteAuthority: string; private _connection: Promise> | null; @@ -79,12 +94,18 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon } private async _createConnection(): Promise> { + let firstCall = true; const options: IConnectionOptions = { isBuilt: this._environmentService.isBuilt, commit: this._commit, webSocketFactory: this._webSocketFactory, addressProvider: { getAddress: async () => { + if (firstCall) { + firstCall = false; + } else { + this._onReconnecting.fire(undefined); + } const { host, port } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority); return { host, port }; } diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index deb624a38e..ae0fe9086c 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -8,6 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; export interface IGetEnvironmentDataArguments { language: string; @@ -19,7 +20,7 @@ export interface IRemoteAgentEnvironmentDTO { pid: number; appRoot: UriComponents; appSettingsHome: UriComponents; - appSettingsPath: UriComponents; + settingsPath: UriComponents; logsPath: UriComponents; extensionsPath: UriComponents; extensionHostLogsPath: UriComponents; @@ -34,7 +35,7 @@ export class RemoteExtensionEnvironmentChannelClient { constructor(private channel: IChannel) { } - getEnvironmentData(remoteAuthority: string, extensionDevelopmentPath?: URI | URI[]): Promise { + getEnvironmentData(remoteAuthority: string, extensionDevelopmentPath?: URI[]): Promise { const args: IGetEnvironmentDataArguments = { language: platform.language, remoteAuthority, @@ -46,7 +47,7 @@ export class RemoteExtensionEnvironmentChannelClient { pid: data.pid, appRoot: URI.revive(data.appRoot), appSettingsHome: URI.revive(data.appSettingsHome), - appSettingsPath: URI.revive(data.appSettingsPath), + settingsPath: URI.revive(data.settingsPath), logsPath: URI.revive(data.logsPath), extensionsPath: URI.revive(data.extensionsPath), extensionHostLogsPath: URI.revive(data.extensionHostLogsPath), @@ -58,4 +59,8 @@ export class RemoteExtensionEnvironmentChannelClient { }; }); } + + getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { + return this.channel.call('getDiagnosticInfo', options); + } } diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index f4b56e6e85..49681d4417 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -6,6 +6,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { Event } from 'vs/base/common/event'; export const RemoteExtensionLogFileName = 'remoteagent'; @@ -16,6 +18,7 @@ export interface IRemoteAgentService { getConnection(): IRemoteAgentConnection | null; getEnvironment(bail?: boolean): Promise; + getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise; } export interface IRemoteAgentConnection { @@ -23,4 +26,5 @@ export interface IRemoteAgentConnection { getChannel(channelName: string): T; registerChannel>(channelName: string, channel: T): void; + onReconnecting: Event; } diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/workbench/services/remote/node/tunnelService.ts new file mode 100644 index 0000000000..498b12773b --- /dev/null +++ b/src/vs/workbench/services/remote/node/tunnelService.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; + +export class TunnelService implements ITunnelService { + _serviceBrand: any; + + public constructor( + ) { + } + + openTunnel(remotePort: number): Promise | undefined { + return undefined; + } +} diff --git a/src/vs/platform/telemetry/electron-browser/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts similarity index 85% rename from src/vs/platform/telemetry/electron-browser/telemetryService.ts rename to src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts index 92e577aca4..1961049b6a 100644 --- a/src/vs/platform/telemetry/electron-browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts @@ -7,15 +7,15 @@ import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/t import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProductService } from 'vs/platform/product/common/product'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IWindowService } from 'vs/platform/windows/common/windows'; import { resolveWorkbenchCommonProperties } from 'vs/platform/telemetry/node/workbenchCommonProperties'; import { TelemetryService as BaseTelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class TelemetryService extends Disposable implements ITelemetryService { @@ -24,13 +24,12 @@ export class TelemetryService extends Disposable implements ITelemetryService { private impl: ITelemetryService; constructor( - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IProductService productService: IProductService, @ISharedProcessService sharedProcessService: ISharedProcessService, @ILogService logService: ILogService, @IStorageService storageService: IStorageService, @IConfigurationService configurationService: IConfigurationService, - @IWindowService windowService: IWindowService ) { super(); @@ -38,7 +37,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { const channel = sharedProcessService.getChannel('telemetryAppender'); const config: ITelemetryServiceConfig = { appender: combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService)), - commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, windowService.getConfiguration().machineId, environmentService.installSourcePath), + commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId, environmentService.installSourcePath), piiPaths: [environmentService.appRoot, environmentService.extensionsPath] }; @@ -60,3 +59,5 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.getTelemetryInfo(); } } + +registerSingleton(ITelemetryService, TelemetryService); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 50bccaa526..de75d0100a 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -203,7 +203,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.versionId; } - revert(soft?: boolean): Promise { + async revert(soft?: boolean): Promise { if (!this.isResolved()) { return Promise.resolve(undefined); } @@ -221,17 +221,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil loadPromise = this.load({ forceReadFromDisk: true }); } - return loadPromise.then(() => { + try { + await loadPromise; // Emit file change event this._onDidStateChange.fire(StateChange.REVERTED); - }, error => { + } catch (error) { // Set flags back to previous values, we are still dirty if revert failed undo(); return Promise.reject(error); - }); + } } load(options?: ILoadOptions): Promise { @@ -255,36 +256,35 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.loadFromFile(options); } - private loadFromBackup(options?: ILoadOptions): Promise { - return this.backupFileService.loadBackupResource(this.resource).then(backup => { + private async loadFromBackup(options?: ILoadOptions): Promise { + const backup = await this.backupFileService.loadBackupResource(this.resource); - // Make sure meanwhile someone else did not suceed or start loading - if (this.createTextEditorModelPromise || this.textEditorModel) { - return this.createTextEditorModelPromise || this; - } + // Make sure meanwhile someone else did not suceed or start loading + if (this.createTextEditorModelPromise || this.textEditorModel) { + return this.createTextEditorModelPromise || this; + } - // If we have a backup, continue loading with it - if (!!backup) { - const content: IRawTextContent = { - resource: this.resource, - name: basename(this.resource), - mtime: Date.now(), - size: 0, - etag: etag(Date.now(), 0), - value: createTextBufferFactory(''), /* will be filled later from backup */ - encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding).encoding, - isReadonly: false - }; + // If we have a backup, continue loading with it + if (!!backup) { + const content: IRawTextContent = { + resource: this.resource, + name: basename(this.resource), + mtime: Date.now(), + size: 0, + etag: etag(Date.now(), 0), + value: createTextBufferFactory(''), /* will be filled later from backup */ + encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding).encoding, + isReadonly: false + }; - return this.loadWithContent(content, options, backup); - } + return this.loadWithContent(content, options, backup); + } - // Otherwise load from file - return this.loadFromFile(options); - }); + // Otherwise load from file + return this.loadFromFile(options); } - private loadFromFile(options?: ILoadOptions): Promise { + private async loadFromFile(options?: ILoadOptions): Promise { const forceReadFromDisk = options && options.forceReadFromDisk; const allowBinary = this.isResolved() /* always allow if we resolved previously */ || (options && options.allowBinary); @@ -305,46 +305,45 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil const currentVersionId = this.versionId; // Resolve Content - return this.textFileService - .resolveTextContent(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding }) - .then(content => { + try { + const content = await this.textFileService.resolve(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding }); - // Clear orphaned state when loading was successful - this.setOrphaned(false); + // Clear orphaned state when loading was successful + this.setOrphaned(false); + + // Guard against the model having changed in the meantime + if (currentVersionId === this.versionId) { + return this.loadWithContent(content, options); + } + + return this; + } catch (error) { + const result = error.fileOperationResult; + + // Apply orphaned state based on error code + this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); + + // NotModified status is expected and can be handled gracefully + if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { // Guard against the model having changed in the meantime if (currentVersionId === this.versionId) { - return this.loadWithContent(content, options); + this.setDirty(false); // Ensure we are not tracking a stale state } return this; - }, error => { - const result = error.fileOperationResult; + } - // Apply orphaned state based on error code - this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); + // Ignore when a model has been resolved once and the file was deleted meanwhile. Since + // we already have the model loaded, we can return to this state and update the orphaned + // flag to indicate that this model has no version on disk anymore. + if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) { + return this; + } - // NotModified status is expected and can be handled gracefully - if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { - - // Guard against the model having changed in the meantime - if (currentVersionId === this.versionId) { - this.setDirty(false); // Ensure we are not tracking a stale state - } - - return this; - } - - // Ignore when a model has been resolved once and the file was deleted meanwhile. Since - // we already have the model loaded, we can return to this state and update the orphaned - // flag to indicate that this model has no version on disk anymore. - if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) { - return this; - } - - // Otherwise bubble up the error - return Promise.reject(error); - }); + // Otherwise bubble up the error + throw error; + } } private loadWithContent(content: IRawTextContent, options?: ILoadOptions, backup?: URI): Promise { @@ -486,12 +485,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - private doLoadBackup(backup: URI | undefined): Promise { + private async doLoadBackup(backup: URI | undefined): Promise { if (!backup) { - return Promise.resolve(null); + return null; } - return this.backupFileService.resolveBackupContent(backup).then(withUndefinedAsNull, error => null /* ignore errors */); + try { + return withUndefinedAsNull(await this.backupFileService.resolveBackupContent(backup)); + } catch (error) { + return null; // ignore errors + } } protected getOrCreateMode(modeService: IModeService, preferredModeIds: string | undefined, firstLineText?: string): ILanguageSelection { @@ -700,12 +703,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk // mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering) - this.logService.trace(`doSave(${versionId}) - before updateContent()`, this.resource); + this.logService.trace(`doSave(${versionId}) - before write()`, this.resource); const snapshot = this.createSnapshot(); if (!snapshot) { throw new Error('Invalid snapshot'); } - return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, snapshot, { + return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(this.lastResolvedDiskStat.resource, snapshot, { overwriteReadonly: options.overwriteReadonly, overwriteEncoding: options.overwriteEncoding, mtime: this.lastResolvedDiskStat.mtime, @@ -713,7 +716,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil etag: this.lastResolvedDiskStat.etag, writeElevated: options.writeElevated }).then(stat => { - this.logService.trace(`doSave(${versionId}) - after updateContent()`, this.resource); + this.logService.trace(`doSave(${versionId}) - after write()`, this.resource); // Update dirty state unless model has changed meanwhile if (versionId === this.versionId) { @@ -847,7 +850,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (!snapshot) { throw new Error('invalid snapshot'); } - return this.saveSequentializer.setPending(versionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, snapshot, { + + return this.saveSequentializer.setPending(versionId, this.textFileService.write(this.lastResolvedDiskStat.resource, snapshot, { mtime: this.lastResolvedDiskStat.mtime, encoding: this.getEncoding(), etag: this.lastResolvedDiskStat.etag diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index f643ee058e..71b6729db2 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -121,7 +121,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE return this.mapResourceToModel.get(resource); } - loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): Promise { + async loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): Promise { // Return early if model is currently being loaded const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource); @@ -190,7 +190,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Store pending loads to avoid race conditions this.mapResourceToPendingModelLoaders.set(resource, modelPromise); - return modelPromise.then(model => { + try { + const model = await modelPromise; // Make known to manager (if not already known) this.add(resource, model); @@ -204,7 +205,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE this.mapResourceToPendingModelLoaders.delete(resource); return model; - }, error => { + } catch (error) { // Free resources of this invalid model if (model) { @@ -214,8 +215,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Remove from pending loads this.mapResourceToPendingModelLoaders.delete(resource); - return Promise.reject(error); - }); + throw error; + } } getAll(resource?: URI, filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] { @@ -315,4 +316,10 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE model.dispose(); } + + dispose(): void { + super.dispose(); + + this.clear(); + } } \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 31b0492c1a..1680813822 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -9,16 +9,16 @@ import * as errors from 'vs/base/common/errors'; import * as objects from 'vs/base/common/objects'; import { Event, Emitter } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; -import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext, IWillMoveEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor'; import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; +import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -30,23 +30,16 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IModelService } from 'vs/editor/common/services/modelService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename } from 'vs/base/common/resources'; -import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; +import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources'; +import { posix } from 'vs/base/common/path'; import { getConfirmMessage, IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { coalesce } from 'vs/base/common/arrays'; import { trim } from 'vs/base/common/strings'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -export interface IBackupResult { - didBackup: boolean; -} /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. - * - * It also adds diagnostics and logging around file system operations. */ export class TextFileService extends Disposable implements ITextFileService { @@ -62,6 +55,8 @@ export class TextFileService extends Disposable implements ITextFileService { get onWillMove(): Event { return this._onWillMove.event; } private _models: TextFileEditorModelManager; + get models(): ITextFileEditorModelManager { return this._models; } + private currentFilesAssociationConfig: { [key: string]: string; }; private configuredAutoSaveDelay?: number; private configuredAutoSaveOnFocusChange: boolean; @@ -78,8 +73,7 @@ export class TextFileService extends Disposable implements ITextFileService { @IConfigurationService private readonly configurationService: IConfigurationService, @IModeService private readonly modeService: IModeService, @IModelService private readonly modelService: IModelService, - @IWindowService private readonly windowService: IWindowService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @INotificationService private readonly notificationService: INotificationService, @IBackupFileService private readonly backupFileService: IBackupFileService, @IWindowsService private readonly windowsService: IWindowsService, @@ -91,7 +85,7 @@ export class TextFileService extends Disposable implements ITextFileService { ) { super(); - this._models = instantiationService.createInstance(TextFileEditorModelManager); + this._models = this._register(instantiationService.createInstance(TextFileEditorModelManager)); this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService); const configuration = configurationService.getValue(); @@ -102,128 +96,7 @@ export class TextFileService extends Disposable implements ITextFileService { this.registerListeners(); } - get models(): ITextFileEditorModelManager { - return this._models; - } - - resolveTextContent(resource: URI, options?: IResolveContentOptions): Promise { - return this.fileService.resolveStreamContent(resource, options).then(streamContent => { - return createTextBufferFactoryFromStream(streamContent.value).then(res => { - return { - resource: streamContent.resource, - name: streamContent.name, - mtime: streamContent.mtime, - etag: streamContent.etag, - encoding: streamContent.encoding, - isReadonly: streamContent.isReadonly, - size: streamContent.size, - value: res - }; - }); - }); - } - - promptForPath(resource: URI, defaultUri: URI): Promise { - - // Help user to find a name for the file by opening it first - return this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true, } }).then(() => { - return this.fileDialogService.showSaveDialog(this.getSaveDialogOptions(defaultUri)); - }); - } - - private getSaveDialogOptions(defaultUri: URI): ISaveDialogOptions { - const options: ISaveDialogOptions = { - defaultUri, - title: nls.localize('saveAsTitle', "Save As") - }; - - // Filters are only enabled on Windows where they work properly - if (!platform.isWindows) { - return options; - } - - interface IFilter { name: string; extensions: string[]; } - - // Build the file filter by using our known languages - const ext: string | undefined = defaultUri ? extname(defaultUri) : undefined; - let matchingFilter: IFilter | undefined; - const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => { - const extensions = this.modeService.getExtensions(languageName); - if (!extensions || !extensions.length) { - return null; - } - - const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => trim(e, '.')) }; - - if (ext && extensions.indexOf(ext) >= 0) { - matchingFilter = filter; - - return null; // matching filter will be added last to the top - } - - return filter; - })); - - // Filters are a bit weird on Windows, based on having a match or not: - // Match: we put the matching filter first so that it shows up selected and the all files last - // No match: we put the all files filter first - const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] }; - if (matchingFilter) { - filters.unshift(matchingFilter); - filters.unshift(allFilesFilter); - } else { - filters.unshift(allFilesFilter); - } - - // Allow to save file without extension - filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] }); - - options.filters = filters; - - return options; - } - - confirmSave(resources?: URI[]): Promise { - if (this.environmentService.isExtensionDevelopment) { - return Promise.resolve(ConfirmResult.DONT_SAVE); // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests) - } - - const resourcesToConfirm = this.getDirty(resources); - if (resourcesToConfirm.length === 0) { - return Promise.resolve(ConfirmResult.DONT_SAVE); - } - - const message = resourcesToConfirm.length === 1 ? nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", basename(resourcesToConfirm[0])) - : getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", resourcesToConfirm.length), resourcesToConfirm); - - const buttons: string[] = [ - resourcesToConfirm.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"), - nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"), - nls.localize('cancel', "Cancel") - ]; - - return this.dialogService.show(Severity.Warning, message, buttons, { - cancelId: 2, - detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.") - }).then(index => { - switch (index) { - case 0: return ConfirmResult.SAVE; - case 1: return ConfirmResult.DONT_SAVE; - default: return ConfirmResult.CANCEL; - } - }); - } - - confirmOverwrite(resource: URI): Promise { - const confirm: IConfirmation = { - message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), - detail: nls.localize('irreversible', "A file or folder with the same name already exists in the folder {0}. Replacing it will overwrite its current contents.", basename(dirname(resource))), - primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - - return this.dialogService.confirm(confirm).then(result => result.confirmed); - } + //#region event handling private registerListeners(): void { @@ -272,8 +145,8 @@ export class TextFileService extends Disposable implements ITextFileService { // If hot exit is enabled, backup dirty files and allow to exit without confirmation if (this.isHotExitEnabled) { - return this.backupBeforeShutdown(dirty, this.models, reason).then(result => { - if (result.didBackup) { + return this.backupBeforeShutdown(dirty, this.models, reason).then(didBackup => { + if (didBackup) { return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful) } @@ -291,50 +164,50 @@ export class TextFileService extends Disposable implements ITextFileService { return this.confirmBeforeShutdown(); } - private backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): Promise { - return this.windowsService.getWindowCount().then(windowCount => { + private async backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): Promise { + const windowCount = await this.windowsService.getWindowCount(); - // When quit is requested skip the confirm callback and attempt to backup all workspaces. - // When quit is not requested the confirm callback should be shown when the window being - // closed is the only VS Code window open, except for on Mac where hot exit is only - // ever activated when quit is requested. + // When quit is requested skip the confirm callback and attempt to backup all workspaces. + // When quit is not requested the confirm callback should be shown when the window being + // closed is the only VS Code window open, except for on Mac where hot exit is only + // ever activated when quit is requested. - let doBackup: boolean | undefined; - switch (reason) { - case ShutdownReason.CLOSE: - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { - doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured - } else if (windowCount > 1 || platform.isMacintosh) { - doBackup = false; // do not backup if a window is closed that does not cause quitting of the application - } else { - doBackup = true; // backup if last window is closed on win/linux where the application quits right after - } - break; + let doBackup: boolean | undefined; + switch (reason) { + case ShutdownReason.CLOSE: + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured + } else if (windowCount > 1 || platform.isMacintosh) { + doBackup = false; // do not backup if a window is closed that does not cause quitting of the application + } else { + doBackup = true; // backup if last window is closed on win/linux where the application quits right after + } + break; - case ShutdownReason.QUIT: - doBackup = true; // backup because next start we restore all backups - break; + case ShutdownReason.QUIT: + doBackup = true; // backup because next start we restore all backups + break; - case ShutdownReason.RELOAD: - doBackup = true; // backup because after window reload, backups restore - break; + case ShutdownReason.RELOAD: + doBackup = true; // backup because after window reload, backups restore + break; - case ShutdownReason.LOAD: - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { - doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured - } else { - doBackup = false; // do not backup because we are switching contexts - } - break; - } + case ShutdownReason.LOAD: + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured + } else { + doBackup = false; // do not backup because we are switching contexts + } + break; + } - if (!doBackup) { - return { didBackup: false }; - } + if (!doBackup) { + return false; + } - // Backup - return this.backupAll(dirtyToBackup, textFileEditorModelManager).then(() => { return { didBackup: true }; }); - }); + await this.backupAll(dirtyToBackup, textFileEditorModelManager); + + return true; } private backupAll(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager): Promise { @@ -356,35 +229,29 @@ export class TextFileService extends Disposable implements ITextFileService { return this.doBackupAll(filesToBackup, untitledToBackup); } - private doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise { - const promises = dirtyFileModels.map(model => { - const snapshot = model.createSnapshot(); - if (snapshot) { - return this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId()); - } - return Promise.resolve(); - }); + private async doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise { // Handle file resources first - return Promise.all(promises).then(results => { + await Promise.all(dirtyFileModels.map(async model => { + const snapshot = model.createSnapshot(); + if (snapshot) { + await this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId()); + } + })); - // Handle untitled resources - const untitledModelPromises = untitledResources - .filter(untitled => this.untitledEditorService.exists(untitled)) - .map(untitled => this.untitledEditorService.loadOrCreate({ resource: untitled })); + // Handle untitled resources + const untitledModelPromises = untitledResources + .filter(untitled => this.untitledEditorService.exists(untitled)) + .map(untitled => this.untitledEditorService.loadOrCreate({ resource: untitled })); - return Promise.all(untitledModelPromises).then(untitledModels => { - const untitledBackupPromises = untitledModels.map(model => { - const snapshot = model.createSnapshot(); - if (snapshot) { - return this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId()); - } - return Promise.resolve(); - }); + const untitledModels = await Promise.all(untitledModelPromises); - return Promise.all(untitledBackupPromises).then(() => undefined); - }); - }); + await Promise.all(untitledModels.map(async model => { + const snapshot = model.createSnapshot(); + if (snapshot) { + await this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId()); + } + })); } private confirmBeforeShutdown(): boolean | Promise { @@ -432,12 +299,12 @@ export class TextFileService extends Disposable implements ITextFileService { return this.cleanupBackupsBeforeShutdown().then(() => false, () => false); } - protected cleanupBackupsBeforeShutdown(): Promise { + protected async cleanupBackupsBeforeShutdown(): Promise { if (this.environmentService.isExtensionDevelopment) { - return Promise.resolve(undefined); + return; } - return this.backupFileService.discardAllWorkspaceBackups(); + await this.backupFileService.discardAllWorkspaceBackups(); } protected onFilesConfigurationChange(configuration: IFilesConfiguration): void { @@ -495,29 +362,133 @@ export class TextFileService extends Disposable implements ITextFileService { } } - getDirty(resources?: URI[]): URI[] { + //#endregion - // Collect files - const dirty = this.getDirtyFileModels(resources).map(m => m.getResource()); + //#region primitives (resolve, create, move, delete, update) - // Add untitled ones - dirty.push(...this.untitledEditorService.getDirty(resources)); + async resolve(resource: URI, options?: IResolveContentOptions): Promise { + const streamContent = await this.fileService.resolveStreamContent(resource, options); + const value = await createTextBufferFactoryFromStream(streamContent.value); - return dirty; + return { + resource: streamContent.resource, + name: streamContent.name, + mtime: streamContent.mtime, + etag: streamContent.etag, + encoding: streamContent.encoding, + isReadonly: streamContent.isReadonly, + size: streamContent.size, + value + }; } - isDirty(resource?: URI): boolean { + async create(resource: URI, contents?: string, options?: { overwrite?: boolean }): Promise { + const existingModel = this.models.get(resource); - // Check for dirty file - if (this._models.getAll(resource).some(model => model.isDirty())) { - return true; + const stat = await this.fileService.createFile(resource, contents, options); + + // If we had an existing model for the given resource, load + // it again to make sure it is up to date with the contents + // we just wrote into the underlying resource by calling + // revert() + if (existingModel && !existingModel.isDisposed()) { + await existingModel.revert(); } - // Check for dirty untitled - return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString()); + return stat; } - save(resource: URI, options?: ISaveOptions): Promise { + async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { + const stat = await this.fileService.updateContent(resource, value, options); + + return stat; + } + + async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise { + const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */)); + + await this.revertAll(dirtyFiles, { soft: true }); + + return this.fileService.del(resource, options); + } + + async move(source: URI, target: URI, overwrite?: boolean): Promise { + const waitForPromises: Promise[] = []; + + // Event + this._onWillMove.fire({ + oldResource: source, + newResource: target, + waitUntil(promise: Promise) { + waitForPromises.push(promise.then(undefined, errors.onUnexpectedError)); + } + }); + + // prevent async waitUntil-calls + Object.freeze(waitForPromises); + + await Promise.all(waitForPromises); + + // Handle target models if existing (if target URI is a folder, this can be multiple) + const dirtyTargetModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)); + if (dirtyTargetModels.length) { + await this.revertAll(dirtyTargetModels.map(targetModel => targetModel.getResource()), { soft: true }); + } + + // Handle dirty source models if existing (if source URI is a folder, this can be multiple) + const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */)); + const dirtyTargetModelUris: URI[] = []; + if (dirtySourceModels.length) { + await Promise.all(dirtySourceModels.map(async sourceModel => { + const sourceModelResource = sourceModel.getResource(); + let targetModelResource: URI; + + // If the source is the actual model, just use target as new resource + if (isEqual(sourceModelResource, source, !platform.isLinux /* ignorecase */)) { + targetModelResource = target; + } + + // Otherwise a parent folder of the source is being moved, so we need + // to compute the target resource based on that + else { + targetModelResource = sourceModelResource.with({ path: joinPath(target, sourceModelResource.path.substr(source.path.length + 1)).path }); + } + + // Remember as dirty target model to load after the operation + dirtyTargetModelUris.push(targetModelResource); + + // Backup dirty source model to the target resource it will become later + const snapshot = sourceModel.createSnapshot(); + if (snapshot) { + await this.backupFileService.backupResource(targetModelResource, snapshot, sourceModel.getVersionId()); + } + })); + } + + + // Soft revert the dirty source files if any + await this.revertAll(dirtySourceModels.map(dirtySourceModel => dirtySourceModel.getResource()), { soft: true }); + + // Rename to target + try { + await this.fileService.move(source, target, overwrite); + + // Load models that were dirty before + await Promise.all(dirtyTargetModelUris.map(dirtyTargetModel => this.models.loadOrCreate(dirtyTargetModel))); + } catch (error) { + + // In case of an error, discard any dirty target backups that were made + await Promise.all(dirtyTargetModelUris.map(dirtyTargetModel => this.backupFileService.discardResourceBackup(dirtyTargetModel))); + + throw error; + } + } + + //#endregion + + //#region save/revert + + async save(resource: URI, options?: ISaveOptions): Promise { // Run a forced save if we detect the file is not dirty so that save participants can still run if (options && options.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) { @@ -525,11 +496,57 @@ export class TextFileService extends Disposable implements ITextFileService { if (model) { options.reason = SaveReason.EXPLICIT; - return model.save(options).then(() => !model.isDirty()); + await model.save(options); + + return !model.isDirty(); } } - return this.saveAll([resource], options).then(result => result.results.length === 1 && !!result.results[0].success); + const result = await this.saveAll([resource], options); + + return result.results.length === 1 && !!result.results[0].success; + } + + async confirmSave(resources?: URI[]): Promise { + if (this.environmentService.isExtensionDevelopment) { + return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests) + } + + const resourcesToConfirm = this.getDirty(resources); + if (resourcesToConfirm.length === 0) { + return ConfirmResult.DONT_SAVE; + } + + const message = resourcesToConfirm.length === 1 ? nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", basename(resourcesToConfirm[0])) + : getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", resourcesToConfirm.length), resourcesToConfirm); + + const buttons: string[] = [ + resourcesToConfirm.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"), + nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"), + nls.localize('cancel', "Cancel") + ]; + + const index = await this.dialogService.show(Severity.Warning, message, buttons, { + cancelId: 2, + detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.") + }); + + switch (index) { + case 0: return ConfirmResult.SAVE; + case 1: return ConfirmResult.DONT_SAVE; + default: return ConfirmResult.CANCEL; + } + } + + async confirmOverwrite(resource: URI): Promise { + const confirm: IConfirmation = { + message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), + detail: nls.localize('irreversible', "A file or folder with the same name already exists in the folder {0}. Replacing it will overwrite its current contents.", basename(dirname(resource))), + primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + return (await this.dialogService.confirm(confirm)).confirmed; } saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise; @@ -558,69 +575,111 @@ export class TextFileService extends Disposable implements ITextFileService { return this.doSaveAll(filesToSave, untitledToSave, options); } - private doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): Promise { + private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): Promise { // Handle files first that can just be saved - return this.doSaveAllFiles(fileResources, options).then(async result => { + const result = await this.doSaveAllFiles(fileResources, options); - // Preflight for untitled to handle cancellation from the dialog - const targetsForUntitled: URI[] = []; - for (const untitled of untitledResources) { - if (this.untitledEditorService.exists(untitled)) { - let targetUri: URI; + // Preflight for untitled to handle cancellation from the dialog + const targetsForUntitled: URI[] = []; + for (const untitled of untitledResources) { + if (this.untitledEditorService.exists(untitled)) { + let targetUri: URI; - // Untitled with associated file path don't need to prompt - if (this.untitledEditorService.hasAssociatedFilePath(untitled)) { - targetUri = this.untitledToAssociatedFileResource(untitled); - } - - // Otherwise ask user - else { - const targetPath = await this.promptForPath(untitled, this.suggestFileName(untitled)); - if (!targetPath) { - return Promise.resolve({ - results: [...fileResources, ...untitledResources].map(r => ({ source: r })) - }); - } - - targetUri = targetPath; - } - - targetsForUntitled.push(targetUri); + // Untitled with associated file path don't need to prompt + if (this.untitledEditorService.hasAssociatedFilePath(untitled)) { + targetUri = toLocalResource(untitled, this.environmentService.configuration.remoteAuthority); } + + // Otherwise ask user + else { + const targetPath = await this.promptForPath(untitled, this.suggestFileName(untitled)); + if (!targetPath) { + return { results: [...fileResources, ...untitledResources].map(r => ({ source: r })) }; + } + + targetUri = targetPath; + } + + targetsForUntitled.push(targetUri); } - - // Handle untitled - const untitledSaveAsPromises: Promise[] = []; - targetsForUntitled.forEach((target, index) => { - const untitledSaveAsPromise = this.saveAs(untitledResources[index], target).then(uri => { - result.results.push({ - source: untitledResources[index], - target: uri, - success: !!uri - }); - }); - - untitledSaveAsPromises.push(untitledSaveAsPromise); - }); - - return Promise.all(untitledSaveAsPromises).then(() => result); - }); - } - - private untitledToAssociatedFileResource(untitled: URI): URI { - const authority = this.windowService.getConfiguration().remoteAuthority; - if (authority) { - let path = untitled.path; - if (path && path[0] !== '/') { - path = '/' + path; - } - return untitled.with({ scheme: REMOTE_HOST_SCHEME, authority, path }); } - return untitled.with({ scheme: Schemas.file }); + + // Handle untitled + await Promise.all(targetsForUntitled.map(async (target, index) => { + const uri = await this.saveAs(untitledResources[index], target); + + result.results.push({ + source: untitledResources[index], + target: uri, + success: !!uri + }); + })); + + return result; } - private doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): Promise { + protected async promptForPath(resource: URI, defaultUri: URI): Promise { + + // Help user to find a name for the file by opening it first + await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true, } }); + + return this.fileDialogService.showSaveDialog(this.getSaveDialogOptions(defaultUri)); + } + + private getSaveDialogOptions(defaultUri: URI): ISaveDialogOptions { + const options: ISaveDialogOptions = { + defaultUri, + title: nls.localize('saveAsTitle', "Save As") + }; + + // Filters are only enabled on Windows where they work properly + if (!platform.isWindows) { + return options; + } + + interface IFilter { name: string; extensions: string[]; } + + // Build the file filter by using our known languages + const ext: string | undefined = defaultUri ? extname(defaultUri) : undefined; + let matchingFilter: IFilter | undefined; + const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => { + const extensions = this.modeService.getExtensions(languageName); + if (!extensions || !extensions.length) { + return null; + } + + const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => trim(e, '.')) }; + + if (ext && extensions.indexOf(ext) >= 0) { + matchingFilter = filter; + + return null; // matching filter will be added last to the top + } + + return filter; + })); + + // Filters are a bit weird on Windows, based on having a match or not: + // Match: we put the matching filter first so that it shows up selected and the all files last + // No match: we put the all files filter first + const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] }; + if (matchingFilter) { + filters.unshift(matchingFilter); + filters.unshift(allFilesFilter); + } else { + filters.unshift(allFilesFilter); + } + + // Allow to save file without extension + filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] }); + + options.filters = filters; + + return options; + } + + private async doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): Promise { const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : undefined /* Save All */) .filter(model => { if ((model.hasState(ModelState.CONFLICT) || model.hasState(ModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE)) { @@ -637,16 +696,18 @@ export class TextFileService extends Disposable implements ITextFileService { }); }); - return Promise.all(dirtyFileModels.map(model => { - return model.save(options).then(() => { - if (!model.isDirty()) { - const result = mapResourceToResult.get(model.getResource()); - if (result) { - result.success = true; - } + await Promise.all(dirtyFileModels.map(async model => { + await model.save(options); + + if (!model.isDirty()) { + const result = mapResourceToResult.get(model.getResource()); + if (result) { + result.success = true; } - }); - })).then(r => ({ results: mapResourceToResult.values() })); + } + })); + + return { results: mapResourceToResult.values() }; } private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] { @@ -666,141 +727,136 @@ export class TextFileService extends Disposable implements ITextFileService { return this.getFileModels(resources).filter(model => model.isDirty()); } - saveAs(resource: URI, target?: URI, options?: ISaveOptions): Promise { + async saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): Promise { // Get to target resource - let targetPromise: Promise; - if (target) { - targetPromise = Promise.resolve(target); - } else { + if (!targetResource) { let dialogPath = resource; if (resource.scheme === Schemas.untitled) { dialogPath = this.suggestFileName(resource); } - targetPromise = this.promptForPath(resource, dialogPath); + targetResource = await this.promptForPath(resource, dialogPath); } - return targetPromise.then(target => { - if (!target) { - return undefined; // user canceled - } + if (!targetResource) { + // {{SQL CARBON EDIT}} @todo anthonydresser necessary to add undefined till we enable strict null check + return undefined; // user canceled + } - // Just save if target is same as models own resource - if (resource.toString() === target.toString()) { - return this.save(resource, options).then(() => resource); - } + // Just save if target is same as models own resource + if (resource.toString() === targetResource.toString()) { + await this.save(resource, options); - // Do it - return this.doSaveAs(resource, target, options); - }); + return resource; + } + + // Do it + return this.doSaveAs(resource, targetResource, options); } - private doSaveAs(resource: URI, target: URI, options?: ISaveOptions): Promise { + private async doSaveAs(resource: URI, target: URI, options?: ISaveOptions): Promise { // Retrieve text model from provided resource if any - let modelPromise: Promise = Promise.resolve(undefined); + let model: ITextFileEditorModel | UntitledEditorModel | undefined; if (this.fileService.canHandleResource(resource)) { - modelPromise = Promise.resolve(this._models.get(resource)); + model = this._models.get(resource); } else if (resource.scheme === Schemas.untitled && this.untitledEditorService.exists(resource)) { - modelPromise = this.untitledEditorService.loadOrCreate({ resource }); + model = await this.untitledEditorService.loadOrCreate({ resource }); } - return modelPromise.then(model => { + // We have a model: Use it (can be null e.g. if this file is binary and not a text file or was never opened before) + let result: boolean; + if (model) { + result = await this.doSaveTextFileAs(model, resource, target, options); + } - // We have a model: Use it (can be null e.g. if this file is binary and not a text file or was never opened before) - if (model) { - return this.doSaveTextFileAs(model, resource, target, options); - } + // Otherwise we can only copy + else { + await this.fileService.copy(resource, target); - // Otherwise we can only copy - return this.fileService.copy(resource, target).then(() => true); - }).then(result => { + result = true; + } - // Return early if the operation was not running - if (!result) { - return target; - } + // Return early if the operation was not running + if (!result) { + return target; + } - // Revert the source - return this.revert(resource).then(() => { + // Revert the source + await this.revert(resource); - // Done: return target - return target; - }); - }); + return target; } - private doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI, options?: ISaveOptions): Promise { - let targetModelResolver: Promise; - let targetExists: boolean = false; + private async doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI, options?: ISaveOptions): Promise { // Prefer an existing model if it is already loaded for the given target resource - const targetModel = this.models.get(target); + let targetExists: boolean = false; + let targetModel = this.models.get(target); if (targetModel && targetModel.isResolved()) { - targetModelResolver = Promise.resolve(targetModel); targetExists = true; } // Otherwise create the target file empty if it does not exist already and resolve it from there else { - targetModelResolver = this.fileService.exists(target).then(exists => { - targetExists = exists; + targetExists = await this.fileService.exists(target); - // create target model adhoc if file does not exist yet - if (!targetExists) { - return this.fileService.updateContent(target, ''); - } + // create target model adhoc if file does not exist yet + if (!targetExists) { + await this.create(target, ''); + } - return Promise.resolve(undefined); - }).then(() => this.models.loadOrCreate(target)); + targetModel = await this.models.loadOrCreate(target); } - return targetModelResolver.then(targetModel => { + try { // Confirm to overwrite if we have an untitled file with associated file where // the file actually exists on disk and we are instructed to save to that file // path. This can happen if the file was created after the untitled file was opened. // See https://github.com/Microsoft/vscode/issues/67946 - let confirmWrite: Promise; - if (sourceModel instanceof UntitledEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, this.untitledToAssociatedFileResource(sourceModel.getResource()))) { - confirmWrite = this.confirmOverwrite(target); + let write: boolean; + if (sourceModel instanceof UntitledEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.getResource(), this.environmentService.configuration.remoteAuthority))) { + write = await this.confirmOverwrite(target); } else { - confirmWrite = Promise.resolve(true); + write = true; } - return confirmWrite.then(write => { - if (!write) { - return false; - } + if (!write) { + return false; + } - // take over encoding and model value from source model - targetModel.updatePreferredEncoding(sourceModel.getEncoding()); - if (targetModel.textEditorModel) { - const snapshot = sourceModel.createSnapshot(); - if (snapshot) { - this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(snapshot)); - } + // take over encoding and model value from source model + targetModel.updatePreferredEncoding(sourceModel.getEncoding()); + if (targetModel.textEditorModel) { + const snapshot = sourceModel.createSnapshot(); + if (snapshot) { + this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(snapshot)); } + } - // save model - return targetModel.save(options).then(() => true); - }); - }, error => { + // save model + await targetModel.save(options); + + return true; + } catch (error) { // binary model: delete the file and run the operation again if ((error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { - return this.fileService.del(target).then(() => this.doSaveTextFileAs(sourceModel, resource, target, options)); + await this.fileService.del(target); + + return this.doSaveTextFileAs(sourceModel, resource, target, options); } - return Promise.reject(error); - }); + throw error; + } } private suggestFileName(untitledResource: URI): URI { const untitledFileName = this.untitledEditorService.suggestFileName(untitledResource); - const remoteAuthority = this.windowService.getConfiguration().remoteAuthority; - const schemeFilter = remoteAuthority ? REMOTE_HOST_SCHEME : Schemas.file; + const remoteAuthority = this.environmentService.configuration.remoteAuthority; + const schemeFilter = remoteAuthority ? Schemas.vscodeRemote : Schemas.file; const lastActiveFile = this.historyService.getLastActiveFile(schemeFilter); if (lastActiveFile) { @@ -813,27 +869,28 @@ export class TextFileService extends Disposable implements ITextFileService { return joinPath(lastActiveFolder, untitledFileName); } - return schemeFilter === Schemas.file ? URI.file(untitledFileName) : URI.from({ scheme: schemeFilter, authority: remoteAuthority, path: '/' + untitledFileName }); + return schemeFilter === Schemas.file ? URI.file(untitledFileName) : URI.from({ scheme: schemeFilter, authority: remoteAuthority, path: posix.sep + untitledFileName }); } - revert(resource: URI, options?: IRevertOptions): Promise { - return this.revertAll([resource], options).then(result => result.results.length === 1 && !!result.results[0].success); + async revert(resource: URI, options?: IRevertOptions): Promise { + const result = await this.revertAll([resource], options); + + return result.results.length === 1 && !!result.results[0].success; } - revertAll(resources?: URI[], options?: IRevertOptions): Promise { + async revertAll(resources?: URI[], options?: IRevertOptions): Promise { // Revert files first - return this.doRevertAllFiles(resources, options).then(operation => { + const revertOperationResult = await this.doRevertAllFiles(resources, options); - // Revert untitled - const reverted = this.untitledEditorService.revertAll(resources); - reverted.forEach(res => operation.results.push({ source: res, success: true })); + // Revert untitled + const untitledReverted = this.untitledEditorService.revertAll(resources); + untitledReverted.forEach(untitled => revertOperationResult.results.push({ source: untitled, success: true })); - return operation; - }); + return revertOperationResult; } - private doRevertAllFiles(resources?: URI[], options?: IRevertOptions): Promise { + private async doRevertAllFiles(resources?: URI[], options?: IRevertOptions): Promise { const fileModels = options && options.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources); const mapResourceToResult = new ResourceMap(); @@ -843,15 +900,17 @@ export class TextFileService extends Disposable implements ITextFileService { }); }); - return Promise.all(fileModels.map(model => { - return model.revert(options && options.soft).then(() => { + await Promise.all(fileModels.map(async model => { + try { + await model.revert(options && options.soft); + if (!model.isDirty()) { const result = mapResourceToResult.get(model.getResource()); if (result) { result.success = true; } } - }, error => { + } catch (error) { // FileNotFound means the file got deleted meanwhile, so still record as successful revert if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { @@ -863,119 +922,40 @@ export class TextFileService extends Disposable implements ITextFileService { // Otherwise bubble up the error else { - return Promise.reject(error); + throw error; } - - return undefined; - }); - })).then(r => ({ results: mapResourceToResult.values() })); - } - - create(resource: URI, contents?: string, options?: { overwrite?: boolean }): Promise { - const existingModel = this.models.get(resource); - - return this.fileService.createFile(resource, contents, options).then(() => { - - // If we had an existing model for the given resource, load - // it again to make sure it is up to date with the contents - // we just wrote into the underlying resource by calling - // revert() - if (existingModel && !existingModel.isDisposed()) { - return existingModel.revert(); } + })); - return undefined; - }); + return { results: mapResourceToResult.values() }; } - delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise { - const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */)); + getDirty(resources?: URI[]): URI[] { - return this.revertAll(dirtyFiles, { soft: true }).then(() => this.fileService.del(resource, options)); + // Collect files + const dirty = this.getDirtyFileModels(resources).map(m => m.getResource()); + + // Add untitled ones + dirty.push(...this.untitledEditorService.getDirty(resources)); + + return dirty; } - move(source: URI, target: URI, overwrite?: boolean): Promise { - const waitForPromises: Promise[] = []; + isDirty(resource?: URI): boolean { - // Event - this._onWillMove.fire({ - oldResource: source, - newResource: target, - waitUntil(promise: Promise) { - waitForPromises.push(promise.then(undefined, errors.onUnexpectedError)); - } - }); + // Check for dirty file + if (this._models.getAll(resource).some(model => model.isDirty())) { + return true; + } - // prevent async waitUntil-calls - Object.freeze(waitForPromises); - - return Promise.all(waitForPromises).then(() => { - - // Handle target models if existing (if target URI is a folder, this can be multiple) - let handleTargetModelPromise: Promise = Promise.resolve(); - const dirtyTargetModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)); - if (dirtyTargetModels.length) { - handleTargetModelPromise = this.revertAll(dirtyTargetModels.map(targetModel => targetModel.getResource()), { soft: true }); - } - - return handleTargetModelPromise.then(() => { - - // Handle dirty source models if existing (if source URI is a folder, this can be multiple) - let handleDirtySourceModels: Promise; - const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */)); - const dirtyTargetModels: URI[] = []; - if (dirtySourceModels.length) { - handleDirtySourceModels = Promise.all(dirtySourceModels.map(sourceModel => { - const sourceModelResource = sourceModel.getResource(); - let targetModelResource: URI; - - // If the source is the actual model, just use target as new resource - if (isEqual(sourceModelResource, source, !platform.isLinux /* ignorecase */)) { - targetModelResource = target; - } - - // Otherwise a parent folder of the source is being moved, so we need - // to compute the target resource based on that - else { - targetModelResource = sourceModelResource.with({ path: joinPath(target, sourceModelResource.path.substr(source.path.length + 1)).path }); - } - - // Remember as dirty target model to load after the operation - dirtyTargetModels.push(targetModelResource); - - // Backup dirty source model to the target resource it will become later - const snapshot = sourceModel.createSnapshot(); - if (snapshot) { - return this.backupFileService.backupResource(targetModelResource, snapshot, sourceModel.getVersionId()); - } - return Promise.resolve(); - })); - } else { - handleDirtySourceModels = Promise.resolve(); - } - - return handleDirtySourceModels.then(() => { - - // Soft revert the dirty source files if any - return this.revertAll(dirtySourceModels.map(dirtySourceModel => dirtySourceModel.getResource()), { soft: true }).then(() => { - - // Rename to target - return this.fileService.move(source, target, overwrite).then(() => { - - // Load models that were dirty before - return Promise.all(dirtyTargetModels.map(dirtyTargetModel => this.models.loadOrCreate(dirtyTargetModel))).then(() => undefined); - }, error => { - - // In case of an error, discard any dirty target backups that were made - return Promise.all(dirtyTargetModels.map(dirtyTargetModel => this.backupFileService.discardResourceBackup(dirtyTargetModel))) - .then(() => Promise.reject(error)); - }); - }); - }); - }); - }); + // Check for dirty untitled + return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString()); } + //#endregion + + //#region config + getAutoSaveMode(): AutoSaveMode { if (this.configuredAutoSaveOnFocusChange) { return AutoSaveMode.ON_FOCUS_CHANGE; @@ -1004,6 +984,8 @@ export class TextFileService extends Disposable implements ITextFileService { return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF; } + //#endregion + dispose(): void { // Clear all caches @@ -1011,6 +993,4 @@ export class TextFileService extends Disposable implements ITextFileService { super.dispose(); } -} - -registerSingleton(ITextFileService, TextFileService); \ No newline at end of file +} \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 3a64e58d77..e50956d535 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -7,8 +7,8 @@ import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IEncodingSupport, ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor'; -import { IResolveContentOptions, ITextSnapshot, IBaseStatWithMetadata } from 'vs/platform/files/common/files'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IResolveContentOptions, ITextSnapshot, IBaseStatWithMetadata, IWriteTextFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -265,17 +265,18 @@ export interface IResolvedTextFileEditorModel extends ITextFileEditorModel { export interface IWillMoveEvent { oldResource: URI; newResource: URI; + waitUntil(p: Promise): void; } export interface ITextFileService extends IDisposable { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; + + readonly onWillMove: Event; readonly onAutoSaveConfigurationChange: Event; readonly onFilesAssociationChange: Event; - onWillMove: Event; - readonly isHotExitEnabled: boolean; /** @@ -283,11 +284,6 @@ export interface ITextFileService extends IDisposable { */ readonly models: ITextFileEditorModelManager; - /** - * Resolve the contents of a file identified by the resource. - */ - resolveTextContent(resource: URI, options?: IResolveContentOptions): Promise; - /** * A resource is dirty if it has unsaved changes or is an untitled file not yet saved. * @@ -349,7 +345,17 @@ export interface ITextFileService extends IDisposable { * 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, options?: { overwrite?: boolean }): Promise; + create(resource: URI, contents?: string, options?: { overwrite?: boolean }): Promise; + + /** + * Resolve the contents of a file identified by the resource. + */ + resolve(resource: URI, options?: IResolveContentOptions): Promise; + + /** + * Update a file with given contents. + */ + write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise; /** * Delete a file. If the file is dirty, it will get reverted and then deleted from disk. diff --git a/src/vs/workbench/services/textfile/node/textFileService.ts b/src/vs/workbench/services/textfile/node/textFileService.ts new file mode 100644 index 0000000000..f66f82dcd0 --- /dev/null +++ b/src/vs/workbench/services/textfile/node/textFileService.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { tmpdir } from 'os'; +import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { URI } from 'vs/base/common/uri'; +import { ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { Schemas } from 'vs/base/common/network'; +import { exists, stat, chmod, rimraf } from 'vs/base/node/pfs'; +import { join, dirname } from 'vs/base/common/path'; +import { isMacintosh } from 'vs/base/common/platform'; +import product from 'vs/platform/product/node/product'; + +export class NodeTextFileService extends TextFileService { + + async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { + + // check for overwriteReadonly property (only supported for local file://) + try { + if (options && options.overwriteReadonly && resource.scheme === Schemas.file && await exists(resource.fsPath)) { + const fileStat = await stat(resource.fsPath); + + // try to change mode to writeable + await chmod(resource.fsPath, fileStat.mode | 128); + } + } catch (error) { + // ignore and simply retry the operation + } + + // check for writeElevated property (only supported for local file://) + if (options && options.writeElevated && resource.scheme === Schemas.file) { + return this.writeElevated(resource, value, options); + } + + return super.write(resource, value, options); + } + + private async writeElevated(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { + + // write into a tmp file first + const tmpPath = join(tmpdir(), `code-elevated-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 6)}`); + await this.write(URI.file(tmpPath), value, { encoding: this.fileService.encoding.getWriteEncoding(resource, options ? options.encoding : undefined).encoding }); + + // sudo prompt copy + await this.sudoPromptCopy(tmpPath, resource.fsPath, options); + + // clean up + await rimraf(tmpPath); + + return this.fileService.resolve(resource, { resolveMetadata: true }); + } + + private async sudoPromptCopy(source: string, target: string, options?: IWriteTextFileOptions): Promise { + + // load sudo-prompt module lazy + const sudoPrompt = await import('sudo-prompt'); + + return new Promise((resolve, reject) => { + const promptOptions = { + name: this.environmentService.appNameLong.replace('-', ''), + icns: (isMacintosh && this.environmentService.isBuilt) ? join(dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : undefined + }; + + const sudoCommand: string[] = [`"${this.environmentService.cliPath}"`]; + if (options && options.overwriteReadonly) { + sudoCommand.push('--file-chmod'); + } + + sudoCommand.push('--file-write', `"${source}"`, `"${target}"`); + + sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => { + if (error || stderr) { + reject(error || stderr); + } else { + resolve(undefined); + } + }); + }); + } +} + +registerSingleton(ITextFileService, NodeTextFileService); \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/node/textResourcePropertiesService.ts b/src/vs/workbench/services/textfile/node/textResourcePropertiesService.ts index d68fdef07f..acbc9cda7d 100644 --- a/src/vs/workbench/services/textfile/node/textResourcePropertiesService.ts +++ b/src/vs/workbench/services/textfile/node/textResourcePropertiesService.ts @@ -9,21 +9,22 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/resour import { OperatingSystem, OS } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; export class TextResourcePropertiesService implements ITextResourcePropertiesService { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; private remoteEnvironment: IRemoteAgentEnvironment | null = null; constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IWindowService private readonly windowService: IWindowService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IStorageService private readonly storageService: IStorageService ) { remoteAgentService.getEnvironment().then(remoteEnv => this.remoteEnvironment = remoteEnv); @@ -40,7 +41,8 @@ export class TextResourcePropertiesService implements ITextResourcePropertiesSer private getOS(resource: URI): OperatingSystem { let os = OS; - const remoteAuthority = this.windowService.getConfiguration().remoteAuthority; + + const remoteAuthority = this.environmentService.configuration.remoteAuthority; if (remoteAuthority) { if (resource.scheme !== Schemas.file) { const osCacheKey = `resource.authority.os.${remoteAuthority}`; @@ -48,6 +50,7 @@ export class TextResourcePropertiesService implements ITextResourcePropertiesSer this.storageService.store(osCacheKey, os, StorageScope.WORKSPACE); } } + return os; } } diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index ba9d62560f..6595244bed 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -21,9 +21,8 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ColorThemeStore } from 'vs/workbench/services/themes/browser/colorThemeStore'; import { FileIconThemeStore } from 'vs/workbench/services/themes/common/fileIconThemeStore'; import { FileIconThemeData } from 'vs/workbench/services/themes/common/fileIconThemeData'; -import { IWindowService } from 'vs/platform/windows/common/windows'; import { removeClasses, addClasses } from 'vs/base/browser/dom'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; @@ -100,8 +99,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @IStorageService private readonly storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IWindowService private readonly windowService: IWindowService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService ) { @@ -218,7 +216,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } if (this.watchedIconThemeLocation && this.currentIconTheme && e.contains(this.watchedIconThemeLocation, FileChangeType.UPDATED)) { await this.currentIconTheme.reload(this.fileService); - _applyIconTheme(this.currentIconTheme, () => Promise.resolve(this.currentIconTheme)); + _applyIconTheme(this.currentIconTheme, () => { + this.doSetFileIconTheme(this.currentIconTheme); + return Promise.resolve(this.currentIconTheme); + }); } }); } @@ -243,7 +244,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { let detectHCThemeSetting = this.configurationService.getValue(DETECT_HC_SETTING); let colorThemeSetting: string; - if (this.windowService.getConfiguration().highContrast && detectHCThemeSetting) { + if (this.environmentService.configuration.highContrast && detectHCThemeSetting) { colorThemeSetting = HC_THEME_ID; } else { colorThemeSetting = this.configurationService.getValue(COLOR_THEME_SETTING); @@ -251,15 +252,11 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { let iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); - const extDevLoc = this.environmentService.extensionDevelopmentLocationURI; + const extDevLocs = this.environmentService.extensionDevelopmentLocationURI; let uri: URI | undefined; - if (Array.isArray(extDevLoc)) { + if (extDevLocs && extDevLocs.length > 0) { // if there are more than one ext dev paths, use first - if (extDevLoc.length > 0) { - uri = extDevLoc[0]; - } - } else { - uri = extDevLoc; + uri = extDevLocs[0]; } return Promise.all([ diff --git a/src/vs/workbench/services/timer/electron-browser/timerService.ts b/src/vs/workbench/services/timer/electron-browser/timerService.ts index 5bbce190fa..fd947df735 100644 --- a/src/vs/workbench/services/timer/electron-browser/timerService.ts +++ b/src/vs/workbench/services/timer/electron-browser/timerService.ts @@ -7,7 +7,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { virtualMachineHint } from 'vs/base/node/id'; import * as perf from 'vs/base/common/performance'; import * as os from 'os'; -import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -19,7 +20,6 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; - /* __GDPR__FRAGMENT__ "IMemoryInfo" : { "workingSetSize" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, @@ -312,7 +312,7 @@ class TimerService implements ITimerService { constructor( @IWindowsService private readonly _windowsService: IWindowsService, - @IWindowService private readonly _windowService: IWindowService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IExtensionService private readonly _extensionService: IExtensionService, @@ -335,7 +335,7 @@ class TimerService implements ITimerService { private async _computeStartupMetrics(): Promise { const now = Date.now(); - const initialStartup = !!this._windowService.getConfiguration().isInitialStartup; + const initialStartup = !!this._environmentService.configuration.isInitialStartup; const startMark = initialStartup ? 'main:started' : 'main:loadWindow'; let totalmem: number | undefined; diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts index 6f3defe6d2..8fe6db11e1 100644 --- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts @@ -7,7 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator, IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import * as arrays from 'vs/base/common/arrays'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; -import { IFilesConfiguration } from 'vs/platform/files/common/files'; +import { IFilesConfiguration, IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; @@ -132,7 +132,8 @@ export class UntitledEditorService extends Disposable implements IUntitledEditor constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IFileService private readonly fileService: IFileService ) { super(); } @@ -194,10 +195,10 @@ export class UntitledEditorService extends Disposable implements IUntitledEditor } createOrGet(resource?: URI, modeId?: string, initialValue?: string, encoding?: string, hasAssociatedFilePath: boolean = false): UntitledEditorInput { - if (resource) { - // Massage resource if it comes with a file:// scheme - if (resource.scheme === Schemas.file) { + + // Massage resource if it comes with known file based resource + if (this.fileService.canHandleResource(resource)) { hasAssociatedFilePath = true; resource = resource.with({ scheme: Schemas.untitled }); // ensure we have the right scheme } diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/workbench/services/window/electron-browser/windowService.ts similarity index 84% rename from src/vs/platform/windows/electron-browser/windowService.ts rename to src/vs/workbench/services/window/electron-browser/windowService.ts index 5bd18c6f0f..8cc4fc0d9d 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/workbench/services/window/electron-browser/windowService.ts @@ -4,13 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { IWindowService, IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, IDevToolsOptions, IOpenSettings, IURIToOpen, isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; +import { IWindowService, IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IDevToolsOptions, IOpenSettings, IURIToOpen, isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class WindowService extends Disposable implements IWindowService { @@ -19,24 +21,26 @@ export class WindowService extends Disposable implements IWindowService { _serviceBrand: any; - private windowId: number; + private _windowId: number; + private remoteAuthority: string | undefined; private _hasFocus: boolean; get hasFocus(): boolean { return this._hasFocus; } constructor( - private configuration: IWindowConfiguration, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IWindowsService private readonly windowsService: IWindowsService, @ILabelService private readonly labelService: ILabelService ) { super(); - this.windowId = configuration.windowId; + this._windowId = environmentService.configuration.windowId; + this.remoteAuthority = environmentService.configuration.remoteAuthority; - const onThisWindowFocus = Event.map(Event.filter(windowsService.onWindowFocus, id => id === this.windowId), _ => true); - const onThisWindowBlur = Event.map(Event.filter(windowsService.onWindowBlur, id => id === this.windowId), _ => false); - const onThisWindowMaximize = Event.map(Event.filter(windowsService.onWindowMaximize, id => id === this.windowId), _ => true); - const onThisWindowUnmaximize = Event.map(Event.filter(windowsService.onWindowUnmaximize, id => id === this.windowId), _ => false); + const onThisWindowFocus = Event.map(Event.filter(windowsService.onWindowFocus, id => id === this._windowId), _ => true); + const onThisWindowBlur = Event.map(Event.filter(windowsService.onWindowBlur, id => id === this._windowId), _ => false); + const onThisWindowMaximize = Event.map(Event.filter(windowsService.onWindowMaximize, id => id === this._windowId), _ => true); + const onThisWindowUnmaximize = Event.map(Event.filter(windowsService.onWindowUnmaximize, id => id === this._windowId), _ => false); this.onDidChangeFocus = Event.any(onThisWindowFocus, onThisWindowBlur); this.onDidChangeMaximize = Event.any(onThisWindowMaximize, onThisWindowUnmaximize); @@ -45,12 +49,8 @@ export class WindowService extends Disposable implements IWindowService { this._register(this.onDidChangeFocus(focus => this._hasFocus = focus)); } - getCurrentWindowId(): number { - return this.windowId; - } - - getConfiguration(): IWindowConfiguration { - return this.configuration; + get windowId(): number { + return this._windowId; } pickFileFolderAndOpen(options: INativeOpenDialogOptions): Promise { @@ -98,9 +98,10 @@ export class WindowService extends Disposable implements IWindowService { } openWindow(uris: IURIToOpen[], options: IOpenSettings = {}): Promise { - if (!!this.configuration.remoteAuthority) { + if (!!this.remoteAuthority) { uris.forEach(u => u.label = u.label || this.getRecentLabel(u)); } + return this.windowsService.openWindow(this.windowId, uris, options); } @@ -183,3 +184,4 @@ export class WindowService extends Disposable implements IWindowService { } } +registerSingleton(IWindowService, WindowService); \ No newline at end of file diff --git a/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts b/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts index 9f66b0f199..5aa051d683 100644 --- a/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts @@ -24,13 +24,14 @@ import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform'; import { isEqual, basename, isEqualOrParent, getComparisonKey } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService } from 'vs/platform/files/common/files'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILabelService } from 'vs/platform/label/common/label'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export class WorkspaceEditingService implements IWorkspaceEditingService { @@ -46,10 +47,11 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { @IBackupFileService private readonly backupFileService: IBackupFileService, @INotificationService private readonly notificationService: INotificationService, @ICommandService private readonly commandService: ICommandService, - @IFileService private readonly fileSystemService: IFileService, + @IFileService private readonly fileService: IFileService, + @ITextFileService private readonly textFileService: ITextFileService, @IWindowsService private readonly windowsService: IWindowsService, @IWorkspacesService private readonly workspaceService: IWorkspacesService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IDialogService private readonly dialogService: IDialogService, @ILifecycleService readonly lifecycleService: ILifecycleService, @@ -245,7 +247,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { if (path && !this.isValidTargetWorkspacePath(path)) { return Promise.reject(null); } - const remoteAuthority = this.windowService.getConfiguration().remoteAuthority; + const remoteAuthority = this.environmentService.configuration.remoteAuthority; const untitledWorkspace = await this.workspaceService.createUntitledWorkspace(folders, remoteAuthority); if (path) { await this.saveWorkspaceAs(untitledWorkspace, path); @@ -296,9 +298,9 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } // Read the contents of the workspace file, update it to new location and save it. - const raw = await this.fileSystemService.resolveContent(configPathURI); + const raw = await this.fileService.resolveContent(configPathURI); const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value, configPathURI, targetConfigPathURI); - await this.fileSystemService.createFile(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true }); + await this.textFileService.create(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true }); } private handleWorkspaceConfigurationEditingError(error: JSONEditingError): Promise { @@ -360,7 +362,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } } - if (this.windowService.getConfiguration().remoteAuthority) { + if (this.environmentService.configuration.remoteAuthority) { this.windowService.reloadWindow(); // TODO aeschli: workaround until restarting works } else { this.extensionService.startExtensionHost(); diff --git a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index 5a42125d73..458ac3380e 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts @@ -7,24 +7,24 @@ import * as assert from 'assert'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { URI } from 'vs/base/common/uri'; -import * as types from 'vs/workbench/api/node/extHostTypes'; +import * as types from 'vs/workbench/api/common/extHostTypes'; import { TextModel as EditorModel } from 'vs/editor/common/model/textModel'; import { TestRPCProtocol } from './testRPCProtocol'; import { MarkerService } from 'vs/platform/markers/common/markerService'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { ExtHostLanguageFeatures } from 'vs/workbench/api/node/extHostLanguageFeatures'; +import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; import { MainThreadLanguageFeatures } from 'vs/workbench/api/browser/mainThreadLanguageFeatures'; import { IHeapService, NullHeapService } from 'vs/workbench/services/heap/common/heap'; -import { ExtHostApiCommands } from 'vs/workbench/api/node/extHostApiCommands'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; -import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; +import { ExtHostApiCommands } from 'vs/workbench/api/common/extHostApiCommands'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostHeapService } from 'vs/workbench/api/common/extHostHeapService'; import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; -import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { MainContext, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; +import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import * as vscode from 'vscode'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import 'vs/workbench/contrib/search/browser/search.contribution'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts index 24ac41c342..eda5b13f45 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainThreadCommandsShape } from 'vs/workbench/api/common/extHost.protocol'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts b/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts index ed59a63481..92a05d2bbf 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; -import { ExtHostConfigProvider } from 'vs/workbench/api/node/extHostConfiguration'; +import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { MainThreadConfigurationShape, IConfigurationInitData } from 'vs/workbench/api/common/extHost.protocol'; import { ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { TestRPCProtocol } from './testRPCProtocol'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts index 17d1fa8c97..61f7175c09 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { DiagnosticCollection, ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; -import { Diagnostic, DiagnosticSeverity, Range, DiagnosticRelatedInformation, Location } from 'vs/workbench/api/node/extHostTypes'; +import { DiagnosticCollection, ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; +import { Diagnostic, DiagnosticSeverity, Range, DiagnosticRelatedInformation, Location } from 'vs/workbench/api/common/extHostTypes'; import { MainThreadDiagnosticsShape, IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts index c4dfa82c00..0522295185 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentData.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocumentData'; -import { Position } from 'vs/workbench/api/node/extHostTypes'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; +import { Position } from 'vs/workbench/api/common/extHostTypes'; import { Range } from 'vs/editor/common/core/range'; import { MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts index f725636054..6d8ce3da6b 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; -import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbench/api/node/extHostTypes'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbench/api/common/extHostTypes'; import { MainThreadTextEditorsShape, WorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; +import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostDocumentSaveParticipant'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as vscode from 'vscode'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentsAndEditors.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentsAndEditors.test.ts index 3c1d91a9b2..dc3556cfb4 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentsAndEditors.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; suite('ExtHostDocumentsAndEditors', () => { diff --git a/src/vs/workbench/test/electron-browser/api/extHostFileSystemEventService.test.ts b/src/vs/workbench/test/electron-browser/api/extHostFileSystemEventService.test.ts index 38ae798de8..4e701b56e5 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostFileSystemEventService.test.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ExtHostFileSystemEventService } from 'vs/workbench/api/node/extHostFileSystemEventService'; +import { ExtHostFileSystemEventService } from 'vs/workbench/api/common/extHostFileSystemEventService'; import { IMainContext } from 'vs/workbench/api/common/extHost.protocol'; suite('ExtHostFileSystemEventService', () => { diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index c1d013188c..86d1abc0b1 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -7,20 +7,20 @@ import * as assert from 'assert'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; -import * as types from 'vs/workbench/api/node/extHostTypes'; +import * as types from 'vs/workbench/api/common/extHostTypes'; import { TextModel as EditorModel } from 'vs/editor/common/model/textModel'; import { Position as EditorPosition, Position } from 'vs/editor/common/core/position'; import { Range as EditorRange } from 'vs/editor/common/core/range'; import { TestRPCProtocol } from './testRPCProtocol'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { MarkerService } from 'vs/platform/markers/common/markerService'; -import { ExtHostLanguageFeatures } from 'vs/workbench/api/node/extHostLanguageFeatures'; +import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures'; import { MainThreadLanguageFeatures } from 'vs/workbench/api/browser/mainThreadLanguageFeatures'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; import { IHeapService, NullHeapService } from 'vs/workbench/services/heap/common/heap'; -import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; import * as modes from 'vs/editor/common/modes'; import { getCodeLensData } from 'vs/editor/contrib/codelens/codelens'; @@ -36,8 +36,8 @@ import { provideSuggestionItems, CompletionOptions } from 'vs/editor/contrib/sug import { getDocumentFormattingEditsUntilResult, getDocumentRangeFormattingEditsUntilResult, getOnTypeFormattingEdits } from 'vs/editor/contrib/format/format'; import { getLinks } from 'vs/editor/contrib/links/getLinks'; import { MainContext, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; -import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; +import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; +import { ExtHostHeapService } from 'vs/workbench/api/common/extHostHeapService'; import * as vscode from 'vscode'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index d65b46c434..a1638d7cd3 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -13,7 +13,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; -import { Range } from 'vs/workbench/api/node/extHostTypes'; +import { Range } from 'vs/workbench/api/common/extHostTypes'; import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteStats, ISearchQuery, ITextQuery, QueryType, resultIsMatch } from 'vs/workbench/services/search/common/search'; import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; import { TestLogService } from 'vs/workbench/test/workbenchTestServices'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts index 086ee14685..1512f53e89 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TextEditorLineNumbersStyle, Range } from 'vs/workbench/api/node/extHostTypes'; +import { TextEditorLineNumbersStyle, Range } from 'vs/workbench/api/common/extHostTypes'; import { TextEditorCursorStyle, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { MainThreadTextEditorsShape, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostTextEditorOptions, ExtHostTextEditor } from 'vs/workbench/api/node/extHostTextEditor'; -import { ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocumentData'; +import { ExtHostTextEditorOptions, ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts index 4b4ce1b9e5..a7603f9c63 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { MainContext, MainThreadTextEditorsShape, WorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; -import { ExtHostEditors } from 'vs/workbench/api/node/extHostTextEditors'; +import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; import { ResourceTextEdit } from 'vs/editor/common/modes'; suite('ExtHostTextEditors.applyWorkspaceEdit', () => { diff --git a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts index f211ddb40e..7b230b439a 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -6,12 +6,12 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { Emitter } from 'vs/base/common/event'; -import { ExtHostTreeViews } from 'vs/workbench/api/node/extHostTreeViews'; -import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; +import { ExtHostTreeViews } from 'vs/workbench/api/common/extHostTreeViews'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainThreadTreeViewsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { TreeDataProvider, TreeItem } from 'vscode'; import { TestRPCProtocol } from './testRPCProtocol'; -import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; +import { ExtHostHeapService } from 'vs/workbench/api/common/extHostHeapService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { MainThreadCommands } from 'vs/workbench/api/browser/mainThreadCommands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts index c6fc73de8e..f9e7502b50 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts @@ -5,10 +5,10 @@ import * as assert from 'assert'; -import { MarkdownString, LogLevel } from 'vs/workbench/api/node/extHostTypeConverters'; +import { MarkdownString, LogLevel } from 'vs/workbench/api/common/extHostTypeConverters'; import { isEmptyObject } from 'vs/base/common/types'; import { size } from 'vs/base/common/collections'; -import * as types from 'vs/workbench/api/node/extHostTypes'; +import * as types from 'vs/workbench/api/common/extHostTypes'; import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; suite('ExtHostTypeConverter', function () { diff --git a/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts index c997f4e076..20e7cb6212 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import * as types from 'vs/workbench/api/node/extHostTypes'; +import * as types from 'vs/workbench/api/common/extHostTypes'; import { isWindows } from 'vs/base/common/platform'; function assertToJSON(a: any, expected: any) { diff --git a/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts index 81eb58df72..46474373c1 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { MainThreadWebviews } from 'vs/workbench/api/electron-browser/mainThreadWebview'; -import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview'; +import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import * as vscode from 'vscode'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts index bb7057ec51..6a887f089a 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts @@ -13,8 +13,8 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IWorkspaceFolderData } from 'vs/platform/workspace/common/workspace'; import { MainThreadWorkspace } from 'vs/workbench/api/browser/mainThreadWorkspace'; import { IMainContext, IWorkspaceData, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { RelativePattern } from 'vs/workbench/api/node/extHostTypes'; -import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; +import { RelativePattern } from 'vs/workbench/api/common/extHostTypes'; +import { ExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { TestRPCProtocol } from './testRPCProtocol'; diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts index d2080bd57b..8634fbbde0 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts @@ -13,7 +13,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol'; import { createTestCodeEditor, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; -import { TestEditorService, TestEditorGroupsService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices'; +import { TestEditorService, TestEditorGroupsService, TestTextResourcePropertiesService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; import { Event } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -81,7 +81,8 @@ suite('MainThreadDocumentsAndEditors', () => { getActivePanel() { return null; } - } + }, + TestEnvironmentService ); /* tslint:enable */ }); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts index b67404f336..c9640a7423 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts @@ -54,7 +54,7 @@ suite('MainThreadEditors', () => { isDirty() { return false; } create(uri: URI, contents?: string, options?: any) { createdResources.add(uri); - return Promise.resolve(undefined); + return Promise.resolve(Object.create(null)); } delete(resource: URI) { deletedResources.add(resource); @@ -113,7 +113,8 @@ suite('MainThreadEditors', () => { getActivePanel() { return null; } - } + }, + TestEnvironmentService ); editors = new MainThreadTextEditors( diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index f95cd4ff76..4f4c0bb46a 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -26,18 +26,17 @@ import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchS import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; -import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncodings, IResourceEncoding, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions } from 'vs/platform/files/common/files'; +import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IWriteTextFileOptions, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncodings, IResourceEncoding, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IRawTextContent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { parseArgs } from 'vs/platform/environment/node/argv'; -import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IWindowsService, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, MenuBarVisibility, IURIToOpen, IOpenSettings } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, MenuBarVisibility, IURIToOpen, IOpenSettings, IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -81,12 +80,15 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPanel } from 'vs/workbench/common/panel'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; +import { VSBuffer } from 'vs/base/common/buffer'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined); } -export const TestEnvironmentService = new EnvironmentService(parseArgs(process.argv), process.execPath); +export const TestEnvironmentService = new WorkbenchEnvironmentService(parseArgs(process.argv) as IWindowConfiguration, process.execPath); export class TestContextService implements IWorkspaceContextService { public _serviceBrand: any; @@ -190,8 +192,7 @@ export class TestTextFileService extends TextFileService { @IConfigurationService configurationService: IConfigurationService, @IModeService modeService: IModeService, @IModelService modelService: IModelService, - @IWindowService windowService: IWindowService, - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @INotificationService notificationService: INotificationService, @IBackupFileService backupFileService: IBackupFileService, @IWindowsService windowsService: IWindowsService, @@ -210,7 +211,6 @@ export class TestTextFileService extends TextFileService { configurationService, modeService, modelService, - windowService, environmentService, notificationService, backupFileService, @@ -235,7 +235,7 @@ export class TestTextFileService extends TextFileService { this.resolveTextContentError = error; } - public resolveTextContent(resource: URI, options?: IResolveContentOptions): Promise { + public resolve(resource: URI, options?: IResolveContentOptions): Promise { if (this.resolveTextContentError) { const error = this.resolveTextContentError; this.resolveTextContentError = null; @@ -659,6 +659,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { orientation: any; whenRestored: Promise = Promise.resolve(undefined); + willRestoreEditors = false; dimension = { width: 800, height: 600 }; @@ -983,7 +984,7 @@ export class TestFileService implements IFileService { }); } - updateContent(resource: URI, _value: string | ITextSnapshot, _options?: IUpdateContentOptions): Promise { + updateContent(resource: URI, _value: string | ITextSnapshot, _options?: IWriteTextFileOptions): Promise { return timeout(0).then(() => ({ resource, etag: 'index.txt', @@ -995,6 +996,10 @@ export class TestFileService implements IFileService { })); } + writeFile(resource: URI, bufferOrReadable: VSBuffer, options?: IWriteFileOptions): Promise { + return this.updateContent(resource, bufferOrReadable.toString(), options); + } + move(_source: URI, _target: URI, _overwrite?: boolean): Promise { return Promise.resolve(null!); } @@ -1007,6 +1012,10 @@ export class TestFileService implements IFileService { throw new Error('not implemented'); } + createFile2(_resource: URI, _content?: VSBuffer, _options?: ICreateFileOptions): Promise { + throw new Error('not implemented'); + } + createFolder(_resource: URI): Promise { throw new Error('not implemented'); } @@ -1138,6 +1147,8 @@ export class TestWindowService implements IWindowService { hasFocus = true; + readonly windowId = 0; + isFocused(): Promise { return Promise.resolve(false); } @@ -1146,14 +1157,6 @@ export class TestWindowService implements IWindowService { return Promise.resolve(false); } - getConfiguration(): IWindowConfiguration { - return Object.create(null); - } - - getCurrentWindowId(): number { - return 0; - } - pickFileFolderAndOpen(_options: INativeOpenDialogOptions): Promise { return Promise.resolve(); } diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 28693c70bc..534d3a500a 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -61,7 +61,7 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { TextResourceConfigurationService } from 'vs/editor/common/services/resourceConfigurationImpl'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { AccessibilityService } from 'vs/platform/accessibility/node/accessibilityService'; +import { AccessibilityService } from 'vs/workbench/services/accessibility/node/accessibilityService'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; @@ -73,8 +73,6 @@ import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; import { LocalizationsService } from 'vs/platform/localizations/electron-browser/localizationsService'; import { ISharedProcessService, SharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { TelemetryService } from 'vs/platform/telemetry/electron-browser/telemetryService'; import { IProductService } from 'vs/platform/product/common/product'; import { ProductService } from 'vs/platform/product/node/productService'; import { IWindowsService } from 'vs/platform/windows/common/windows'; @@ -89,12 +87,17 @@ import { IMenubarService } from 'vs/platform/menubar/common/menubar'; import { MenubarService } from 'vs/platform/menubar/electron-browser/menubarService'; import { IURLService } from 'vs/platform/url/common/url'; import { RelayURLService } from 'vs/platform/url/electron-browser/urlService'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { TunnelService } from 'vs/workbench/services/remote/node/tunnelService'; +import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/electron-browser/configurationResolverService'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; + +import 'vs/platform/remote/node/tunnelService'; import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import 'vs/workbench/services/integrity/node/integrityService'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/textMate/electron-browser/textMateService'; -import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import 'vs/workbench/services/workspace/electron-browser/workspaceEditingService'; import 'vs/workbench/services/extensions/common/inactiveExtensionUrlHandler'; import 'vs/workbench/services/decorations/browser/decorationsService'; @@ -107,7 +110,7 @@ import 'vs/workbench/services/preferences/browser/preferencesService'; import 'vs/workbench/services/output/node/outputChannelModelService'; import 'vs/workbench/services/configuration/common/jsonEditingService'; import 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; -import 'vs/workbench/services/textfile/common/textFileService'; +import 'vs/workbench/services/textfile/node/textFileService'; import 'vs/workbench/services/dialogs/browser/fileDialogService'; import 'vs/workbench/services/dialogs/electron-browser/dialogService'; import 'vs/workbench/services/backup/node/backupFileService'; @@ -131,6 +134,8 @@ import 'vs/workbench/services/extensions/electron-browser/extensionManagementSer import 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/heap/node/heap'; +import 'vs/workbench/services/window/electron-browser/windowService'; +import 'vs/workbench/services/telemetry/electron-browser/telemetryService'; registerSingleton(IMenuService, MenuService, true); registerSingleton(IListService, ListService, true); @@ -150,7 +155,6 @@ registerSingleton(IRequestService, RequestService, true); registerSingleton(ILifecycleService, LifecycleService); registerSingleton(ILocalizationsService, LocalizationsService); registerSingleton(ISharedProcessService, SharedProcessService, true); -registerSingleton(ITelemetryService, TelemetryService); registerSingleton(IProductService, ProductService, true); registerSingleton(IWindowsService, WindowsService); registerSingleton(IUpdateService, UpdateService); @@ -158,6 +162,8 @@ registerSingleton(IIssueService, IssueService); registerSingleton(IWorkspacesService, WorkspacesService); registerSingleton(IMenubarService, MenubarService); registerSingleton(IURLService, RelayURLService); +registerSingleton(ITunnelService, TunnelService, true); +registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); //#endregion diff --git a/src/vs/workbench/workbench.nodeless.main.ts b/src/vs/workbench/workbench.nodeless.main.ts index d14d57ff5e..4524e466f4 100644 --- a/src/vs/workbench/workbench.nodeless.main.ts +++ b/src/vs/workbench/workbench.nodeless.main.ts @@ -75,8 +75,6 @@ import { ContextViewService } from 'vs/platform/contextview/browser/contextViewS // import { ISharedProcessService, SharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; // import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-browser/remoteAuthorityResolverService'; // import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -// import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -// import { TelemetryService } from 'vs/platform/telemetry/electron-browser/telemetryService'; // import { IProductService } from 'vs/platform/product/common/product'; // import { ProductService } from 'vs/platform/product/node/productService'; // import { IWindowsService } from 'vs/platform/windows/common/windows'; @@ -93,6 +91,10 @@ import { ContextViewService } from 'vs/platform/contextview/browser/contextViewS // import { RelayURLService } from 'vs/platform/url/electron-browser/urlService'; import { IHeapService, NullHeapService } from 'vs/workbench/services/heap/common/heap'; import { IBroadcastService, NullBroadcastService } from 'vs/workbench/services/broadcast/common/broadcast'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; +import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import 'vs/workbench/browser/nodeless.simpleservices'; import 'vs/platform/dialogs/browser/dialogService'; @@ -102,7 +104,6 @@ import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; // import 'vs/workbench/services/integrity/node/integrityService'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; // import 'vs/workbench/services/textMate/electron-browser/textMateService'; -import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; // import 'vs/workbench/services/workspace/electron-browser/workspaceEditingService'; // import 'vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler'; import 'vs/workbench/services/decorations/browser/decorationsService'; @@ -114,7 +115,7 @@ import 'vs/workbench/services/preferences/browser/preferencesService'; import 'vs/workbench/services/output/common/outputChannelModelService'; import 'vs/workbench/services/configuration/common/jsonEditingService'; import 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; -import 'vs/workbench/services/textfile/common/textFileService'; +// import 'vs/workbench/services/textfile/node/textFileService'; import 'vs/workbench/services/dialogs/browser/fileDialogService'; // import 'vs/workbench/services/dialogs/electron-browser/dialogService'; // import 'vs/workbench/services/backup/node/backupFileService'; @@ -137,7 +138,9 @@ import 'vs/workbench/services/label/common/labelService'; // import 'vs/workbench/services/extensions/electron-browser/extensionManagementServerService'; // import 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import 'vs/workbench/services/notification/common/notificationService'; - +// import 'vs/workbench/services/heap/node/heap'; +// import 'vs/workbench/services/window/electron-browser/windowService'; +// import 'vs/workbench/services/telemetry/electron-browser/telemetryService'; @@ -159,7 +162,6 @@ registerSingleton(IContextViewService, ContextViewService, true); // registerSingleton(ILifecycleService, LifecycleService); // registerSingleton(ILocalizationsService, LocalizationsService); // registerSingleton(ISharedProcessService, SharedProcessService, true); -// registerSingleton(ITelemetryService, TelemetryService); // registerSingleton(IProductService, ProductService, true); // registerSingleton(IWindowsService, WindowsService); // registerSingleton(IUpdateService, UpdateService); @@ -169,8 +171,9 @@ registerSingleton(IContextViewService, ContextViewService, true); // registerSingleton(IURLService, RelayURLService); registerSingleton(IHeapService, NullHeapService); registerSingleton(IBroadcastService, NullBroadcastService); - registerSingleton(IContextMenuService, ContextMenuService); +registerSingleton(ITextFileService, TextFileService); +registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); //#endregion diff --git a/tslint.json b/tslint.json index 008be35943..4940f999ff 100644 --- a/tslint.json +++ b/tslint.json @@ -44,4 +44,4 @@ "no-nls-in-standalone-editor": true }, "defaultSeverity": "warning" -} \ No newline at end of file +}