From cb5bcf224826a7f1db6e47bf1e319744cbbd1cfc Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Fri, 5 Apr 2019 10:09:18 -0700 Subject: [PATCH] Merge from vscode 2b0b9136329c181a9e381463a1f7dc3a2d105a34 (#4880) --- .vscode/tasks.json | 14 + .yarnrc | 2 +- build/gulpfile.vscode.js | 3 +- build/package.json | 2 +- build/yarn.lock | 8 +- cgmanifest.json | 4 +- .../debug-server-ready/.vscode/launch.json | 16 + extensions/git/src/commands.ts | 1 + .../src/commands/refreshPreview.ts | 5 +- .../src/extension.ts | 2 +- .../src/features/documentLinkProvider.ts | 6 +- .../src/features/foldingProvider.ts | 42 +- .../src/features/workspaceSymbolProvider.ts | 3 +- .../src/markdownEngine.ts | 9 + .../src/typings/ref.d.ts | 1 + .../src/util/arrays.ts | 4 + .../src/util/links.ts | 4 +- package.json | 5 +- .../browser/ui/editableDropdown/dropdown.ts | 2 +- .../modelComponents/webview.component.ts | 3 +- src/sql/parts/notebook/notebook.component.ts | 2 +- .../objectExplorer/viewlet/treeUpdateUtils.ts | 38 +- src/sql/parts/query/editor/messagePanel.ts | 27 +- .../taskHistory/viewlet/taskHistoryView.ts | 7 +- .../browser/connectionDialogWidget.ts | 9 +- .../browser/fileBrowserTreeView.ts | 5 +- .../parts/insights/insightsUtils.test.ts | 4 +- src/sqltest/stubs/contextKeyServiceStub.ts | 3 + src/typings/electron.d.ts | 4 +- src/typings/yauzl.d.ts | 5 +- src/vs/base/browser/dnd.ts | 2 +- src/vs/base/browser/hash.ts | 15 + src/vs/base/browser/ui/actionbar/actionbar.ts | 2 +- .../ui/breadcrumbs/breadcrumbsWidget.ts | 2 +- src/vs/base/browser/ui/button/button.ts | 2 +- .../browser/ui/contextview/contextview.ts | 2 +- src/vs/base/browser/ui/dialog/dialog.css | 7 +- src/vs/base/browser/ui/dropdown/dropdown.ts | 12 +- src/vs/base/browser/ui/grid/grid.ts | 8 +- .../ui/highlightedlabel/highlightedLabel.ts | 26 +- .../ui/keybindingLabel/keybindingLabel.ts | 4 +- src/vs/base/browser/ui/list/rangeMap.ts | 2 +- .../browser/ui/progressbar/progressbar.ts | 10 +- src/vs/base/browser/ui/tree/abstractTree.ts | 34 +- src/vs/base/common/async.ts | 13 - src/vs/base/common/buffer.ts | 21 +- src/vs/base/common/cancellation.ts | 10 +- src/vs/base/common/event.ts | 57 +- src/vs/base/common/extpath.ts | 56 +- src/vs/base/common/glob.ts | 23 +- src/vs/base/common/linkedList.ts | 87 +-- src/vs/base/common/objects.ts | 28 - src/vs/base/common/processes.ts | 11 + src/vs/base/common/types.ts | 23 +- src/vs/base/common/uint.ts | 112 --- src/vs/base/node/config.ts | 73 +- src/vs/base/node/cpuUsage.sh | 2 +- src/vs/base/node/decoder.ts | 4 +- src/vs/base/node/encoding.ts | 2 +- src/vs/base/node/extfs.ts | 707 ------------------ src/vs/base/node/extpath.ts | 91 +++ src/vs/base/node/flow.ts | 187 ----- src/vs/base/node/pfs.ts | 646 +++++++++++++--- src/vs/base/node/ps.ts | 17 +- src/vs/base/node/storage.ts | 79 +- src/vs/base/node/watcher.ts | 192 +++++ src/vs/base/node/zip.ts | 33 +- .../electron-browser/contextmenu.ts | 4 +- src/vs/base/parts/ipc/common/ipc.net.ts | 50 +- src/vs/base/parts/ipc/node/ipc.net.ts | 4 + .../parts/quickopen/browser/quickOpenModel.ts | 21 +- src/vs/base/parts/tree/browser/tree.ts | 44 -- src/vs/base/parts/tree/browser/treeImpl.ts | 26 +- src/vs/base/parts/tree/browser/treeModel.ts | 13 - src/vs/base/parts/tree/browser/treeUtils.ts | 18 - src/vs/base/parts/tree/browser/treeView.ts | 31 - src/vs/base/test/browser/hash.test.ts | 16 + .../test/browser/ui/tree/objectTree.test.ts | 40 +- src/vs/base/test/common/cancellation.test.ts | 15 + src/vs/base/test/common/event.test.ts | 130 +++- src/vs/base/test/common/extpath.test.ts | 51 +- src/vs/base/test/common/linkedList.test.ts | 8 + src/vs/base/test/common/types.test.ts | 27 +- src/vs/base/test/node/extfs/extfs.test.ts | 615 --------------- src/vs/base/test/node/extpath.test.ts | 73 ++ src/vs/base/test/node/flow.test.ts | 488 ------------ src/vs/base/test/node/glob.test.ts | 6 +- src/vs/base/test/node/pfs.test.ts | 138 ---- .../fixtures/examples/company.jxs | 0 .../fixtures/examples/conway.jxs | 0 .../fixtures/examples/employee.jxs | 0 .../fixtures/examples/small.jxs | 0 .../node/{extfs => pfs}/fixtures/index.html | 0 .../node/{extfs => pfs}/fixtures/site.css | 2 +- src/vs/base/test/node/pfs/pfs.test.ts | 612 +++++++++++++++ src/vs/base/test/node/storage/storage.test.ts | 34 +- src/vs/base/test/node/utils.ts | 6 +- .../issue/issueReporterMain.ts | 30 +- .../issue/issueReporterModel.ts | 18 +- .../issue/test/testReporterModel.test.ts | 15 +- .../processExplorer/processExplorerMain.ts | 3 +- .../sharedProcess/contrib/logsDataCleaner.ts | 9 +- .../contrib/nodeCachedDataCleaner.ts | 15 +- .../contrib/storageDataCleaner.ts | 9 +- src/vs/code/electron-main/app.ts | 52 +- src/vs/code/electron-main/main.ts | 18 +- src/vs/code/electron-main/window.ts | 20 +- src/vs/code/electron-main/windows.ts | 293 ++++---- src/vs/code/node/cli.ts | 7 +- src/vs/code/node/cliProcessMain.ts | 8 +- src/vs/code/node/paths.ts | 3 +- src/vs/code/node/shellEnv.ts | 4 +- src/vs/editor/browser/core/editorState.ts | 55 +- .../editor/browser/widget/codeEditorWidget.ts | 125 ++-- src/vs/editor/common/model/textModel.ts | 20 +- src/vs/editor/common/modes.ts | 19 +- .../common/services/resourceConfiguration.ts | 2 +- .../editor/contrib/codeAction/codeAction.ts | 14 +- .../editor/contrib/codelens/codeLensCache.ts | 120 +++ .../contrib/codelens/codelensController.ts | 31 +- .../contrib/codelens/codelensWidget.css | 13 +- .../editor/contrib/codelens/codelensWidget.ts | 31 +- .../contrib/documentSymbols/outlineModel.ts | 10 +- src/vs/editor/contrib/find/findWidget.ts | 8 +- .../editor/contrib/find/simpleFindWidget.ts | 13 + src/vs/editor/contrib/format/format.ts | 16 +- .../contrib/goToDefinition/goToDefinition.ts | 4 +- .../goToDefinition/goToDefinitionCommands.ts | 9 +- .../linesOperations/linesOperations.ts | 50 +- .../test/linesOperations.test.ts | 59 +- .../contrib/snippet/snippetVariables.ts | 9 +- .../common/monarch/monarchCompile.ts | 32 +- .../test/common/model/textModel.test.ts | 20 + src/vs/monaco.d.ts | 1 + src/vs/platform/actions/common/menuService.ts | 4 +- .../backup/electron-main/backupMainService.ts | 9 +- .../electron-main/backupMainService.test.ts | 4 +- .../configuration/node/configuration.ts | 56 +- .../contextkey/browser/contextKeyService.ts | 93 +-- .../platform/contextkey/common/contextkey.ts | 3 + .../diagnostics/common/diagnosticsService.ts | 44 ++ .../electron-main/diagnosticsService.ts | 352 ++------- .../diagnostics/node/diagnosticsService.ts | 167 +++++ src/vs/platform/dialogs/common/dialogs.ts | 8 +- .../environment/common/environment.ts | 2 +- src/vs/platform/environment/node/argv.ts | 5 +- .../node/extensionGalleryService.ts | 5 +- .../node/extensionsManifestCache.ts | 2 +- .../test/node/extensionGalleryService.test.ts | 11 +- .../extensions/common/extensionHost.ts | 12 - src/vs/platform/files/common/files.ts | 37 +- .../instantiation/common/instantiation.ts | 2 +- .../common/instantiationService.ts | 5 +- .../issue/electron-main/issueService.ts | 34 +- .../common/abstractKeybindingService.test.ts | 1 + .../test/common/mockKeybindingService.ts | 1 + src/vs/platform/label/common/label.ts | 9 +- .../launch/electron-main/launchService.ts | 2 +- .../lifecycle/common/lifecycleService.ts | 4 +- .../platform/menubar/electron-main/menubar.ts | 11 +- src/vs/platform/progress/common/progress.ts | 2 +- .../browser/remoteAuthorityResolverService.ts | 4 + .../remote/common/remoteAuthorityResolver.ts | 1 + .../remoteAuthorityResolverService.ts | 6 + src/vs/platform/state/common/state.ts | 2 +- src/vs/platform/state/node/stateService.ts | 9 +- src/vs/platform/state/test/node/state.test.ts | 8 +- src/vs/platform/storage/common/storage.ts | 5 +- src/vs/platform/storage/node/storageIpc.ts | 8 +- .../storage/node/storageMainService.ts | 6 +- .../platform/storage/node/storageService.ts | 6 +- .../storage/test/node/storageService.test.ts | 4 +- .../telemetry/node/commonProperties.ts | 5 + .../electron-browser/commonProperties.test.ts | 5 +- src/vs/platform/windows/common/windows.ts | 58 +- .../windows/electron-browser/windowService.ts | 17 +- .../platform/windows/electron-main/windows.ts | 9 +- .../windows/electron-main/windowsService.ts | 6 +- src/vs/platform/windows/node/windowsIpc.ts | 12 +- .../electron-main/workspacesMainService.ts | 10 +- .../platform/workspaces/node/workspacesIpc.ts | 2 +- .../workspacesMainService.test.ts | 4 +- src/vs/vscode.d.ts | 4 +- src/vs/vscode.proposed.d.ts | 9 +- .../api/browser/mainThreadComments.ts | 237 +++--- .../api/browser/mainThreadConsole.ts | 15 +- .../workbench/api/browser/mainThreadKeytar.ts | 61 ++ .../api/browser/mainThreadLanguageFeatures.ts | 64 +- .../api/browser/mainThreadWorkspace.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 50 +- .../api/common/menusExtensionPoint.ts | 1 - .../extensionHost.contribution.ts | 1 + .../api/electron-browser/mainThreadWebview.ts | 33 +- src/vs/workbench/api/node/apiCommands.ts | 7 +- src/vs/workbench/api/node/extHost.api.impl.ts | 109 ++- src/vs/workbench/api/node/extHostCLIServer.ts | 44 +- src/vs/workbench/api/node/extHostComments.ts | 70 +- .../api/node/extHostExtensionService.ts | 19 +- .../api/node/extHostLanguageFeatures.ts | 112 ++- .../api/node/extHostOutputService.ts | 2 +- src/vs/workbench/api/node/extHostSearch.ts | 6 +- src/vs/workbench/api/node/extHostTypes.ts | 36 +- src/vs/workbench/browser/actions.ts | 10 +- .../browser/actions/layoutActions.ts | 2 - .../workbench/browser/actions/listCommands.ts | 10 +- .../browser/actions/workspaceActions.ts | 2 +- .../browser/actions/workspaceCommands.ts | 4 +- src/vs/workbench/browser/composite.ts | 10 +- src/vs/workbench/browser/contextkeys.ts | 10 +- src/vs/workbench/browser/dnd.ts | 28 +- src/vs/workbench/browser/editor.ts | 6 +- src/vs/workbench/browser/labels.ts | 10 +- src/vs/workbench/browser/layout.ts | 4 +- .../browser/nodeless.simpleservices.ts | 9 +- src/vs/workbench/browser/panel.ts | 2 +- .../workbench/browser/parts/compositePart.ts | 8 +- .../browser/parts/editor/baseEditor.ts | 2 +- .../browser/parts/editor/binaryEditor.ts | 8 +- .../browser/parts/editor/editorActions.ts | 18 +- .../browser/parts/editor/editorCommands.ts | 4 +- .../browser/parts/editor/editorControl.ts | 2 +- .../browser/parts/editor/editorDropTarget.ts | 8 +- .../browser/parts/editor/editorGroupView.ts | 55 +- .../browser/parts/editor/editorPart.ts | 2 +- .../browser/parts/editor/editorStatus.ts | 30 +- .../browser/parts/editor/editorWidgets.ts | 7 +- .../browser/parts/editor/sideBySideEditor.ts | 4 +- .../browser/parts/editor/textDiffEditor.ts | 10 +- .../browser/parts/editor/textEditor.ts | 12 +- .../browser/parts/editor/titleControl.ts | 10 +- .../notifications/notificationsCommands.ts | 2 +- .../notifications/notificationsViewer.ts | 4 +- .../browser/parts/statusbar/statusbarPart.ts | 15 +- .../browser/parts/titlebar/menubarControl.ts | 21 +- .../browser/parts/views/panelViewlet.ts | 2 +- src/vs/workbench/browser/quickopen.ts | 4 +- src/vs/workbench/browser/workbench.ts | 2 +- src/vs/workbench/buildfile.js | 4 +- src/vs/workbench/common/composite.ts | 2 +- src/vs/workbench/common/editor.ts | 22 +- .../common/editor/binaryEditorModel.ts | 2 +- .../common/editor/dataUriEditorInput.ts | 4 +- .../common/editor/diffEditorModel.ts | 12 +- src/vs/workbench/common/editor/editorGroup.ts | 21 +- .../common/editor/resourceEditorInput.ts | 4 +- .../common/editor/textDiffEditorModel.ts | 8 +- .../common/editor/untitledEditorInput.ts | 2 +- src/vs/workbench/common/notifications.ts | 2 +- src/vs/workbench/common/theme.ts | 6 + src/vs/workbench/common/viewlet.ts | 1 - .../contrib/backup/common/backupRestorer.ts | 2 +- .../contrib/cli/node/cli.contribution.ts | 6 +- .../comments/browser/commentService.ts | 2 +- .../comments/browser/commentThreadWidget.ts | 121 +-- .../browser/commentsEditorContribution.ts | 12 +- .../contrib/comments/common/commentModel.ts | 6 +- .../contrib/debug/browser/breakpointsView.ts | 1 + .../contrib/debug/browser/debugActionItems.ts | 10 +- .../contrib/debug/browser/debugCommands.ts | 9 +- .../contrib/debug/browser/debugToolBar.ts | 5 +- .../contrib/debug/browser/debugViewlet.ts | 2 +- .../contrib/debug/browser/linkDetector.ts | 2 +- .../debug/browser/loadedScriptsView.ts | 4 +- .../debug/browser/media/debugViewlet.css | 8 +- .../workbench/contrib/debug/common/debug.ts | 1 + .../contrib/debug/common/debugSource.ts | 1 + .../contrib/debug/common/debugViewModel.ts | 2 +- .../debug/electron-browser/debugService.ts | 72 +- .../extensions/browser/extensionsViewer.ts | 166 ++-- .../extensions/common/extensionsInput.ts | 2 +- .../electron-browser/extensionEditor.ts | 7 +- .../extensionsAutoProfiler.ts | 46 +- .../electron-browser/extensionsSlowActions.ts | 185 +++++ .../media/extensionEditor.css | 28 +- .../runtimeExtensionsEditor.ts | 51 +- .../runtimeExtensionsInput.ts | 2 +- .../extensionsTipsService.test.ts | 5 +- .../feedback/electron-browser/feedback.ts | 2 +- .../electron-browser/feedbackStatusbarItem.ts | 6 +- .../browser/editors/fileEditorTracker.ts | 24 +- .../files/browser/editors/textFileEditor.ts | 6 +- .../contrib/files/browser/explorerViewlet.ts | 4 +- .../files/browser/fileActions.contribution.ts | 12 +- .../contrib/files/browser/fileActions.ts | 593 +++++++-------- .../files/browser/files.contribution.ts | 5 - .../workbench/contrib/files/browser/files.ts | 6 +- .../files/browser/views/explorerView.ts | 30 +- .../files/browser/views/explorerViewer.ts | 6 +- .../files/browser/views/openEditorsView.ts | 4 +- .../files/common/editors/fileEditorInput.ts | 2 +- .../contrib/files/common/explorerModel.ts | 4 +- .../workbench/contrib/files/common/files.ts | 50 +- .../format/browser/formatActionsMultiple.ts | 21 +- .../contrib/markers/browser/markersModel.ts | 8 +- .../contrib/markers/browser/markersPanel.ts | 3 +- .../markers/browser/markersTreeViewer.ts | 2 +- .../output/common/outputLinkProvider.ts | 5 +- .../electron-browser/startupProfiler.ts | 6 +- .../electron-browser/startupTimings.ts | 7 +- .../preferences/browser/keybindingWidgets.ts | 3 +- .../preferences/browser/keybindingsEditor.ts | 23 +- .../preferences/browser/preferencesEditor.ts | 6 +- .../common/preferencesContribution.ts | 6 +- .../quickopen/browser/commandsHandler.ts | 8 +- .../relauncher.contribution.ts | 26 +- .../contrib/scm/browser/media/scmViewlet.css | 1 + .../contrib/scm/browser/scmViewlet.ts | 4 +- .../search/browser/search.contribution.ts | 2 +- .../contrib/search/browser/searchView.ts | 4 +- .../snippets/browser/snippetsService.ts | 22 +- .../contrib/stats/node/workspaceStats.ts | 4 +- .../contrib/tasks/common/taskConfiguration.ts | 22 +- .../terminal/browser/terminal.contribution.ts | 5 + .../terminal/browser/terminalInstance.ts | 8 + .../contrib/terminal/common/terminal.ts | 1 + .../electron-browser/releaseNotesEditor.ts | 4 +- .../webview-pre.js => browser/pre/main.js} | 245 +++--- .../webview/browser/webview.contribution.ts | 138 ++++ .../webviewCommands.ts | 2 +- .../webviewEditor.ts | 44 +- .../webviewEditorInput.ts | 28 +- .../webviewEditorInputFactory.ts | 4 +- .../webviewEditorService.ts | 8 +- .../webviewFindWidget.ts | 35 +- .../contrib/webview/common/webview.ts | 88 +++ .../electron-browser/pre/electron-index.js | 43 ++ .../electron-browser/webview.contribution.ts | 133 +--- .../electron-browser/webviewElement.ts | 41 +- .../electron-browser/webviewService.ts | 30 + .../welcome/page/browser/welcomePage.ts | 13 +- .../walkThrough/common/walkThroughInput.ts | 2 +- .../electron-browser/actions/windowActions.ts | 39 +- src/vs/workbench/electron-browser/main.ts | 41 +- src/vs/workbench/electron-browser/window.ts | 8 +- .../services/backup/common/backup.ts | 14 +- .../services/backup/node/backupFileService.ts | 4 +- .../backupFileService.test.ts | 22 +- .../electron-browser/broadcastService.ts | 2 +- .../bulkEdit/browser/bulkEditService.ts | 2 +- .../commands/common/commandService.ts | 25 +- .../test/common/commandService.test.ts | 39 + .../{node => browser}/configuration.ts | 612 ++++++++------- .../{node => browser}/configurationService.ts | 154 +--- .../configuration/common/configuration.ts | 18 + .../configuration/node/configurationCache.ts | 82 ++ .../node/configurationExportHelper.ts | 107 +++ .../node/configurationFileService.ts | 21 + .../configurationEditingService.test.ts | 36 +- .../configurationService.test.ts | 20 +- .../configurationResolverService.test.ts | 45 +- .../electron-browser/contextmenuService.ts | 4 +- .../dialogs/browser/fileDialogService.ts | 54 +- .../dialogs/browser/remoteFileDialog.ts | 51 +- .../dialogs/electron-browser/dialogService.ts | 2 +- .../services/editor/browser/editorService.ts | 4 +- .../services/editor/common/editorService.ts | 1 + .../extensions/common/extensionHostDebug.ts | 30 + .../cachedExtensionScanner.ts | 2 +- .../electron-browser/extensionHost.ts | 66 +- .../extensionHostDebugService.ts | 135 ++++ .../electron-browser/extensionHostProfiler.ts | 2 +- .../node/multiExtensionManagement.ts | 0 .../workbench/services/files/node/encoding.ts | 14 +- .../services/files/node/fileService.ts | 677 +---------------- .../services/files/node/remoteFileService.ts | 272 +------ .../files/node/watcher/nsfw/watcherService.ts | 120 --- .../files/node/watcher/unix/watcherService.ts | 123 --- .../node/watcher/win32/watcherService.ts | 70 -- .../test/electron-browser/fileService.test.ts | 113 +-- .../test/electron-browser/resolver.test.ts | 179 ----- .../files/test/electron-browser/utils.ts | 20 - .../services/files2/common/fileService2.ts | 149 +++- .../files2/common/workspaceWatcher.ts | 160 ++++ .../files2/node/diskFileSystemProvider.ts | 124 ++- .../node/watcher/nodejs/watcherService.ts | 124 +++ .../node/watcher/nsfw/nsfwWatcherService.ts | 54 +- .../nsfw/test/nsfwWatcherService.test.ts | 8 +- .../node/watcher/nsfw}/watcher.ts | 8 +- .../node/watcher/nsfw/watcherApp.ts | 4 +- .../node/watcher/nsfw}/watcherIpc.ts | 4 +- .../node/watcher/nsfw/watcherService.ts | 92 +++ .../watcher/unix/chokidarWatcherService.ts | 46 +- .../unix/test/chockidarWatcherService.test.ts | 64 +- .../node/watcher/unix}/watcher.ts | 8 +- .../node/watcher/unix/watcherApp.ts | 4 +- .../node/watcher/unix}/watcherIpc.ts | 4 +- .../node/watcher/unix/watcherService.ts | 92 +++ .../node/watcher/watcher.ts} | 56 +- .../node/watcher/win32/CodeHelper.exe | Bin .../node/watcher/win32/CodeHelper.md | 0 .../watcher/win32/csharpWatcherService.ts | 15 +- .../node/watcher/win32/watcherService.ts | 69 ++ .../files2/test/browser/fileService2.test.ts | 54 +- .../files2/test/node/diskFileService.test.ts | 245 +++++- .../test/node/normalizer.test.ts} | 41 +- .../services/hash/common/hashService.ts | 32 - .../services/hash/node/hashService.ts | 19 - .../services/hash/test/hashService.test.ts | 20 - .../services/history/browser/history.ts | 14 +- .../keybindingEditing.test.ts | 33 +- .../services/label/common/labelService.ts | 10 +- .../output/common/outputChannelModel.ts | 8 +- .../output/node/outputChannelModelService.ts | 47 +- .../common/preferencesEditorInput.ts | 14 +- .../progress/browser/progressService2.ts | 8 +- .../services/search/common/search.ts | 2 +- .../services/search/common/searchExtTypes.ts | 413 ++++++++++ .../services/search/node/fileSearch.ts | 41 +- .../services/search/node/fileSearchManager.ts | 8 +- .../services/search/node/ripgrepFileSearch.ts | 3 +- .../search/node/ripgrepSearchProvider.ts | 15 +- .../search/node/ripgrepSearchUtils.ts | 51 +- .../search/node/ripgrepTextSearchEngine.ts | 22 +- .../services/search/node/textSearchAdapter.ts | 4 +- .../services/search/node/textSearchManager.ts | 50 +- .../test/node/ripgrepTextSearchEngine.test.ts | 3 +- .../test/node/textSearchManager.test.ts | 9 +- .../textfile/common/textFileEditorModel.ts | 66 +- .../textfile/common/textFileService.ts | 32 +- .../services/textfile/common/textfiles.ts | 2 +- .../services/themes/browser/colorThemeData.ts | 73 +- .../themes/browser/workbenchThemeService.ts | 26 +- .../untitled/common/untitledEditorService.ts | 4 +- .../workspaceEditingService.ts | 71 +- .../api/extHostSearch.test.ts | 30 +- .../api/mainThreadConfiguration.test.ts | 2 +- .../workbench/test/workbenchTestServices.ts | 23 +- src/vs/workbench/workbench.main.ts | 10 +- src/vs/workbench/workbench.nodeless.main.ts | 6 +- test/smoke/README.md | 5 + test/smoke/package.json | 2 +- test/smoke/yarn.lock | 8 +- yarn.lock | 8 +- 433 files changed, 8915 insertions(+), 8361 deletions(-) create mode 100644 extensions/debug-server-ready/.vscode/launch.json create mode 100644 src/vs/base/browser/hash.ts delete mode 100644 src/vs/base/common/uint.ts delete mode 100644 src/vs/base/node/extfs.ts create mode 100644 src/vs/base/node/extpath.ts delete mode 100644 src/vs/base/node/flow.ts create mode 100644 src/vs/base/node/watcher.ts create mode 100644 src/vs/base/test/browser/hash.test.ts delete mode 100644 src/vs/base/test/node/extfs/extfs.test.ts create mode 100644 src/vs/base/test/node/extpath.test.ts delete mode 100644 src/vs/base/test/node/flow.test.ts delete mode 100644 src/vs/base/test/node/pfs.test.ts rename src/vs/base/test/node/{extfs => pfs}/fixtures/examples/company.jxs (100%) rename src/vs/base/test/node/{extfs => pfs}/fixtures/examples/conway.jxs (100%) rename src/vs/base/test/node/{extfs => pfs}/fixtures/examples/employee.jxs (100%) rename src/vs/base/test/node/{extfs => pfs}/fixtures/examples/small.jxs (100%) rename src/vs/base/test/node/{extfs => pfs}/fixtures/index.html (100%) rename src/vs/base/test/node/{extfs => pfs}/fixtures/site.css (93%) create mode 100644 src/vs/base/test/node/pfs/pfs.test.ts create mode 100644 src/vs/editor/contrib/codelens/codeLensCache.ts create mode 100644 src/vs/platform/diagnostics/common/diagnosticsService.ts create mode 100644 src/vs/platform/diagnostics/node/diagnosticsService.ts delete mode 100644 src/vs/platform/extensions/common/extensionHost.ts create mode 100644 src/vs/workbench/api/browser/mainThreadKeytar.ts create mode 100644 src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts rename src/vs/workbench/contrib/webview/{electron-browser/webview-pre.js => browser/pre/main.js} (70%) create mode 100644 src/vs/workbench/contrib/webview/browser/webview.contribution.ts rename src/vs/workbench/contrib/webview/{electron-browser => browser}/webviewCommands.ts (98%) rename src/vs/workbench/contrib/webview/{electron-browser => browser}/webviewEditor.ts (84%) rename src/vs/workbench/contrib/webview/{electron-browser => browser}/webviewEditorInput.ts (91%) rename src/vs/workbench/contrib/webview/{electron-browser => browser}/webviewEditorInputFactory.ts (96%) rename src/vs/workbench/contrib/webview/{electron-browser => browser}/webviewEditorService.ts (96%) rename src/vs/workbench/contrib/webview/{electron-browser => browser}/webviewFindWidget.ts (72%) create mode 100644 src/vs/workbench/contrib/webview/common/webview.ts create mode 100644 src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js create mode 100644 src/vs/workbench/contrib/webview/electron-browser/webviewService.ts rename src/vs/workbench/services/configuration/{node => browser}/configuration.ts (56%) rename src/vs/workbench/services/configuration/{node => browser}/configurationService.ts (86%) create mode 100644 src/vs/workbench/services/configuration/node/configurationCache.ts create mode 100644 src/vs/workbench/services/configuration/node/configurationExportHelper.ts create mode 100644 src/vs/workbench/services/configuration/node/configurationFileService.ts create mode 100644 src/vs/workbench/services/extensions/common/extensionHostDebug.ts create mode 100644 src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts rename src/vs/workbench/services/{extensionManagement => extensions}/node/multiExtensionManagement.ts (100%) delete mode 100644 src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts delete mode 100644 src/vs/workbench/services/files/node/watcher/unix/watcherService.ts delete mode 100644 src/vs/workbench/services/files/node/watcher/win32/watcherService.ts delete mode 100644 src/vs/workbench/services/files/test/electron-browser/resolver.test.ts delete mode 100644 src/vs/workbench/services/files/test/electron-browser/utils.ts create mode 100644 src/vs/workbench/services/files2/common/workspaceWatcher.ts create mode 100644 src/vs/workbench/services/files2/node/watcher/nodejs/watcherService.ts rename src/vs/workbench/services/{files => files2}/node/watcher/nsfw/nsfwWatcherService.ts (82%) rename src/vs/workbench/services/{files => files2}/node/watcher/nsfw/test/nsfwWatcherService.test.ts (88%) rename src/vs/workbench/services/{files/node/watcher/unix => files2/node/watcher/nsfw}/watcher.ts (78%) rename src/vs/workbench/services/{files => files2}/node/watcher/nsfw/watcherApp.ts (69%) rename src/vs/workbench/services/{files/node/watcher/unix => files2/node/watcher/nsfw}/watcherIpc.ts (90%) create mode 100644 src/vs/workbench/services/files2/node/watcher/nsfw/watcherService.ts rename src/vs/workbench/services/{files => files2}/node/watcher/unix/chokidarWatcherService.ts (88%) rename src/vs/workbench/services/{files => files2}/node/watcher/unix/test/chockidarWatcherService.test.ts (85%) rename src/vs/workbench/services/{files/node/watcher/nsfw => files2/node/watcher/unix}/watcher.ts (78%) rename src/vs/workbench/services/{files => files2}/node/watcher/unix/watcherApp.ts (76%) rename src/vs/workbench/services/{files/node/watcher/nsfw => files2/node/watcher/unix}/watcherIpc.ts (90%) create mode 100644 src/vs/workbench/services/files2/node/watcher/unix/watcherService.ts rename src/vs/workbench/services/{files/node/watcher/common.ts => files2/node/watcher/watcher.ts} (65%) rename src/vs/workbench/services/{files => files2}/node/watcher/win32/CodeHelper.exe (100%) rename src/vs/workbench/services/{files => files2}/node/watcher/win32/CodeHelper.md (100%) rename src/vs/workbench/services/{files => files2}/node/watcher/win32/csharpWatcherService.ts (90%) create mode 100644 src/vs/workbench/services/files2/node/watcher/win32/watcherService.ts rename src/vs/workbench/services/{files/test/electron-browser/watcher.test.ts => files2/test/node/normalizer.test.ts} (85%) delete mode 100644 src/vs/workbench/services/hash/common/hashService.ts delete mode 100644 src/vs/workbench/services/hash/node/hashService.ts delete mode 100644 src/vs/workbench/services/hash/test/hashService.test.ts create mode 100644 src/vs/workbench/services/search/common/searchExtTypes.ts rename src/vs/workbench/services/workspace/{node => electron-browser}/workspaceEditingService.ts (91%) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7055bf826f..89aba8e5fe 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -28,6 +28,20 @@ } } }, + { + "type": "npm", + "script": "strict-initialization-watch", + "label": "TS - Strict Initialization", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "problemMatcher": { + "base": "$tsc-watch", + "owner": "typescript-strict-initialization", + "applyTo": "allDocuments" + } + }, { "type": "gulp", "task": "tslint", diff --git a/.yarnrc b/.yarnrc index 692f879945..beab9f70b9 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "3.1.6" +target "3.1.8" runtime "electron" diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index e7d4335c91..e999d640f9 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -84,7 +84,8 @@ const vscodeResources = [ 'out-build/vs/workbench/browser/media/*-theme.css', 'out-build/vs/workbench/contrib/debug/**/*.json', 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', - 'out-build/vs/workbench/contrib/webview/electron-browser/webview-pre.js', + 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', + 'out-build/vs/workbench/contrib/webview/electron-browser/pre/*.js', 'out-build/vs/**/markdown.css', 'out-build/vs/workbench/contrib/tasks/**/*.json', 'out-build/vs/workbench/contrib/welcome/walkThrough/**/*.md', diff --git a/build/package.json b/build/package.json index 641769179d..50b8a85f39 100644 --- a/build/package.json +++ b/build/package.json @@ -43,7 +43,7 @@ "request": "^2.85.0", "tslint": "^5.9.1", "service-downloader": "github:anthonydresser/service-downloader#0.1.5", - "typescript": "3.3.1", + "typescript": "3.4.1", "vsce": "1.48.0", "xml2js": "^0.4.17" }, diff --git a/build/yarn.lock b/build/yarn.lock index 85b5dbdd7f..c1f72ff764 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -3201,10 +3201,10 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" -typescript@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.1.tgz#6de14e1db4b8a006ac535e482c8ba018c55f750b" - integrity sha512-cTmIDFW7O0IHbn1DPYjkiebHxwtCMU+eTy30ZtJNBPF9j2O1ITu5XH2YnBeVRKWHqF+3JQwWJv0Q0aUgX8W7IA== +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== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" diff --git a/cgmanifest.json b/cgmanifest.json index afaebd6177..927c66cbc3 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -73,12 +73,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "73158a6419a3e2da9e4d523e1131052abd28fbbb" + "commitHash": "e84a6860e35e14b4031b88bb9b49841cdb89a305" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "3.1.6" + "version": "3.1.8" }, { "component": { diff --git a/extensions/debug-server-ready/.vscode/launch.json b/extensions/debug-server-ready/.vscode/launch.json new file mode 100644 index 0000000000..00d60a1219 --- /dev/null +++ b/extensions/debug-server-ready/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Server Ready Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 5ebf8928cd..e034c98593 100755 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1445,6 +1445,7 @@ export class CommandCenter { const quickpick = window.createQuickPick(); quickpick.items = picks; quickpick.placeholder = placeHolder; + quickpick.ignoreFocusOut = true; quickpick.show(); const choice = await new Promise(c => quickpick.onDidAccept(() => c(quickpick.activeItems[0]))); diff --git a/extensions/markdown-language-features/src/commands/refreshPreview.ts b/extensions/markdown-language-features/src/commands/refreshPreview.ts index 338f24c3d9..4e683bd3f2 100644 --- a/extensions/markdown-language-features/src/commands/refreshPreview.ts +++ b/extensions/markdown-language-features/src/commands/refreshPreview.ts @@ -5,15 +5,18 @@ import { Command } from '../commandManager'; import { MarkdownPreviewManager } from '../features/previewManager'; +import { MarkdownEngine } from '../markdownEngine'; export class RefreshPreviewCommand implements Command { public readonly id = 'markdown.preview.refresh'; public constructor( - private readonly webviewManager: MarkdownPreviewManager + private readonly webviewManager: MarkdownPreviewManager, + private readonly engine: MarkdownEngine ) { } public execute() { + this.engine.cleanCache(); this.webviewManager.refresh(); } } \ No newline at end of file diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 104300acf3..03fe82929f 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -78,7 +78,7 @@ function registerMarkdownCommands( commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter)); commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager, telemetryReporter)); commandManager.register(new commands.ShowSourceCommand(previewManager)); - commandManager.register(new commands.RefreshPreviewCommand(previewManager)); + commandManager.register(new commands.RefreshPreviewCommand(previewManager, engine)); commandManager.register(new commands.MoveCursorToPositionCommand()); commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager)); commandManager.register(new commands.OpenDocumentLinkCommand(engine)); diff --git a/extensions/markdown-language-features/src/features/documentLinkProvider.ts b/extensions/markdown-language-features/src/features/documentLinkProvider.ts index bec722c99e..a41e4adc7f 100644 --- a/extensions/markdown-language-features/src/features/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/features/documentLinkProvider.ts @@ -81,8 +81,10 @@ export default class LinkProvider implements vscode.DocumentLinkProvider { const base = document.uri.scheme === 'file' ? path.dirname(document.uri.fsPath) : ''; const text = document.getText(); - return this.providerInlineLinks(text, document, base) - .concat(this.provideReferenceLinks(text, document, base)); + return [ + ...this.providerInlineLinks(text, document, base), + ...this.provideReferenceLinks(text, document, base) + ]; } private providerInlineLinks( diff --git a/extensions/markdown-language-features/src/features/foldingProvider.ts b/extensions/markdown-language-features/src/features/foldingProvider.ts index 426cba30d5..ba3e074717 100644 --- a/extensions/markdown-language-features/src/features/foldingProvider.ts +++ b/extensions/markdown-language-features/src/features/foldingProvider.ts @@ -7,24 +7,36 @@ import { Token } from 'markdown-it'; import * as vscode from 'vscode'; import { MarkdownEngine } from '../markdownEngine'; import { TableOfContentsProvider } from '../tableOfContentsProvider'; +import { flatten } from '../util/arrays'; const rangeLimit = 5000; +const isStartRegion = (t: string) => /^\s*/.test(t); +const isEndRegion = (t: string) => /^\s*/.test(t); + +const isRegionMarker = (token: Token) => + token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content)); + export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvider { constructor( private readonly engine: MarkdownEngine ) { } + public async provideFoldingRanges( + document: vscode.TextDocument, + _: vscode.FoldingContext, + _token: vscode.CancellationToken + ): Promise { + const foldables = await Promise.all([ + this.getRegions(document), + this.getHeaderFoldingRanges(document), + this.getBlockFoldingRanges(document) + ]); + return flatten(foldables).slice(0, rangeLimit); + } + private async getRegions(document: vscode.TextDocument): Promise { - - const isStartRegion = (t: string) => /^\s*/.test(t); - const isEndRegion = (t: string) => /^\s*/.test(t); - - const isRegionMarker = (token: Token) => token.type === 'html_block' && - (isStartRegion(token.content) || isEndRegion(token.content)); - - const tokens = await this.engine.parse(document); const regionMarkers = tokens.filter(isRegionMarker) .map(token => ({ line: token.map[0], isStart: isStartRegion(token.content) })); @@ -44,18 +56,6 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi .filter((region: vscode.FoldingRange | null): region is vscode.FoldingRange => !!region); } - public async provideFoldingRanges( - document: vscode.TextDocument, - _: vscode.FoldingContext, - _token: vscode.CancellationToken - ): Promise { - const foldables = await Promise.all([ - this.getRegions(document), - this.getHeaderFoldingRanges(document), - this.getBlockFoldingRanges(document)]); - return ([] as vscode.FoldingRange[]).concat.apply([], foldables).slice(0, rangeLimit); - } - private async getHeaderFoldingRanges(document: vscode.TextDocument) { const tocProvider = new TableOfContentsProvider(this.engine, document); const toc = await tocProvider.getToc(); @@ -70,7 +70,7 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi private async getBlockFoldingRanges(document: vscode.TextDocument): Promise { - const isFoldableToken = (token: Token) => { + const isFoldableToken = (token: Token): boolean => { switch (token.type) { case 'fence': case 'list_item_open': diff --git a/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts b/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts index 81258b5a6d..459eb3f7fd 100644 --- a/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts +++ b/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts @@ -9,6 +9,7 @@ import { isMarkdownFile } from '../util/file'; import { Lazy, lazy } from '../util/lazy'; import MDDocumentSymbolProvider from './documentSymbolProvider'; import { SkinnyTextDocument } from '../tableOfContentsProvider'; +import { flatten } from '../util/arrays'; export interface WorkspaceMarkdownDocumentProvider { getAllMarkdownDocuments(): Thenable>; @@ -108,7 +109,7 @@ export default class MarkdownWorkspaceSymbolProvider extends Disposable implemen } const allSymbolsSets = await Promise.all(Array.from(this._symbolCache.values()).map(x => x.value)); - const allSymbols: vscode.SymbolInformation[] = Array.prototype.concat.apply([], allSymbolsSets); + const allSymbols = flatten(allSymbolsSets); return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1); } diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index aafabfa4cf..9b6c292728 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -47,6 +47,11 @@ class TokenCache { }; this.tokens = tokens; } + + public clean(): void { + this.cachedDocument = undefined; + this.tokens = undefined; + } } export class MarkdownEngine { @@ -154,6 +159,10 @@ export class MarkdownEngine { return this.tokenize(document, config, engine); } + public cleanCache(): void { + this._tokenCache.clean(); + } + private getConfig(resource: vscode.Uri): MarkdownItConfig { const config = vscode.workspace.getConfiguration('markdown', resource); return { diff --git a/extensions/markdown-language-features/src/typings/ref.d.ts b/extensions/markdown-language-features/src/typings/ref.d.ts index acf3ed8429..37d9f00e11 100644 --- a/extensions/markdown-language-features/src/typings/ref.d.ts +++ b/extensions/markdown-language-features/src/typings/ref.d.ts @@ -4,4 +4,5 @@ *--------------------------------------------------------------------------------------------*/ /// +/// /// diff --git a/extensions/markdown-language-features/src/util/arrays.ts b/extensions/markdown-language-features/src/util/arrays.ts index bf5524c901..7b19733ba9 100644 --- a/extensions/markdown-language-features/src/util/arrays.ts +++ b/extensions/markdown-language-features/src/util/arrays.ts @@ -16,3 +16,7 @@ export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEq return true; } + +export function flatten(arr: ReadonlyArray[]): T[] { + return ([] as T[]).concat.apply([], arr); +} \ No newline at end of file diff --git a/extensions/markdown-language-features/src/util/links.ts b/extensions/markdown-language-features/src/util/links.ts index bc45b9dbc2..d3d1198a38 100644 --- a/extensions/markdown-language-features/src/util/links.ts +++ b/extensions/markdown-language-features/src/util/links.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; -const knownSchemes = ['http:', 'https:', 'file:', 'mailto:', 'data:', 'vscode-resource:']; +const knownSchemes = ['http:', 'https:', 'file:', 'mailto:', 'data:', `${vscode.env.uriScheme}:`, 'vscode:', 'vscode-insiders:', 'vscode-resource:']; export function getUriForLinkWithKnownExternalScheme( link: string, @@ -15,4 +15,4 @@ export function getUriForLinkWithKnownExternalScheme( } return undefined; -} \ No newline at end of file +} diff --git a/package.json b/package.json index 7a359c2fd5..98df3f5f77 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "monaco-compile-check": "tsc -p src/tsconfig.monaco.json --noEmit", "tslint": "node node_modules/tslint/bin/tslint -c tslint-gci.json -p src/tsconfig.json", "strict-null-check": "tsc -p src/tsconfig.strictNullChecks.json", - "strict-null-check-watch": "tsc -p src/tsconfig.strictNullChecks.json --watch" + "strict-null-check-watch": "tsc -p src/tsconfig.strictNullChecks.json --watch", + "strict-initialization-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictPropertyInitialization" }, "dependencies": { "@angular/animations": "~4.1.3", @@ -155,7 +156,7 @@ "tslint": "^5.11.0", "tslint-microsoft-contrib": "^6.0.0", "typemoq": "^0.3.2", - "typescript": "3.3.1", + "typescript": "3.4.1", "typescript-formatter": "7.1.0", "typescript-tslint-plugin": "^0.0.7", "uglify-es": "^3.0.18", diff --git a/src/sql/base/browser/ui/editableDropdown/dropdown.ts b/src/sql/base/browser/ui/editableDropdown/dropdown.ts index 711ccc4dc4..51db880375 100644 --- a/src/sql/base/browser/ui/editableDropdown/dropdown.ts +++ b/src/sql/base/browser/ui/editableDropdown/dropdown.ts @@ -233,7 +233,7 @@ export class Dropdown extends Disposable { this._layoutTree(); return { dispose: () => { } }; }, - onDOMEvent: e => { + onDOMEvent: (e: any) => { if (!DOM.isAncestor(e.srcElement, this.$el.getHTMLElement()) && !DOM.isAncestor(e.srcElement, this.$treeContainer.getHTMLElement())) { this._input.validate(); this._onBlur.fire(); diff --git a/src/sql/parts/modelComponents/webview.component.ts b/src/sql/parts/modelComponents/webview.component.ts index 5013c54b31..dffcc815a0 100644 --- a/src/sql/parts/modelComponents/webview.component.ts +++ b/src/sql/parts/modelComponents/webview.component.ts @@ -23,8 +23,9 @@ import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { WebviewElement, WebviewOptions, WebviewContentOptions } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; +import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; +import { WebviewContentOptions } from 'vs/workbench/contrib/webview/common/webview'; function reviveWebviewOptions(options: vscode.WebviewOptions): vscode.WebviewOptions { return { diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index 67448561ae..74dc0a0427 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -164,7 +164,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe //Saves scrollTop value on scroll change public scrollHandler(event: Event){ - this._scrollTop = event.srcElement.scrollTop; + this._scrollTop = (event.srcElement).scrollTop; } public unselectActiveCell() { diff --git a/src/sql/parts/objectExplorer/viewlet/treeUpdateUtils.ts b/src/sql/parts/objectExplorer/viewlet/treeUpdateUtils.ts index 279bed9aa8..c005883508 100644 --- a/src/sql/parts/objectExplorer/viewlet/treeUpdateUtils.ts +++ b/src/sql/parts/objectExplorer/viewlet/treeUpdateUtils.ts @@ -14,6 +14,34 @@ import { TreeNode } from 'sql/parts/objectExplorer/common/treeNode'; import errors = require('vs/base/common/errors'); import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; + +export interface IExpandableTree extends ITree { + // {{SQL CARBON EDIT }} - add back deleted VS Code tree methods + /** + * Returns a list of the currently expanded elements. + */ + getExpandedElements(): any[]; + + /** + * Returns a number between 0 and 1 representing how much the tree is scroll down. 0 means all the way + * to the top; 1 means all the way down. + */ + getScrollPosition(): number; + + /** + * Sets the scroll position with a number between 0 and 1 representing how much the tree is scroll down. 0 means all the way + * to the top; 1 means all the way down. + */ + setScrollPosition(pos: number): void; + + /** + * Returns the total height of the tree's content. + */ + getContentHeight(): number; + // {{SQL CARBON EDIT }} - end block +} + + export class TreeUpdateUtils { public static isInDragAndDrop: boolean = false; @@ -22,6 +50,9 @@ export class TreeUpdateUtils { * Set input for the tree. */ public static structuralTreeUpdate(tree: ITree, viewKey: string, connectionManagementService: IConnectionManagementService, providers?: string[]): Promise { + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = tree; + let selectedElement: any; let targetsToExpand: any[]; if (tree) { @@ -29,7 +60,7 @@ export class TreeUpdateUtils { if (selection && selection.length === 1) { selectedElement = selection[0]; } - targetsToExpand = tree.getExpandedElements(); + targetsToExpand = expandableTree.getExpandedElements(); } let groups; let treeInput = new ConnectionProfileGroup('root', null, undefined, undefined, undefined); @@ -59,6 +90,9 @@ export class TreeUpdateUtils { * Set input for the registered servers tree. */ public static registeredServerUpdate(tree: ITree, connectionManagementService: IConnectionManagementService, elementToSelect?: any): Promise { + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = tree; + let selectedElement: any = elementToSelect; let targetsToExpand: any[]; @@ -72,7 +106,7 @@ export class TreeUpdateUtils { selectedElement = selection[0]; } } - targetsToExpand = tree.getExpandedElements(); + targetsToExpand = expandableTree.getExpandedElements(); if (selectedElement && targetsToExpand.indexOf(selectedElement) === -1) { targetsToExpand.push(selectedElement); } diff --git a/src/sql/parts/query/editor/messagePanel.ts b/src/sql/parts/query/editor/messagePanel.ts index 6e2017431a..a31deba60c 100644 --- a/src/sql/parts/query/editor/messagePanel.ts +++ b/src/sql/parts/query/editor/messagePanel.ts @@ -30,6 +30,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IExpandableTree } from 'sql/parts/objectExplorer/viewlet/treeUpdateUtils'; export interface IResultMessageIntern extends IResultMessage { id?: string; @@ -109,8 +110,11 @@ export class MessagePanel extends ViewletPanel { }, { keyboardSupport: false, horizontalScrollMode: ScrollbarVisibility.Auto }); this.disposables.push(this.tree); this.tree.onDidScroll(e => { + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = this.tree; + if (this.state) { - this.state.scrollPosition = this.tree.getScrollPosition(); + this.state.scrollPosition = expandableTree.getScrollPosition(); } }); this.onDidChange(e => { @@ -178,13 +182,16 @@ export class MessagePanel extends ViewletPanel { } protected layoutBody(size: number): void { - const previousScrollPosition = this.tree.getScrollPosition(); + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = this.tree; + + const previousScrollPosition = expandableTree.getScrollPosition(); this.tree.layout(size); if (this.state && this.state.scrollPosition) { - this.tree.setScrollPosition(this.state.scrollPosition); + expandableTree.setScrollPosition(this.state.scrollPosition); } else { if (previousScrollPosition === 1) { - this.tree.setScrollPosition(1); + expandableTree.setScrollPosition(1); } } } @@ -214,17 +221,19 @@ export class MessagePanel extends ViewletPanel { if (hasError) { this.setExpanded(true); } + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = this.tree; if (this.state.scrollPosition) { this.tree.refresh(this.model).then(() => { // Restore the previous scroll position when switching between tabs - this.tree.setScrollPosition(this.state.scrollPosition); + expandableTree.setScrollPosition(this.state.scrollPosition); }); } else { - const previousScrollPosition = this.tree.getScrollPosition(); + const previousScrollPosition = expandableTree.getScrollPosition(); this.tree.refresh(this.model).then(() => { // Scroll to the end if the user was already at the end otherwise leave the current scroll position if (previousScrollPosition === 1) { - this.tree.setScrollPosition(1); + expandableTree.setScrollPosition(1); } }); } @@ -244,8 +253,10 @@ export class MessagePanel extends ViewletPanel { public set state(val: MessagePanelState) { this._state = val; + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = this.tree; if (this.state.scrollPosition) { - this.tree.setScrollPosition(this.state.scrollPosition); + expandableTree.setScrollPosition(this.state.scrollPosition); } this.setExpanded(!this.state.collapsed); } diff --git a/src/sql/parts/taskHistory/viewlet/taskHistoryView.ts b/src/sql/parts/taskHistory/viewlet/taskHistoryView.ts index c9422090d0..c5b598b8c6 100644 --- a/src/sql/parts/taskHistory/viewlet/taskHistoryView.ts +++ b/src/sql/parts/taskHistory/viewlet/taskHistoryView.ts @@ -14,6 +14,7 @@ import { ITree } from 'vs/base/parts/tree/browser/tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { DefaultFilter, DefaultDragAndDrop, DefaultAccessibilityProvider } from 'vs/base/parts/tree/browser/treeDefaults'; import { localize } from 'vs/nls'; +import { hide, $, append } from 'vs/base/browser/dom'; import { TaskHistoryRenderer } from 'sql/parts/taskHistory/viewlet/taskHistoryRenderer'; import { TaskHistoryDataSource } from 'sql/parts/taskHistory/viewlet/taskHistoryDataSource'; @@ -22,7 +23,7 @@ import { TaskHistoryActionProvider } from 'sql/parts/taskHistory/viewlet/taskHis import { ITaskService } from 'sql/platform/taskHistory/common/taskService'; import { TaskNode, TaskStatus } from 'sql/parts/taskHistory/common/taskNode'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; -import { hide, $, append } from 'vs/base/browser/dom'; +import { IExpandableTree } from 'sql/parts/objectExplorer/viewlet/treeUpdateUtils'; /** * TaskHistoryView implements the dynamic tree view. @@ -112,7 +113,9 @@ export class TaskHistoryView { if (selection && selection.length === 1) { selectedElement = selection[0]; } - targetsToExpand = this._tree.getExpandedElements(); + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = this._tree; + targetsToExpand = expandableTree.getExpandedElements(); } //Get the tree Input diff --git a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts index fdf32c5252..6dc85ba90a 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts @@ -12,7 +12,7 @@ import { Modal } from 'sql/workbench/browser/modal/modal'; import { IConnectionManagementService, INewConnectionParams } from 'sql/platform/connection/common/connectionManagement'; import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper'; import { TreeCreationUtils } from 'sql/parts/objectExplorer/viewlet/treeCreationUtils'; -import { TreeUpdateUtils } from 'sql/parts/objectExplorer/viewlet/treeUpdateUtils'; +import { TreeUpdateUtils, IExpandableTree } from 'sql/parts/objectExplorer/viewlet/treeUpdateUtils'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { TabbedPanel, PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel'; import { RecentConnectionTreeController, RecentConnectionActionsProvider } from 'sql/parts/connection/connectionDialog/recentConnectionTreeController'; @@ -178,11 +178,14 @@ export class ConnectionDialogWidget extends Modal { }); this._panel.onTabChange(async c => { - if (c === savedConnectionTabId && this._savedConnectionTree.getContentHeight() === 0) { + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = this._savedConnectionTree; + + if (c === savedConnectionTabId && expandableTree.getContentHeight() === 0) { // Update saved connection tree await TreeUpdateUtils.structuralTreeUpdate(this._savedConnectionTree, 'saved', this._connectionManagementService, this._providers); - if (this._savedConnectionTree.getContentHeight() > 0) { + if (expandableTree.getContentHeight() > 0) { this._noSavedConnectionBuilder.hide(); this._savedConnectionBuilder.show(); } else { diff --git a/src/sql/workbench/services/fileBrowser/browser/fileBrowserTreeView.ts b/src/sql/workbench/services/fileBrowser/browser/fileBrowserTreeView.ts index e212b4cdbb..2e6f17a744 100644 --- a/src/sql/workbench/services/fileBrowser/browser/fileBrowserTreeView.ts +++ b/src/sql/workbench/services/fileBrowser/browser/fileBrowserTreeView.ts @@ -18,6 +18,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITree } from 'vs/base/parts/tree/browser/tree'; +import { IExpandableTree } from 'sql/parts/objectExplorer/viewlet/treeUpdateUtils'; /** * Implements tree view for file browser @@ -96,7 +97,9 @@ export class FileBrowserTreeView implements IDisposable { if (selection && selection.length === 1) { selectedElement = selection[0]; } - targetsToExpand = this._tree.getExpandedElements(); + // convert to old VS Code tree interface with expandable methods + let expandableTree: IExpandableTree = this._tree; + targetsToExpand = expandableTree.getExpandedElements(); } if (rootNode) { diff --git a/src/sqltest/parts/insights/insightsUtils.test.ts b/src/sqltest/parts/insights/insightsUtils.test.ts index 8965156dc3..6260c2f811 100644 --- a/src/sqltest/parts/insights/insightsUtils.test.ts +++ b/src/sqltest/parts/insights/insightsUtils.test.ts @@ -171,8 +171,8 @@ suite('Insights Utils tests', function () { }); - suiteTeardown(done => { + suiteTeardown(() => { // Clean up our test files - pfs.del(testRootPath).then(done()); + return pfs.rimraf(testRootPath); }); }); diff --git a/src/sqltest/stubs/contextKeyServiceStub.ts b/src/sqltest/stubs/contextKeyServiceStub.ts index b221d1dd84..b4bc132950 100644 --- a/src/sqltest/stubs/contextKeyServiceStub.ts +++ b/src/sqltest/stubs/contextKeyServiceStub.ts @@ -36,4 +36,7 @@ export class ContextKeyServiceStub implements IContextKeyService { return undefined; } + bufferChangeEvents(callback: Function): void { + } + } \ No newline at end of file diff --git a/src/typings/electron.d.ts b/src/typings/electron.d.ts index f680249a66..20e58bcd73 100644 --- a/src/typings/electron.d.ts +++ b/src/typings/electron.d.ts @@ -1,4 +1,4 @@ -// Type definitions for Electron 3.1.6 +// Type definitions for Electron 3.1.8 // Project: http://electronjs.org/ // Definitions by: The Electron Team // Definitions: https://github.com/electron/electron-typescript-definitions @@ -86,7 +86,7 @@ declare namespace Electron { webviewTag: WebviewTag; } - interface AllElectron extends MainInterface, RendererInterface { } + interface AllElectron extends MainInterface, RendererInterface {} const app: App; const autoUpdater: AutoUpdater; diff --git a/src/typings/yauzl.d.ts b/src/typings/yauzl.d.ts index 3a26286d60..93163a2f21 100644 --- a/src/typings/yauzl.d.ts +++ b/src/typings/yauzl.d.ts @@ -40,9 +40,10 @@ declare module 'yauzl' { } export interface IOptions { - autoClose: boolean; + autoClose?: boolean; + lazyEntries?: boolean; } export function open(path: string, callback: (err?: Error, zipfile?: ZipFile) => void): void; - export function open(path: string, options: IOptions, callback: (err?: Error, zipfile?: ZipFile) => void): void; + export function open(path: string, options: IOptions | undefined, callback: (err?: Error, zipfile?: ZipFile) => void): void; } \ No newline at end of file diff --git a/src/vs/base/browser/dnd.ts b/src/vs/base/browser/dnd.ts index c6c9d68769..2485a9c1e7 100644 --- a/src/vs/base/browser/dnd.ts +++ b/src/vs/base/browser/dnd.ts @@ -29,7 +29,7 @@ export class DelayedDragHandler extends Disposable { })); ['dragleave', 'drop', 'dragend'].forEach(type => { - this._register(addDisposableListener(container, type as 'dragleave' | 'drop' | 'dragend', () => { + this._register(addDisposableListener(container, type, () => { this.clearDragTimeout(); })); }); diff --git a/src/vs/base/browser/hash.ts b/src/vs/base/browser/hash.ts new file mode 100644 index 0000000000..eee8c0f56e --- /dev/null +++ b/src/vs/base/browser/hash.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function createSHA1(content: string): Thenable { + if (typeof require !== 'undefined') { + const _crypto: typeof crypto = require.__$__nodeRequire('crypto'); + return Promise.resolve(_crypto['createHash']('sha1').update(content).digest('hex')); + } + return crypto.subtle.digest('SHA-1', new TextEncoder().encode(content)).then(buffer => { + // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#Converting_a_digest_to_a_hex_string + return Array.prototype.map.call(new Uint8Array(buffer), (value: number) => `00${value.toString(16)}`.slice(-2)).join(''); + }); +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 947e04dc72..ca685b7bc8 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -678,7 +678,7 @@ export class ActionBar extends Disposable implements IActionRunner { focus(index?: number): void; focus(selectFirst?: boolean): void; - focus(arg?: any): void { + focus(arg?: number | boolean): void { let selectFirst: boolean = false; let index: number | undefined = undefined; if (arg === undefined) { diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts index db789dd53d..3dabdfb972 100644 --- a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -334,7 +334,7 @@ export class BreadcrumbsWidget { private _onClick(event: IMouseEvent): void { for (let el: HTMLElement | null = event.target; el; el = el.parentElement) { - let idx = this._nodes.indexOf(el as any); + let idx = this._nodes.indexOf(el as HTMLDivElement); if (idx >= 0) { this._focus(idx, event); this._select(idx, event); diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 2acd6ffe02..9de98f977e 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -40,7 +40,7 @@ export class Button extends Disposable { private buttonForeground: Color | undefined; private buttonBorder: Color | undefined; - private _onDidClick = this._register(new Emitter()); + private _onDidClick = this._register(new Emitter()); get onDidClick(): BaseEvent { return this._onDidClick.event; } private focusTracker: DOM.IFocusTracker; diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 8d460ca778..e5889ec04c 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -120,7 +120,7 @@ export class ContextView extends Disposable { setContainer(container: HTMLElement | null): void { if (this.container) { - this.toDisposeOnSetContainer = dispose(this.toDisposeOnSetContainer); + dispose(this.toDisposeOnSetContainer); this.container.removeChild(this.view); this.container = null; } diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 241012a826..8f5d835974 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -19,7 +19,9 @@ .monaco-workbench .dialog-box { display: flex; flex-direction: column-reverse; + width: min-content; min-width: 500px; + max-width: 90%; min-height: 75px; padding: 5px; } @@ -134,12 +136,13 @@ overflow: hidden; /* buttons row should never overflow */ } -.monaco-workbench .monaco-workbench .dialog-box > .dialog-buttons-row { +.monaco-workbench .dialog-box > .dialog-buttons-row { display: flex; + white-space: nowrap; } /** Dialog: Buttons */ -.monaco-workbench .monaco-workbench .dialog-box > .dialog-buttons-row > .dialog-buttons { +.monaco-workbench .dialog-box > .dialog-buttons-row > .dialog-buttons { display: flex; overflow: hidden; } diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 64e9625dfa..68cbae533b 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -272,12 +272,12 @@ export class DropdownMenuActionItem extends BaseActionItem { private contextMenuProvider: IContextMenuProvider; private actionItemProvider?: IActionItemProvider; private keybindings?: (action: IAction) => ResolvedKeybinding | undefined; - private clazz: string; + private clazz: string | undefined; private anchorAlignmentProvider: (() => AnchorAlignment) | undefined; - constructor(action: IAction, menuActions: IAction[], contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding | undefined) | undefined, clazz: string, anchorAlignmentProvider?: () => AnchorAlignment); - constructor(action: IAction, actionProvider: IActionProvider, contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding) | undefined, clazz: string, anchorAlignmentProvider?: () => AnchorAlignment); - constructor(action: IAction, menuActionsOrProvider: any, contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding | undefined) | undefined, clazz: string, anchorAlignmentProvider?: () => AnchorAlignment) { + constructor(action: IAction, menuActions: IAction[], contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding | undefined) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment); + constructor(action: IAction, actionProvider: IActionProvider, contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment); + constructor(action: IAction, menuActionsOrProvider: any, contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider | undefined, actionRunner: IActionRunner, keybindings: ((action: IAction) => ResolvedKeybinding | undefined) | undefined, clazz: string | undefined, anchorAlignmentProvider?: () => AnchorAlignment) { super(null, action); this.menuActionsOrProvider = menuActionsOrProvider; @@ -292,7 +292,9 @@ export class DropdownMenuActionItem extends BaseActionItem { render(container: HTMLElement): void { const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { this.element = append(el, $('a.action-label.icon')); - addClasses(this.element, this.clazz); + if (this.clazz) { + addClasses(this.element, this.clazz); + } this.element.tabIndex = 0; this.element.setAttribute('role', 'button'); diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index e77aa196cd..4e2d1a196e 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -457,7 +457,7 @@ export class SerializableGrid extends Grid { return { children, box }; } else if (json.type === 'leaf') { - const view = deserializer.fromJSON(json.data) as T; + const view: T = deserializer.fromJSON(json.data); return { view, box }; } @@ -481,9 +481,9 @@ export class SerializableGrid extends Grid { throw new Error('Invalid JSON: \'height\' property must be a number.'); } - const orientation = json.orientation as Orientation; - const width = json.width as number; - const height = json.height as number; + const orientation = json.orientation; + const width = json.width; + const height = json.height; const box: Box = { top: 0, left: 0, width, height }; const root = SerializableGrid.deserializeNode(json.root, orientation, box, deserializer) as GridBranchNode; diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index 1df8594e5b..950001d013 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from 'vs/base/browser/dom'; import * as objects from 'vs/base/common/objects'; import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { escape } from 'vs/base/common/strings'; @@ -54,10 +53,9 @@ export class HighlightedLabel { this.render(); } - private render() { - dom.clearNode(this.domNode); + private render(): void { - let htmlContent: string[] = []; + let htmlContent = ''; let pos = 0; for (const highlight of this.highlights) { @@ -65,27 +63,27 @@ export class HighlightedLabel { continue; } if (pos < highlight.start) { - htmlContent.push(''); + htmlContent += ''; const substring = this.text.substring(pos, highlight.start); - htmlContent.push(this.supportOcticons ? renderOcticons(substring) : escape(substring)); - htmlContent.push(''); + htmlContent += this.supportOcticons ? renderOcticons(substring) : escape(substring); + htmlContent += ''; pos = highlight.end; } - htmlContent.push(''); + htmlContent += ''; const substring = this.text.substring(highlight.start, highlight.end); - htmlContent.push(this.supportOcticons ? renderOcticons(substring) : escape(substring)); - htmlContent.push(''); + htmlContent += this.supportOcticons ? renderOcticons(substring) : escape(substring); + htmlContent += ''; pos = highlight.end; } if (pos < this.text.length) { - htmlContent.push(''); + htmlContent += ''; const substring = this.text.substring(pos); - htmlContent.push(this.supportOcticons ? renderOcticons(substring) : escape(substring)); - htmlContent.push(''); + htmlContent += this.supportOcticons ? renderOcticons(substring) : escape(substring); + htmlContent += ''; } - this.domNode.innerHTML = htmlContent.join(''); + this.domNode.innerHTML = htmlContent; this.domNode.title = this.title; this.didEverRender = true; } diff --git a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts index 4bb7504483..5a3e1f4d52 100644 --- a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts +++ b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.ts @@ -33,7 +33,7 @@ export interface KeybindingLabelOptions { export class KeybindingLabel { private domNode: HTMLElement; - private keybinding: ResolvedKeybinding | null | undefined; + private keybinding: ResolvedKeybinding | undefined; private matches: Matches | undefined; private didEverRender: boolean; @@ -47,7 +47,7 @@ export class KeybindingLabel { return this.domNode; } - set(keybinding: ResolvedKeybinding | null | undefined, matches?: Matches) { + set(keybinding: ResolvedKeybinding | undefined, matches?: Matches) { if (this.didEverRender && this.keybinding === keybinding && KeybindingLabel.areSame(this.matches, matches)) { return; } diff --git a/src/vs/base/browser/ui/list/rangeMap.ts b/src/vs/base/browser/ui/list/rangeMap.ts index 8ff677be49..11bb1f6aa2 100644 --- a/src/vs/base/browser/ui/list/rangeMap.ts +++ b/src/vs/base/browser/ui/list/rangeMap.ts @@ -84,7 +84,7 @@ export function consolidate(groups: IRangedGroup[]): IRangedGroup[] { * collection. */ function concat(...groups: IRangedGroup[][]): IRangedGroup[] { - return consolidate(groups.reduce((r, g) => r.concat(g), [] as IRangedGroup[])); + return consolidate(groups.reduce((r, g) => r.concat(g), [])); } export class RangeMap { diff --git a/src/vs/base/browser/ui/progressbar/progressbar.ts b/src/vs/base/browser/ui/progressbar/progressbar.ts index 51d5de84d1..d394e08ef7 100644 --- a/src/vs/base/browser/ui/progressbar/progressbar.ts +++ b/src/vs/base/browser/ui/progressbar/progressbar.ts @@ -10,6 +10,7 @@ import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; import { removeClasses, addClass, hasClass, addClasses, removeClass, hide, show } from 'vs/base/browser/dom'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { isNumber } from 'vs/base/common/types'; const css_done = 'done'; const css_active = 'active'; @@ -146,7 +147,7 @@ export class ProgressBar extends Disposable { * Finds out if this progress bar is configured with total work */ hasTotal(): boolean { - return !isNaN(this.totalWork as number); + return isNumber(this.totalWork); } /** @@ -172,10 +173,11 @@ export class ProgressBar extends Disposable { } private doSetWorked(value: number): ProgressBar { - assert.ok(!isNaN(this.totalWork as number), 'Total work not set'); + assert.ok(isNumber(this.totalWork), 'Total work not set'); + const totalWork = this.totalWork!; this.workedVal = value; - this.workedVal = Math.min(this.totalWork as number, this.workedVal); + this.workedVal = Math.min(totalWork, this.workedVal); if (hasClass(this.element, css_infinite)) { removeClass(this.element, css_infinite); @@ -193,7 +195,7 @@ export class ProgressBar extends Disposable { addClass(this.element, css_discrete); } - this.bit.style.width = 100 * (this.workedVal / (this.totalWork as number)) + '%'; + this.bit.style.width = 100 * (this.workedVal / (totalWork)) + '%'; return this; } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index ba898d1458..eed5499d2c 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -818,18 +818,32 @@ class Trait { return; } - const identityProvider = this.identityProvider; - const nodesByIdentity = new Map>(); - this.nodes.forEach(node => nodesByIdentity.set(identityProvider.getId(node.element).toString(), node)); + const deletedNodesIdSet = new Set(); + const deletedNodesVisitor = (node: ITreeNode) => deletedNodesIdSet.add(this.identityProvider!.getId(node.element).toString()); + deletedNodes.forEach(node => dfs(node, deletedNodesVisitor)); - const toDeleteByIdentity = new Map>(); - const toRemoveSetter = (node: ITreeNode) => toDeleteByIdentity.set(identityProvider.getId(node.element).toString(), node); - const toRemoveDeleter = (node: { element: T; }) => toDeleteByIdentity.delete(identityProvider.getId(node.element).toString()); - deletedNodes.forEach(node => dfs(node, toRemoveSetter)); - insertedNodes.forEach(node => dfs(node, toRemoveDeleter)); + const insertedNodesMap = new Map>(); + const insertedNodesVisitor = (node: ITreeNode) => insertedNodesMap.set(this.identityProvider!.getId(node.element).toString(), node); + insertedNodes.forEach(node => dfs(node, insertedNodesVisitor)); - toDeleteByIdentity.forEach((_, id) => nodesByIdentity.delete(id)); - this.set(values(nodesByIdentity)); + const nodes: ITreeNode[] = []; + + for (const node of this.nodes) { + const id = this.identityProvider.getId(node.element).toString(); + const wasDeleted = deletedNodesIdSet.has(id); + + if (!wasDeleted) { + nodes.push(node); + } else { + const insertedNode = insertedNodesMap.get(id); + + if (insertedNode) { + nodes.push(insertedNode); + } + } + } + + this.set(nodes); } private createNodeSet(): Set> { diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index a5bcb9857b..152ac99e65 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -659,19 +659,6 @@ export class RunOnceWorker extends RunOnceScheduler { } } -export function nfcall(fn: Function, ...args: any[]): Promise; -export function nfcall(fn: Function, ...args: any[]): Promise; -export function nfcall(fn: Function, ...args: any[]): any { - return new Promise((c, e) => fn(...args, (err: any, result: any) => err ? e(err) : c(result))); -} - -export function ninvoke(thisArg: any, fn: Function, ...args: any[]): Promise; -export function ninvoke(thisArg: any, fn: Function, ...args: any[]): Promise; -export function ninvoke(thisArg: any, fn: Function, ...args: any[]): any { - return new Promise((resolve, reject) => fn.call(thisArg, ...args, (err: any, result: any) => err ? reject(err) : resolve(result))); -} - - //#region -- run on idle tricks ------------ export interface IdleDeadline { diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index f04e22427d..ce5bb1d562 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -6,6 +6,9 @@ declare var Buffer: any; const hasBuffer = (typeof Buffer !== 'undefined'); +let textEncoder: TextEncoder | null; +let textDecoder: TextDecoder | null; + export class VSBuffer { public static alloc(byteLength: number): VSBuffer { @@ -21,7 +24,14 @@ export class VSBuffer { } public static fromString(source: string): VSBuffer { - return new VSBuffer(Buffer.from(source)); + if (hasBuffer) { + return new VSBuffer(Buffer.from(source)); + } else { + if (!textEncoder) { + textEncoder = new TextEncoder(); + } + return new VSBuffer(textEncoder.encode(source)); + } } public static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer { @@ -52,7 +62,14 @@ export class VSBuffer { } public toString(): string { - return this.buffer.toString(); + if (hasBuffer) { + return this.buffer.toString(); + } else { + if (!textDecoder) { + textDecoder = new TextDecoder(); + } + return textDecoder.decode(this.buffer); + } } public slice(start?: number, end?: number): VSBuffer { diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts index 56b740fb6f..2563259cc4 100644 --- a/src/vs/base/common/cancellation.ts +++ b/src/vs/base/common/cancellation.ts @@ -87,7 +87,12 @@ class MutableToken implements CancellationToken { export class CancellationTokenSource { - private _token?: CancellationToken; + private _token?: CancellationToken = undefined; + private _parentListener?: IDisposable = undefined; + + constructor(parent?: CancellationToken) { + this._parentListener = parent && parent.onCancellationRequested(this.cancel, this); + } get token(): CancellationToken { if (!this._token) { @@ -112,6 +117,9 @@ export class CancellationTokenSource { } dispose(): void { + if (this._parentListener) { + this._parentListener.dispose(); + } if (!this._token) { // ensure to initialize with an empty token if we had none this._token = CancellationToken.None; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 136a7697d0..9a1390cec0 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -130,7 +130,7 @@ export namespace Event { * @param leading Whether the event should fire in the leading phase of the timeout. * @param leakWarningThreshold The leak warning threshold override. */ - export function debounce(event: Event, merge: (last: T, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): Event; + export function debounce(event: Event, merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): Event; export function debounce(event: Event, merge: (last: O | undefined, event: I) => O, delay?: number, leading?: boolean, leakWarningThreshold?: number): Event; export function debounce(event: Event, merge: (last: O | undefined, event: I) => O, delay: number = 100, leading = false, leakWarningThreshold?: number): Event { @@ -488,7 +488,7 @@ export class Emitter { private readonly _leakageMon?: LeakageMonitor; private _disposed: boolean = false; private _event?: Event; - private _deliveryQueue: [Listener, T][]; + private _deliveryQueue?: LinkedList<[Listener, T]>; protected _listeners?: LinkedList>; constructor(options?: EmitterOptions) { @@ -570,14 +570,14 @@ export class Emitter { // the driver of this if (!this._deliveryQueue) { - this._deliveryQueue = []; + this._deliveryQueue = new LinkedList(); } for (let iter = this._listeners.iterator(), e = iter.next(); !e.done; e = iter.next()) { this._deliveryQueue.push([e.value, event]); } - while (this._deliveryQueue.length > 0) { + while (this._deliveryQueue.size > 0) { const [listener, event] = this._deliveryQueue.shift()!; try { if (typeof listener === 'function') { @@ -594,10 +594,10 @@ export class Emitter { dispose() { if (this._listeners) { - this._listeners = undefined; + this._listeners.clear(); } if (this._deliveryQueue) { - this._deliveryQueue.length = 0; + this._deliveryQueue.clear(); } if (this._leakageMon) { this._leakageMon.dispose(); @@ -606,6 +606,51 @@ export class Emitter { } } +export class PauseableEmitter extends Emitter { + + private _isPaused = 0; + private _eventQueue = new LinkedList(); + private _mergeFn?: (input: T[]) => T; + + constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) { + super(options); + this._mergeFn = options && options.merge; + } + + pause(): void { + this._isPaused++; + } + + resume(): void { + if (this._isPaused !== 0 && --this._isPaused === 0) { + if (this._mergeFn) { + // use the merge function to create a single composite + // event. make a copy in case firing pauses this emitter + const events = this._eventQueue.toArray(); + this._eventQueue.clear(); + super.fire(this._mergeFn(events)); + + } else { + // no merging, fire each event individually and test + // that this emitter isn't paused halfway through + while (!this._isPaused && this._eventQueue.size !== 0) { + super.fire(this._eventQueue.shift()!); + } + } + } + } + + fire(event: T): void { + if (this._listeners) { + if (this._isPaused !== 0) { + this._eventQueue.push(event); + } else { + super.fire(event); + } + } + } +} + export interface IWaitUntil { waitUntil(thenable: Promise): void; } diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index 8bdc589e6f..80736ee8a2 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { isWindows } from 'vs/base/common/platform'; -import { startsWithIgnoreCase, equalsIgnoreCase } from 'vs/base/common/strings'; +import { startsWithIgnoreCase, equalsIgnoreCase, endsWith, rtrim } from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; -import { sep, posix } from 'vs/base/common/path'; +import { sep, posix, isAbsolute, join, normalize } from 'vs/base/common/path'; function isPathSeparator(code: number) { return code === CharCode.Slash || code === CharCode.Backslash; @@ -227,4 +227,56 @@ export function isEqualOrParent(path: string, candidate: string, ignoreCase?: bo export function isWindowsDriveLetter(char0: number): boolean { return char0 >= CharCode.A && char0 <= CharCode.Z || char0 >= CharCode.a && char0 <= CharCode.z; +} + +export function sanitizeFilePath(candidate: string, cwd: string): string { + + // Special case: allow to open a drive letter without trailing backslash + if (isWindows && endsWith(candidate, ':')) { + candidate += sep; + } + + // Ensure absolute + if (!isAbsolute(candidate)) { + candidate = join(cwd, candidate); + } + + // Ensure normalized + candidate = normalize(candidate); + + // Ensure no trailing slash/backslash + if (isWindows) { + candidate = rtrim(candidate, sep); + + // Special case: allow to open drive root ('C:\') + if (endsWith(candidate, ':')) { + candidate += sep; + } + + } else { + candidate = rtrim(candidate, sep); + + // Special case: allow to open root ('/') + if (!candidate) { + candidate = sep; + } + } + + return candidate; +} + +export function isRootOrDriveLetter(path: string): boolean { + const pathNormalized = normalize(path); + + if (isWindows) { + if (path.length > 3) { + return false; + } + + return isWindowsDriveLetter(pathNormalized.charCodeAt(0)) + && pathNormalized.charCodeAt(1) === CharCode.Colon + && (path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash); + } + + return pathNormalized === posix.sep; } \ No newline at end of file diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index ef8fed56f7..6b23264127 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -12,7 +12,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { isThenable } from 'vs/base/common/async'; export interface IExpression { - [pattern: string]: boolean | SiblingClause | any; + [pattern: string]: boolean | SiblingClause; } export interface IRelativePattern { @@ -429,7 +429,7 @@ function toRegExp(pattern: string): ParsedStringPattern { */ export function match(pattern: string | IRelativePattern, path: string): boolean; export function match(expression: IExpression, path: string, hasSibling?: (name: string) => boolean): string /* the matching pattern */; -export function match(arg1: string | IExpression | IRelativePattern, path: string, hasSibling?: (name: string) => boolean): any { +export function match(arg1: string | IExpression | IRelativePattern, path: string, hasSibling?: (name: string) => boolean): boolean | string | null | Promise { if (!arg1 || typeof path !== 'string') { return false; } @@ -447,14 +447,14 @@ export function match(arg1: string | IExpression | IRelativePattern, path: strin */ export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern; export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression; -export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): any { +export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): ParsedPattern | ParsedExpression { if (!arg1) { return FALSE; } // Glob with String if (typeof arg1 === 'string' || isRelativePattern(arg1)) { - const parsedPattern = parsePattern(arg1 as string | IRelativePattern, options); + const parsedPattern = parsePattern(arg1, options); if (parsedPattern === NULL) { return FALSE; } @@ -512,23 +512,12 @@ function listToMap(list: string[]) { return map; } -export function isRelativePattern(obj: any): obj is IRelativePattern { +export function isRelativePattern(obj: unknown): obj is IRelativePattern { const rp = obj as IRelativePattern; return rp && typeof rp.base === 'string' && typeof rp.pattern === 'string'; } -/** - * Same as `parse`, but the ParsedExpression is guaranteed to return a Promise - */ -export function parseToAsync(expression: IExpression, options?: IGlobOptions): ParsedExpression { - const parsedExpression = parse(expression, options); - return (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise): string | null | Promise => { - const result = parsedExpression(path, basename, hasSibling); - return isThenable(result) ? result : Promise.resolve(result); - }; -} - export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] { return (patternOrExpression).allBasenames || []; } @@ -613,7 +602,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return resultExpression; } -function parseExpressionPattern(pattern: string, value: any, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) { +function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) { if (value === false) { return NULL; // pattern is disabled } diff --git a/src/vs/base/common/linkedList.ts b/src/vs/base/common/linkedList.ts index 2405511f81..1849266b11 100644 --- a/src/vs/base/common/linkedList.ts +++ b/src/vs/base/common/linkedList.ts @@ -6,19 +6,24 @@ import { Iterator, IteratorResult, FIN } from 'vs/base/common/iterator'; class Node { + + static readonly Undefined = new Node(undefined); + element: E; - next: Node | undefined; - prev: Node | undefined; + next: Node; + prev: Node; constructor(element: E) { this.element = element; + this.next = Node.Undefined; + this.prev = Node.Undefined; } } export class LinkedList { - private _first: Node | undefined; - private _last: Node | undefined; + private _first: Node = Node.Undefined; + private _last: Node = Node.Undefined; private _size: number = 0; get size(): number { @@ -26,12 +31,12 @@ export class LinkedList { } isEmpty(): boolean { - return !this._first; + return this._first === Node.Undefined; } clear(): void { - this._first = undefined; - this._last = undefined; + this._first = Node.Undefined; + this._last = Node.Undefined; this._size = 0; } @@ -45,7 +50,7 @@ export class LinkedList { private _insert(element: E, atTheEnd: boolean): () => void { const newNode = new Node(element); - if (!this._first) { + if (this._first === Node.Undefined) { this._first = newNode; this._last = newNode; @@ -64,12 +69,18 @@ export class LinkedList { oldFirst.prev = newNode; } this._size += 1; - return this._remove.bind(this, newNode); + + let didRemove = false; + return () => { + if (!didRemove) { + didRemove = true; + this._remove(newNode); + } + }; } - shift(): E | undefined { - if (!this._first) { + if (this._first === Node.Undefined) { return undefined; } else { const res = this._first.element; @@ -79,7 +90,7 @@ export class LinkedList { } pop(): E | undefined { - if (!this._last) { + if (this._last === Node.Undefined) { return undefined; } else { const res = this._last.element; @@ -89,38 +100,30 @@ export class LinkedList { } private _remove(node: Node): void { - let candidate: Node | undefined = this._first; - while (candidate instanceof Node) { - if (candidate !== node) { - candidate = candidate.next; - continue; - } - if (candidate.prev && candidate.next) { - // middle - const anchor = candidate.prev; - anchor.next = candidate.next; - candidate.next.prev = anchor; + if (node.prev !== Node.Undefined && node.next !== Node.Undefined) { + // middle + const anchor = node.prev; + anchor.next = node.next; + node.next.prev = anchor; - } else if (!candidate.prev && !candidate.next) { - // only node - this._first = undefined; - this._last = undefined; + } else if (node.prev === Node.Undefined && node.next === Node.Undefined) { + // only node + this._first = Node.Undefined; + this._last = Node.Undefined; - } else if (!candidate.next) { - // last - this._last = this._last!.prev!; - this._last.next = undefined; + } else if (node.next === Node.Undefined) { + // last + this._last = this._last!.prev!; + this._last.next = Node.Undefined; - } else if (!candidate.prev) { - // first - this._first = this._first!.next!; - this._first.prev = undefined; - } - - // done - this._size -= 1; - break; + } else if (node.prev === Node.Undefined) { + // first + this._first = this._first!.next!; + this._first.prev = Node.Undefined; } + + // done + this._size -= 1; } iterator(): Iterator { @@ -128,7 +131,7 @@ export class LinkedList { let node = this._first; return { next(): IteratorResult { - if (!node) { + if (node === Node.Undefined) { return FIN; } @@ -145,7 +148,7 @@ export class LinkedList { toArray(): E[] { const result: E[] = []; - for (let node = this._first; node instanceof Node; node = node.next) { + for (let node = this._first; node !== Node.Undefined; node = node.next) { result.push(node.element); } return result; diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index 6ca46fab65..c8c1586c73 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -175,34 +175,6 @@ export function equals(one: any, other: any): boolean { return true; } -function arrayToHash(array: string[]): { [name: string]: true } { - const result: any = {}; - for (const e of array) { - result[e] = true; - } - return result; -} - -/** - * Given an array of strings, returns a function which, given a string - * returns true or false whether the string is in that array. - */ -export function createKeywordMatcher(arr: string[], caseInsensitive: boolean = false): (str: string) => boolean { - if (caseInsensitive) { - arr = arr.map(function (x) { return x.toLowerCase(); }); - } - const hash = arrayToHash(arr); - if (caseInsensitive) { - return function (word) { - return hash[word.toLowerCase()] !== undefined && hash.hasOwnProperty(word.toLowerCase()); - }; - } else { - return function (word) { - return hash[word] !== undefined && hash.hasOwnProperty(word); - }; - } -} - /** * Calls JSON.Stringify with a replacer to break apart any circular references. * This prevents JSON.stringify from throwing the exception diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts index d0232d52c2..2eefa4795a 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts @@ -87,6 +87,17 @@ export const enum TerminateResponseCode { ProcessNotFound = 3, } +export interface ProcessItem { + name: string; + cmd: string; + pid: number; + ppid: number; + load: number; + mem: number; + + children?: ProcessItem[]; +} + /** * Sanitizes a VS Code process environment by removing all Electron/VS Code-related values. */ diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index a9667e174c..cd14337bce 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -159,27 +159,6 @@ export function validateConstraint(arg: any, constraint: TypeConstraint | undefi } } -/** - * Creates a new object of the provided class and will call the constructor with - * any additional argument supplied. - */ -export function create(ctor: Function, ...args: any[]): any { - if (isNativeClass(ctor)) { - return new (ctor as any)(...args); - } else { - const obj = Object.create(ctor.prototype); - ctor.apply(obj, args); - return obj; - } -} - -// https://stackoverflow.com/a/32235645/1499159 -function isNativeClass(thing: any): boolean { - return typeof thing === 'function' - && thing.hasOwnProperty('prototype') - && !thing.hasOwnProperty('arguments'); -} - export function getAllPropertyNames(obj: object): string[] { let res: string[] = []; let proto = Object.getPrototypeOf(obj); @@ -202,4 +181,4 @@ export function withNullAsUndefined(x: T | null): T | undefined { */ export function withUndefinedAsNull(x: T | undefined): T | null { return typeof x === 'undefined' ? null : x; -} \ No newline at end of file +} diff --git a/src/vs/base/common/uint.ts b/src/vs/base/common/uint.ts deleted file mode 100644 index 3886194553..0000000000 --- a/src/vs/base/common/uint.ts +++ /dev/null @@ -1,112 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export function toUint8ArrayBuffer(str: string): ArrayBuffer { - - if (typeof TextEncoder !== 'undefined') { - return new TextEncoder().encode(str).buffer; - } - - let i: number, len: number, length = 0, charCode = 0, trailCharCode = 0, codepoint = 0; - - // First pass, for the size - for (i = 0, len = str.length; i < len; i++) { - charCode = str.charCodeAt(i); - - // Surrogate pair - if (charCode >= 0xD800 && charCode < 0xDC00) { - trailCharCode = str.charCodeAt(++i); - - if (!(trailCharCode >= 0xDC00 && trailCharCode < 0xE000)) { - throw new Error('Invalid char code'); - } - - // Code point can be obtained by subtracting 0xD800 and 0xDC00 from both char codes respectively - // and joining the 10 least significant bits from each, finally adding 0x10000. - codepoint = ((((charCode - 0xD800) & 0x3FF) << 10) | ((trailCharCode - 0xDC00) & 0x3FF)) + 0x10000; - - } else { - codepoint = charCode; - } - - length += byteSizeInUTF8(codepoint); - } - - let result = new ArrayBuffer(length); - let view = new Uint8Array(result); - let pos = 0; - - // Second pass, for the data - for (i = 0, len = str.length; i < len; i++) { - charCode = str.charCodeAt(i); - - if (charCode >= 0xD800 && charCode < 0xDC00) { - trailCharCode = str.charCodeAt(++i); - codepoint = ((((charCode - 0xD800) & 0x3FF) << 10) | ((trailCharCode - 0xDC00) & 0x3FF)) + 0x10000; - } else { - codepoint = charCode; - } - - pos += writeUTF8(codepoint, view, pos); - } - - return result; -} - -function byteSizeInUTF8(codePoint: number): number { - codePoint = codePoint >>> 0; - - if (codePoint < 0x80) { - return 1; - } else if (codePoint < 0x800) { - return 2; - } else if (codePoint < 0x10000) { - return 3; - } else if (codePoint < 0x200000) { - return 4; - } else if (codePoint < 0x4000000) { - return 5; - } else if (codePoint < 0x80000000) { - return 6; - } else { - throw new Error('Code point 0x' + toHexString(codePoint) + ' not encodable in UTF8.'); - } -} - -function writeUTF8(codePoint: number, buffer: Uint8Array, pos: number): number { - - // How many bits needed for codePoint - let byteSize = byteSizeInUTF8(codePoint); - - // 0xxxxxxx - if (byteSize === 1) { - buffer[pos] = codePoint; - return 1; - } - - // 110xxxxx 10xxxxxx - // 1110xxxx 10xxxxxx 10xxxxxx - // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - // 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx - // 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx - - // first byte - buffer[pos] = ((0xFC << (6 - byteSize)) | (codePoint >>> (6 * (byteSize - 1)))) & 0xFF; - - // successive bytes - for (let i = 1; i < byteSize; i++) { - buffer[pos + i] = (0x80 | (0x3F & (codePoint >>> (6 * (byteSize - i - 1))))) & 0xFF; - } - - return byteSize; -} - -function leftPad(value: string, length: number, char: string = '0'): string { - return new Array(length - value.length + 1).join(char) + value; -} - -function toHexString(value: number, bitsize: number = 32): string { - return leftPad((value >>> 0).toString(16), bitsize / 4); -} diff --git a/src/vs/base/node/config.ts b/src/vs/base/node/config.ts index 1f2814184c..aec9aa9ede 100644 --- a/src/vs/base/node/config.ts +++ b/src/vs/base/node/config.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import { dirname, basename } from 'vs/base/common/path'; +import { dirname } from 'vs/base/common/path'; import * as objects from 'vs/base/common/objects'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; -import * as extfs from 'vs/base/node/extfs'; -import { isWindows } from 'vs/base/common/platform'; +import { readlink, statLink } from 'vs/base/node/pfs'; +import { watchFolder, watchFile } from 'vs/base/node/watcher'; export interface IConfigurationChangeEvent { config: T; @@ -48,11 +48,9 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { private timeoutHandle: NodeJS.Timer | null; private disposables: IDisposable[]; private readonly _onDidUpdateConfiguration: Emitter>; - private configName: string; constructor(private _path: string, private options: IConfigOptions = { defaultConfig: Object.create(null), onError: error => console.error(error) }) { this.disposables = []; - this.configName = basename(this._path); this._onDidUpdateConfiguration = new Emitter>(); this.disposables.push(this._onDidUpdateConfiguration); @@ -61,15 +59,15 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { this.initAsync(); } - public get path(): string { + get path(): string { return this._path; } - public get hasParseErrors(): boolean { + get hasParseErrors(): boolean { return this.parseErrors && this.parseErrors.length > 0; } - public get onDidUpdateConfiguration(): Event> { + get onDidUpdateConfiguration(): Event> { return this._onDidUpdateConfiguration.event; } @@ -126,50 +124,31 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { this.watch(parentFolder, true); // Check if the path is a symlink and watch its target if so - fs.lstat(this._path, (err, stat) => { - if (err || stat.isDirectory()) { - return; // path is not a valid file - } - - // We found a symlink - if (stat.isSymbolicLink()) { - fs.readlink(this._path, (err, realPath) => { - if (err) { - return; // path is not a valid symlink - } - - this.watch(realPath, false); - }); - } - }); + this.handleSymbolicLink().then(undefined, error => { /* ignore error */ }); } - private watch(path: string, isParentFolder: boolean): void { + private async handleSymbolicLink(): Promise { + const { stat, isSymbolicLink } = await statLink(this._path); + if (isSymbolicLink && !stat.isDirectory()) { + const realPath = await readlink(this._path); + + this.watch(realPath, false); + } + } + + private watch(path: string, isFolder: boolean): void { if (this.disposed) { return; // avoid watchers that will never get disposed by checking for being disposed } - this.disposables.push(extfs.watch(path, - (type, file) => this.onConfigFileChange(type, file, isParentFolder), - (error: string) => this.options.onError(error) - )); + if (isFolder) { + this.disposables.push(watchFolder(path, (type, path) => path === this._path ? this.onConfigFileChange() : undefined, error => this.options.onError(error))); + } else { + this.disposables.push(watchFile(path, (type, path) => this.onConfigFileChange(), error => this.options.onError(error))); + } } - private onConfigFileChange(eventType: string, filename: string | undefined, isParentFolder: boolean): void { - if (isParentFolder) { - - // Windows: in some cases the filename contains artifacts from the absolute path - // see https://github.com/nodejs/node/issues/19170 - // As such, we have to ensure that the filename basename is used for comparison. - if (isWindows && filename && filename !== this.configName) { - filename = basename(filename); - } - - if (filename !== this.configName) { - return; // a change to a sibling file that is not our config file - } - } - + private onConfigFileChange(): void { if (this.timeoutHandle) { global.clearTimeout(this.timeoutHandle); this.timeoutHandle = null; @@ -179,7 +158,7 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { this.timeoutHandle = global.setTimeout(() => this.reload(), this.options.changeBufferDelay || 0); } - public reload(callback?: (config: T) => void): void { + reload(callback?: (config: T) => void): void { this.loadAsync(currentConfig => { if (!objects.equals(currentConfig, this.cache)) { this.updateCache(currentConfig); @@ -193,7 +172,7 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { }); } - public getConfig(): T { + getConfig(): T { this.ensureLoaded(); return this.cache; @@ -205,7 +184,7 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { } } - public dispose(): void { + dispose(): void { this.disposed = true; this.disposables = dispose(this.disposables); } diff --git a/src/vs/base/node/cpuUsage.sh b/src/vs/base/node/cpuUsage.sh index 94d56de989..3d42b36dc2 100755 --- a/src/vs/base/node/cpuUsage.sh +++ b/src/vs/base/node/cpuUsage.sh @@ -56,7 +56,7 @@ for PID in "$@"; do PROCESS_TIME_BEFORE=${PROCESS_BEFORE_TIMES[$ITER]} let PROCESS_DELTA=$PROCESS_TIME_AFTER-$PROCESS_TIME_BEFORE let TOTAL_DELTA=$TOTAL_TIME_AFTER-$TOTAL_TIME_BEFORE - CPU_USAGE=`echo "100*$PROCESS_DELTA/$TOTAL_DELTA" | bc -l` + CPU_USAGE=`echo "$((100*$PROCESS_DELTA/$TOTAL_DELTA))"` # Parent script reads from stdout, so echo result to be read echo $CPU_USAGE diff --git a/src/vs/base/node/decoder.ts b/src/vs/base/node/decoder.ts index 1857c7fbf9..b087eee4c8 100644 --- a/src/vs/base/node/decoder.ts +++ b/src/vs/base/node/decoder.ts @@ -23,7 +23,7 @@ export class LineDecoder { this.remaining = null; } - public write(buffer: Buffer): string[] { + write(buffer: Buffer): string[] { const result: string[] = []; const value = this.remaining ? this.remaining + this.stringDecoder.write(buffer) @@ -56,7 +56,7 @@ export class LineDecoder { return result; } - public end(): string | null { + end(): string | null { return this.remaining; } } \ No newline at end of file diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index c41217ad8d..2d38d1b3e9 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -36,7 +36,7 @@ export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions readable.pipe(new class extends Writable { private _decodeStream: NodeJS.ReadWriteStream; - private _decodeStreamConstruction: Promise; + private _decodeStreamConstruction: Promise; private _buffer: Buffer[] = []; private _bytesBuffered = 0; diff --git a/src/vs/base/node/extfs.ts b/src/vs/base/node/extfs.ts deleted file mode 100644 index 8844eea666..0000000000 --- a/src/vs/base/node/extfs.ts +++ /dev/null @@ -1,707 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as fs from 'fs'; -import * as paths from 'vs/base/common/path'; -import { nfcall } from 'vs/base/common/async'; -import { normalizeNFC } from 'vs/base/common/normalization'; -import * as platform from 'vs/base/common/platform'; -import * as strings from 'vs/base/common/strings'; -import * as uuid from 'vs/base/common/uuid'; -import { encode, encodeStream } from 'vs/base/node/encoding'; -import * as flow from 'vs/base/node/flow'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; - -const loop = flow.loop; - -export function readdirSync(path: string): string[] { - // Mac: uses NFD unicode form on disk, but we want NFC - // See also https://github.com/nodejs/node/issues/2165 - if (platform.isMacintosh) { - return fs.readdirSync(path).map(c => normalizeNFC(c)); - } - - return fs.readdirSync(path); -} - -export function readdir(path: string, callback: (error: Error | null, files: string[]) => void): void { - // Mac: uses NFD unicode form on disk, but we want NFC - // See also https://github.com/nodejs/node/issues/2165 - if (platform.isMacintosh) { - return fs.readdir(path, (error, children) => { - if (error) { - return callback(error, []); - } - - return callback(null, children.map(c => normalizeNFC(c))); - }); - } - - return fs.readdir(path, callback); -} - -export interface IStatAndLink { - stat: fs.Stats; - isSymbolicLink: boolean; -} - -export function statLink(path: string, callback: (error: Error | null, statAndIsLink: IStatAndLink | null) => void): void { - fs.lstat(path, (error, lstat) => { - if (error || lstat.isSymbolicLink()) { - fs.stat(path, (error, stat) => { - if (error) { - return callback(error, null); - } - - callback(null, { stat, isSymbolicLink: lstat && lstat.isSymbolicLink() }); - }); - } else { - callback(null, { stat: lstat, isSymbolicLink: false }); - } - }); -} - -export function copy(source: string, target: string, callback: (error: Error | null) => void, copiedSourcesIn?: { [path: string]: boolean }): void { - const copiedSources = copiedSourcesIn ? copiedSourcesIn : Object.create(null); - - fs.stat(source, (error, stat) => { - if (error) { - return callback(error); - } - - if (!stat.isDirectory()) { - return doCopyFile(source, target, stat.mode & 511, callback); - } - - if (copiedSources[source]) { - return callback(null); // escape when there are cycles (can happen with symlinks) - } - - copiedSources[source] = true; // remember as copied - - const proceed = function () { - readdir(source, (err, files) => { - loop(files, (file: string, clb: (error: Error | null, result: string[]) => void) => { - copy(paths.join(source, file), paths.join(target, file), (error: Error) => clb(error, []), copiedSources); - }, callback); - }); - }; - - mkdirp(target, stat.mode & 511).then(proceed, proceed); - }); -} - -function doCopyFile(source: string, target: string, mode: number, callback: (error: Error) => void): void { - const reader = fs.createReadStream(source); - const writer = fs.createWriteStream(target, { mode }); - - let finished = false; - const finish = (error?: Error) => { - if (!finished) { - finished = true; - - // in error cases, pass to callback - if (error) { - callback(error); - } - - // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 - else { - fs.chmod(target, mode, callback); - } - } - }; - - // handle errors properly - reader.once('error', error => finish(error)); - writer.once('error', error => finish(error)); - - // we are done (underlying fd has been closed) - writer.once('close', () => finish()); - - // start piping - reader.pipe(writer); -} - -export function mkdirp(path: string, mode?: number, token?: CancellationToken): Promise { - const mkdir = (): Promise => { - return nfcall(fs.mkdir, path, mode).then(undefined, (mkdirErr: NodeJS.ErrnoException) => { - - // ENOENT: a parent folder does not exist yet - if (mkdirErr.code === 'ENOENT') { - return Promise.reject(mkdirErr); - } - - // Any other error: check if folder exists and - // return normally in that case if its a folder - return nfcall(fs.stat, path).then((stat: fs.Stats) => { - if (!stat.isDirectory()) { - return Promise.reject(new Error(`'${path}' exists and is not a directory.`)); - } - - return null; - }, statErr => { - return Promise.reject(mkdirErr); // bubble up original mkdir error - }); - }); - }; - - // stop at root - if (path === paths.dirname(path)) { - return Promise.resolve(true); - } - - // recursively mkdir - return mkdir().then(undefined, (err: NodeJS.ErrnoException) => { - - // Respect cancellation - if (token && token.isCancellationRequested) { - return Promise.resolve(false); - } - - // ENOENT: a parent folder does not exist yet, continue - // to create the parent folder and then try again. - if (err.code === 'ENOENT') { - return mkdirp(paths.dirname(path), mode).then(mkdir); - } - - // Any other error - return Promise.reject(err); - }); -} - -// Deletes the given path by first moving it out of the workspace. This has two benefits. For one, the operation can return fast because -// after the rename, the contents are out of the workspace although not yet deleted. The greater benefit however is that this operation -// will fail in case any file is used by another process. fs.unlink() in node will not bail if a file unlinked is used by another process. -// However, the consequences are bad as outlined in all the related bugs from https://github.com/joyent/node/issues/7164 -export function del(path: string, tmpFolder: string, callback: (error: Error | null) => void, done?: (error: Error | null) => void): void { - fs.exists(path, exists => { - if (!exists) { - return callback(null); - } - - fs.stat(path, (err, stat) => { - if (err || !stat) { - return callback(err); - } - - // Special windows workaround: A file or folder that ends with a "." cannot be moved to another place - // because it is not a valid file name. In this case, we really have to do the deletion without prior move. - if (path[path.length - 1] === '.' || strings.endsWith(path, './') || strings.endsWith(path, '.\\')) { - return rmRecursive(path, callback); - } - - const pathInTemp = paths.join(tmpFolder, uuid.generateUuid()); - fs.rename(path, pathInTemp, (error: Error | null) => { - if (error) { - return rmRecursive(path, callback); // if rename fails, delete without tmp dir - } - - // Return early since the move succeeded - callback(null); - - // do the heavy deletion outside the callers callback - rmRecursive(pathInTemp, error => { - if (done) { - done(error); - } - }); - }); - }); - }); -} - -function rmRecursive(path: string, callback: (error: Error | null) => void): void { - if (path === paths.win32.sep || path === paths.posix.sep) { - return callback(new Error('Will not delete root!')); - } - - fs.exists(path, exists => { - if (!exists) { - callback(null); - } else { - fs.lstat(path, (err, stat) => { - if (err || !stat) { - callback(err); - } else if (!stat.isDirectory() || stat.isSymbolicLink() /* !!! never recurse into links when deleting !!! */) { - const mode = stat.mode; - if (!(mode & 128)) { // 128 === 0200 - fs.chmod(path, mode | 128, (err: Error) => { // 128 === 0200 - if (err) { - callback(err); - } else { - fs.unlink(path, callback); - } - }); - } else { - fs.unlink(path, callback); - } - } else { - readdir(path, (err, children) => { - if (err || !children) { - callback(err); - } else if (children.length === 0) { - fs.rmdir(path, callback); - } else { - let firstError: Error | null = null; - let childrenLeft = children.length; - children.forEach(child => { - rmRecursive(paths.join(path, child), (err: Error) => { - childrenLeft--; - if (err) { - firstError = firstError || err; - } - - if (childrenLeft === 0) { - if (firstError) { - callback(firstError); - } else { - fs.rmdir(path, callback); - } - } - }); - }); - } - }); - } - }); - } - }); -} - -export function delSync(path: string): void { - if (path === paths.win32.sep || path === paths.posix.sep) { - throw new Error('Will not delete root!'); - } - - try { - const stat = fs.lstatSync(path); - if (stat.isDirectory() && !stat.isSymbolicLink()) { - readdirSync(path).forEach(child => delSync(paths.join(path, child))); - fs.rmdirSync(path); - } else { - fs.unlinkSync(path); - } - } catch (err) { - if (err.code === 'ENOENT') { - return; // not found - } - - throw err; - } -} - -export function mv(source: string, target: string, callback: (error: Error | null) => void): void { - if (source === target) { - return callback(null); - } - - function updateMtime(err: Error | null): void { - if (err) { - return callback(err); - } - - fs.lstat(target, (error, stat) => { - if (error) { - return callback(error); - } - - if (stat.isDirectory() || stat.isSymbolicLink()) { - return callback(null); - } - - fs.open(target, 'a', null, (err: Error, fd: number) => { - if (err) { - return callback(err); - } - - fs.futimes(fd, stat.atime, new Date(), (err: Error) => { - if (err) { - return callback(err); - } - - fs.close(fd, callback); - }); - }); - }); - } - - // Try native rename() - fs.rename(source, target, (err: Error) => { - if (!err) { - return updateMtime(null); - } - - // In two cases we fallback to classic copy and delete: - // - // 1.) The EXDEV error indicates that source and target are on different devices - // In this case, fallback to using a copy() operation as there is no way to - // rename() between different devices. - // - // 2.) The user tries to rename a file/folder that ends with a dot. This is not - // really possible to move then, at least on UNC devices. - if (err && source.toLowerCase() !== target.toLowerCase() && ((err).code === 'EXDEV') || strings.endsWith(source, '.')) { - return copy(source, target, (err: Error) => { - if (err) { - return callback(err); - } - - rmRecursive(source, updateMtime); - }); - } - - return callback(err); - }); -} - -export interface IWriteFileOptions { - mode?: number; - flag?: string; - encoding?: { - charset: string; - addBOM: boolean; - }; -} - -interface IEnsuredWriteFileOptions extends IWriteFileOptions { - mode: number; - flag: string; -} - -let canFlush = true; -export function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options: IWriteFileOptions, callback: (error?: Error) => void): void { - const ensuredOptions = ensureWriteOptions(options); - - if (typeof data === 'string' || Buffer.isBuffer(data) || data instanceof Uint8Array) { - doWriteFileAndFlush(path, data, ensuredOptions, callback); - } else { - doWriteFileStreamAndFlush(path, data, ensuredOptions, callback); - } -} - -function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: IEnsuredWriteFileOptions, callback: (error?: Error) => void): void { - - // finish only once - let finished = false; - const finish = (error?: Error) => { - if (!finished) { - finished = true; - - // in error cases we need to manually close streams - // if the write stream was successfully opened - if (error) { - if (isOpen) { - writer.once('close', () => callback(error)); - writer.destroy(); - } else { - callback(error); - } - } - - // otherwise just return without error - else { - callback(); - } - } - }; - - // create writer to target. we set autoClose: false because we want to use the streams - // file descriptor to call fs.fdatasync to ensure the data is flushed to disk - const writer = fs.createWriteStream(path, { mode: options.mode, flags: options.flag, autoClose: false }); - - // Event: 'open' - // Purpose: save the fd for later use and start piping - // Notes: will not be called when there is an error opening the file descriptor! - let fd: number; - let isOpen: boolean; - writer.once('open', descriptor => { - fd = descriptor; - isOpen = true; - - // if an encoding is provided, we need to pipe the stream through - // an encoder stream and forward the encoding related options - if (options.encoding) { - reader = reader.pipe(encodeStream(options.encoding.charset, { addBOM: options.encoding.addBOM })); - } - - // start data piping only when we got a successful open. this ensures that we do - // not consume the stream when an error happens and helps to fix this issue: - // https://github.com/Microsoft/vscode/issues/42542 - reader.pipe(writer); - }); - - // Event: 'error' - // Purpose: to return the error to the outside and to close the write stream (does not happen automatically) - reader.once('error', error => finish(error)); - writer.once('error', error => finish(error)); - - // Event: 'finish' - // Purpose: use fs.fdatasync to flush the contents to disk - // Notes: event is called when the writer has finished writing to the underlying resource. we must call writer.close() - // because we have created the WriteStream with autoClose: false - writer.once('finish', () => { - - // flush to disk - if (canFlush && isOpen) { - fs.fdatasync(fd, (syncError: Error) => { - - // In some exotic setups it is well possible that node fails to sync - // In that case we disable flushing and warn to the console - if (syncError) { - console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError); - canFlush = false; - } - - writer.destroy(); - }); - } else { - writer.destroy(); - } - }); - - // Event: 'close' - // Purpose: signal we are done to the outside - // Notes: event is called when the writer's filedescriptor is closed - writer.once('close', () => finish()); -} - -// Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk -// We do this in cases where we want to make sure the data is really on disk and -// not in some cache. -// -// See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194 -function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, options: IEnsuredWriteFileOptions, callback: (error?: Error) => void): void { - if (options.encoding) { - data = encode(data instanceof Uint8Array ? Buffer.from(data) : data, options.encoding.charset, { addBOM: options.encoding.addBOM }); - } - - if (!canFlush) { - return fs.writeFile(path, data, { mode: options.mode, flag: options.flag }, callback); - } - - // Open the file with same flags and mode as fs.writeFile() - fs.open(path, options.flag, options.mode, (openError, fd) => { - if (openError) { - return callback(openError); - } - - // It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open! - fs.writeFile(fd, data, writeError => { - if (writeError) { - return fs.close(fd, () => callback(writeError)); // still need to close the handle on error! - } - - // Flush contents (not metadata) of the file to disk - fs.fdatasync(fd, (syncError: Error) => { - - // In some exotic setups it is well possible that node fails to sync - // In that case we disable flushing and warn to the console - if (syncError) { - console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError); - canFlush = false; - } - - return fs.close(fd, closeError => callback(closeError)); - }); - }); - }); -} - -export function writeFileAndFlushSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void { - const ensuredOptions = ensureWriteOptions(options); - - if (ensuredOptions.encoding) { - data = encode(data, ensuredOptions.encoding.charset, { addBOM: ensuredOptions.encoding.addBOM }); - } - - if (!canFlush) { - return fs.writeFileSync(path, data, { mode: ensuredOptions.mode, flag: ensuredOptions.flag }); - } - - // Open the file with same flags and mode as fs.writeFile() - const fd = fs.openSync(path, ensuredOptions.flag, ensuredOptions.mode); - - try { - - // It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open! - fs.writeFileSync(fd, data); - - // Flush contents (not metadata) of the file to disk - try { - fs.fdatasyncSync(fd); - } catch (syncError) { - console.warn('[node.js fs] fdatasyncSync is now disabled for this session because it failed: ', syncError); - canFlush = false; - } - } finally { - fs.closeSync(fd); - } -} - -function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptions { - if (!options) { - return { mode: 0o666, flag: 'w' }; - } - - return { - mode: typeof options.mode === 'number' ? options.mode : 0o666, - flag: typeof options.flag === 'string' ? options.flag : 'w', - encoding: options.encoding - }; -} - -/** - * Copied from: https://github.com/Microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83 - * - * Given an absolute, normalized, and existing file path 'realcase' returns the exact path that the file has on disk. - * On a case insensitive file system, the returned path might differ from the original path by character casing. - * On a case sensitive file system, the returned path will always be identical to the original path. - * In case of errors, null is returned. But you cannot use this function to verify that a path exists. - * realcaseSync does not handle '..' or '.' path segments and it does not take the locale into account. - */ -export function realcaseSync(path: string): string | null { - const dir = paths.dirname(path); - if (path === dir) { // end recursion - return path; - } - - const name = (paths.basename(path) /* can be '' for windows drive letters */ || path).toLowerCase(); - try { - const entries = readdirSync(dir); - const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search - if (found.length === 1) { - // on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition - const prefix = realcaseSync(dir); // recurse - if (prefix) { - return paths.join(prefix, found[0]); - } - } else if (found.length > 1) { - // must be a case sensitive $filesystem - const ix = found.indexOf(name); - if (ix >= 0) { // case sensitive - const prefix = realcaseSync(dir); // recurse - if (prefix) { - return paths.join(prefix, found[ix]); - } - } - } - } catch (error) { - // silently ignore error - } - - return null; -} - -export function realpathSync(path: string): string { - try { - return fs.realpathSync(path); - } catch (error) { - - // We hit an error calling fs.realpathSync(). Since fs.realpathSync() is doing some path normalization - // we now do a similar normalization and then try again if we can access the path with read - // permissions at least. If that succeeds, we return that path. - // fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is - // to not resolve links but to simply see if the path is read accessible or not. - const normalizedPath = normalizePath(path); - fs.accessSync(normalizedPath, fs.constants.R_OK); // throws in case of an error - - return normalizedPath; - } -} - -export function realpath(path: string, callback: (error: Error | null, realpath: string) => void): void { - return fs.realpath(path, (error, realpath) => { - if (!error) { - return callback(null, realpath); - } - - // We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization - // we now do a similar normalization and then try again if we can access the path with read - // permissions at least. If that succeeds, we return that path. - // fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is - // to not resolve links but to simply see if the path is read accessible or not. - const normalizedPath = normalizePath(path); - - return fs.access(normalizedPath, fs.constants.R_OK, error => { - return callback(error, normalizedPath); - }); - }); -} - -function normalizePath(path: string): string { - return strings.rtrim(paths.normalize(path), paths.sep); -} - -export function watch(path: string, onChange: (type: string, path?: string) => void, onError: (error: string) => void): IDisposable { - try { - const watcher = fs.watch(path); - - watcher.on('change', (type, raw) => { - let file: string | undefined; - if (raw) { // https://github.com/Microsoft/vscode/issues/38191 - file = raw.toString(); - if (platform.isMacintosh) { - // Mac: uses NFD unicode form on disk, but we want NFC - // See also https://github.com/nodejs/node/issues/2165 - file = normalizeNFC(file); - } - } - - onChange(type, file); - }); - - watcher.on('error', (code: number, signal: string) => onError(`Failed to watch ${path} for changes (${code}, ${signal})`)); - - return toDisposable(() => { - watcher.removeAllListeners(); - watcher.close(); - }); - } catch (error) { - fs.exists(path, exists => { - if (exists) { - onError(`Failed to watch ${path} for changes (${error.toString()})`); - } - }); - } - - return Disposable.None; -} - -export function sanitizeFilePath(candidate: string, cwd: string): string { - - // Special case: allow to open a drive letter without trailing backslash - if (platform.isWindows && strings.endsWith(candidate, ':')) { - candidate += paths.sep; - } - - // Ensure absolute - if (!paths.isAbsolute(candidate)) { - candidate = paths.join(cwd, candidate); - } - - // Ensure normalized - candidate = paths.normalize(candidate); - - // Ensure no trailing slash/backslash - if (platform.isWindows) { - candidate = strings.rtrim(candidate, paths.sep); - - // Special case: allow to open drive root ('C:\') - if (strings.endsWith(candidate, ':')) { - candidate += paths.sep; - } - - } else { - candidate = strings.rtrim(candidate, paths.sep); - - // Special case: allow to open root ('/') - if (!candidate) { - candidate = paths.sep; - } - } - - return candidate; -} \ No newline at end of file diff --git a/src/vs/base/node/extpath.ts b/src/vs/base/node/extpath.ts new file mode 100644 index 0000000000..b3b55b7aae --- /dev/null +++ b/src/vs/base/node/extpath.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import { rtrim } from 'vs/base/common/strings'; +import { sep, join, normalize, dirname, basename } from 'vs/base/common/path'; +import { readdirSync } from 'vs/base/node/pfs'; +import { promisify } from 'util'; + +/** + * Copied from: https://github.com/Microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83 + * + * Given an absolute, normalized, and existing file path 'realcase' returns the exact path that the file has on disk. + * On a case insensitive file system, the returned path might differ from the original path by character casing. + * On a case sensitive file system, the returned path will always be identical to the original path. + * In case of errors, null is returned. But you cannot use this function to verify that a path exists. + * realcaseSync does not handle '..' or '.' path segments and it does not take the locale into account. + */ +export function realcaseSync(path: string): string | null { + const dir = dirname(path); + if (path === dir) { // end recursion + return path; + } + + const name = (basename(path) /* can be '' for windows drive letters */ || path).toLowerCase(); + try { + const entries = readdirSync(dir); + const found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search + if (found.length === 1) { + // on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition + const prefix = realcaseSync(dir); // recurse + if (prefix) { + return join(prefix, found[0]); + } + } else if (found.length > 1) { + // must be a case sensitive $filesystem + const ix = found.indexOf(name); + if (ix >= 0) { // case sensitive + const prefix = realcaseSync(dir); // recurse + if (prefix) { + return join(prefix, found[ix]); + } + } + } + } catch (error) { + // silently ignore error + } + + return null; +} + +export async function realpath(path: string): Promise { + try { + return await promisify(fs.realpath)(path); + } catch (error) { + + // We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization + // we now do a similar normalization and then try again if we can access the path with read + // permissions at least. If that succeeds, we return that path. + // fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is + // to not resolve links but to simply see if the path is read accessible or not. + const normalizedPath = normalizePath(path); + + await promisify(fs.access)(normalizedPath, fs.constants.R_OK); + + return normalizedPath; + } +} + +export function realpathSync(path: string): string { + try { + return fs.realpathSync(path); + } catch (error) { + + // We hit an error calling fs.realpathSync(). Since fs.realpathSync() is doing some path normalization + // we now do a similar normalization and then try again if we can access the path with read + // permissions at least. If that succeeds, we return that path. + // fs.realpath() is resolving symlinks and that can fail in certain cases. The workaround is + // to not resolve links but to simply see if the path is read accessible or not. + const normalizedPath = normalizePath(path); + fs.accessSync(normalizedPath, fs.constants.R_OK); // throws in case of an error + + return normalizedPath; + } +} + +function normalizePath(path: string): string { + return rtrim(normalize(path), sep); +} \ No newline at end of file diff --git a/src/vs/base/node/flow.ts b/src/vs/base/node/flow.ts deleted file mode 100644 index e16f98631b..0000000000 --- a/src/vs/base/node/flow.ts +++ /dev/null @@ -1,187 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; - -/** - * Executes the given function (fn) over the given array of items (list) in parallel and returns the resulting errors and results as - * array to the callback (callback). The resulting errors and results are evaluated by calling the provided callback function. - */ -export function parallel(list: T[], fn: (item: T, callback: (err: Error | null, result: E | null) => void) => void, callback: (err: Array | null, result: E[]) => void): void { - const results = new Array(list.length); - const errors = new Array(list.length); - let didErrorOccur = false; - let doneCount = 0; - - if (list.length === 0) { - return callback(null, []); - } - - list.forEach((item, index) => { - fn(item, (error, result) => { - if (error) { - didErrorOccur = true; - results[index] = null; - errors[index] = error; - } else { - results[index] = result; - errors[index] = null; - } - - if (++doneCount === list.length) { - return callback(didErrorOccur ? errors : null, results); - } - }); - }); -} - -/** - * Executes the given function (fn) over the given array of items (param) in sequential order and returns the first occurred error or the result as - * array to the callback (callback). The resulting errors and results are evaluated by calling the provided callback function. The first param can - * either be a function that returns an array of results to loop in async fashion or be an array of items already. - */ -export function loop(param: (callback: (error: Error, result: T[]) => void) => void, fn: (item: T, callback: (error: Error | null, result: E | null) => void, index: number, total: number) => void, callback: (error: Error | null, result: E[] | null) => void): void; -export function loop(param: T[], fn: (item: T, callback: (error: Error | null, result: E | null) => void, index: number, total: number) => void, callback: (error: Error | null, result: E[] | null) => void): void; -export function loop(param: any, fn: (item: any, callback: (error: Error | null, result: E | null) => void, index: number, total: number) => void, callback: (error: Error | null, result: E[] | null) => void): void { - - // Assert - assert.ok(param, 'Missing first parameter'); - assert.ok(typeof (fn) === 'function', 'Second parameter must be a function that is called for each element'); - assert.ok(typeof (callback) === 'function', 'Third parameter must be a function that is called on error and success'); - - // Param is function, execute to retrieve array - if (typeof (param) === 'function') { - try { - param((error: Error, result: E[]) => { - if (error) { - callback(error, null); - } else { - loop(result, fn, callback); - } - }); - } catch (error) { - callback(error, null); - } - } - - // Expect the param to be an array and loop over it - else { - const results: E[] = []; - - const looper: (i: number) => void = function (i: number): void { - - // Still work to do - if (i < param.length) { - - // Execute function on array element - try { - fn(param[i], (error: any, result: E) => { - - // A method might only send a boolean value as return value (e.g. fs.exists), support this case gracefully - if (error === true || error === false) { - result = error; - error = null; - } - - // Quit looping on error - if (error) { - callback(error, null); - } - - // Otherwise push result on stack and continue looping - else { - if (result) { //Could be that provided function is not returning a result - results.push(result); - } - - process.nextTick(() => { - looper(i + 1); - }); - } - }, i, param.length); - } catch (error) { - callback(error, null); - } - } - - // Done looping, pass back results too callback function - else { - callback(null, results); - } - }; - - // Start looping with first element in array - looper(0); - } -} - -function Sequence(sequences: { (...param: any[]): void; }[]): void { - - // Assert - assert.ok(sequences.length > 1, 'Need at least one error handler and one function to process sequence'); - sequences.forEach((sequence) => { - assert.ok(typeof (sequence) === 'function'); - }); - - // Execute in Loop - const errorHandler = sequences.splice(0, 1)[0]; //Remove error handler - let sequenceResult: any = null; - - loop(sequences, (sequence, clb) => { - const sequenceFunction = function (error: any, result: any): void { - - // A method might only send a boolean value as return value (e.g. fs.exists), support this case gracefully - if (error === true || error === false) { - result = error; - error = null; - } - - // Handle Error and Result - if (error) { - clb(error, null); - } else { - sequenceResult = result; //Remember result of sequence - clb(null, null); //Don't pass on result to Looper as we are not aggregating it - } - }; - - // We call the sequence function setting "this" to be the callback we define here - // and we pass in the "sequenceResult" as first argument. Doing all this avoids having - // to pass in a callback to the sequence because the callback is already "this". - try { - sequence.call(sequenceFunction, sequenceResult); - } catch (error) { - clb(error, null); - } - }, (error, result) => { - if (error) { - errorHandler(error); - } - }); -} - -/** - * Takes a variable list of functions to execute in sequence. The first function must be the error handler and the - * following functions can do arbitrary work. "this" must be used as callback value for async functions to continue - * through the sequence: - * sequence( - * function errorHandler(error) { - * clb(error, null); - * }, - * - * function doSomethingAsync() { - * fs.doAsync(path, this); - * }, - * - * function done(result) { - * clb(null, result); - * } - * ); - */ -export function sequence(errorHandler: (error: Error) => void, ...sequences: Function[]): void; -export function sequence(sequences: Function[]): void; -export function sequence(sequences: any): void { - Sequence((Array.isArray(sequences)) ? sequences : Array.prototype.slice.call(arguments)); -} \ No newline at end of file diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index fc176f9d95..40d63048d1 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -3,68 +3,200 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as extfs from 'vs/base/node/extfs'; -import { join } from 'vs/base/common/path'; -import { nfcall, Queue } from 'vs/base/common/async'; +import { join, dirname } from 'vs/base/common/path'; +import { Queue } from 'vs/base/common/async'; import * as fs from 'fs'; import * as os from 'os'; import * as platform from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; +import { endsWith } from 'vs/base/common/strings'; +import { promisify } from 'util'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { isRootOrDriveLetter } from 'vs/base/common/extpath'; +import { generateUuid } from 'vs/base/common/uuid'; +import { normalizeNFC } from 'vs/base/common/normalization'; +import { encode, encodeStream } from 'vs/base/node/encoding'; -export function readdir(path: string): Promise { - return nfcall(extfs.readdir, path); +export enum RimRafMode { + + /** + * Slow version that unlinks each file and folder. + */ + UNLINK, + + /** + * Fast version that first moves the file/folder + * into a temp directory and then deletes that + * without waiting for it. + */ + MOVE +} + +export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { + if (isRootOrDriveLetter(path)) { + throw new Error('rimraf - will refuse to recursively delete root'); + } + + // delete: via unlink + if (mode === RimRafMode.UNLINK) { + return rimrafUnlink(path); + } + + // delete: via move + return rimrafMove(path); +} + +async function rimrafUnlink(path: string): Promise { + try { + const stat = await lstat(path); + + // Folder delete (recursive) - NOT for symbolic links though! + if (stat.isDirectory() && !stat.isSymbolicLink()) { + + // Children + const children = await readdir(path); + await Promise.all(children.map(child => rimrafUnlink(join(path, child)))); + + // Folder + await promisify(fs.rmdir)(path); + } + + // Single file delete + else { + + // chmod as needed to allow for unlink + const mode = stat.mode; + if (!(mode & 128)) { // 128 === 0200 + await chmod(path, mode | 128); + } + + return unlink(path); + } + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } +} + +async function rimrafMove(path: string): Promise { + try { + const pathInTemp = join(os.tmpdir(), generateUuid()); + try { + await rename(path, pathInTemp); + } catch (error) { + return rimrafUnlink(path); // if rename fails, delete without tmp dir + } + + // Delete but do not return as promise + rimrafUnlink(pathInTemp); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } +} + +export function rimrafSync(path: string): void { + if (isRootOrDriveLetter(path)) { + throw new Error('rimraf - will refuse to recursively delete root'); + } + + try { + const stat = fs.lstatSync(path); + + // Folder delete (recursive) - NOT for symbolic links though! + if (stat.isDirectory() && !stat.isSymbolicLink()) { + + // Children + const children = readdirSync(path); + children.map(child => rimrafSync(join(path, child))); + + // Folder + fs.rmdirSync(path); + } + + // Single file delete + else { + + // chmod as needed to allow for unlink + const mode = stat.mode; + if (!(mode & 128)) { // 128 === 0200 + fs.chmodSync(path, mode | 128); + } + + return fs.unlinkSync(path); + } + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } +} + +export async function readdir(path: string): Promise { + return handleDirectoryChildren(await promisify(fs.readdir)(path)); +} + +export function readdirSync(path: string): string[] { + return handleDirectoryChildren(fs.readdirSync(path)); +} + +function handleDirectoryChildren(children: string[]): string[] { + // Mac: uses NFD unicode form on disk, but we want NFC + // See also https://github.com/nodejs/node/issues/2165 + if (platform.isMacintosh) { + return children.map(child => normalizeNFC(child)); + } + + return children; } export function exists(path: string): Promise { - return new Promise(c => fs.exists(path, c)); + return promisify(fs.exists)(path); } -export function chmod(path: string, mode: number): Promise { - return nfcall(fs.chmod, path, mode); -} - -export import mkdirp = extfs.mkdirp; - -export function rimraf(path: string): Promise { - return lstat(path).then(stat => { - if (stat.isDirectory() && !stat.isSymbolicLink()) { - return readdir(path) - .then(children => Promise.all(children.map(child => rimraf(join(path, child))))) - .then(() => rmdir(path)); - } else { - return unlink(path); - } - }, (err: NodeJS.ErrnoException) => { - if (err.code === 'ENOENT') { - return undefined; - } - - return Promise.reject(err); - }); -} - -export function realpath(path: string): Promise { - return nfcall(extfs.realpath, path); +export function chmod(path: string, mode: number): Promise { + return promisify(fs.chmod)(path, mode); } export function stat(path: string): Promise { - return nfcall(fs.stat, path); + return promisify(fs.stat)(path); } -export function statLink(path: string): Promise<{ stat: fs.Stats, isSymbolicLink: boolean }> { - return nfcall(extfs.statLink, path); +export interface IStatAndLink { + stat: fs.Stats; + isSymbolicLink: boolean; +} + +export async function statLink(path: string): Promise { + + // First stat the link + let linkStat: fs.Stats | undefined; + let linkStatError: NodeJS.ErrnoException | undefined; + try { + linkStat = await lstat(path); + } catch (error) { + linkStatError = error; + } + + // Then stat the target and return that + const isLink = !!(linkStat && linkStat.isSymbolicLink()); + if (linkStatError || isLink) { + const fileStat = await stat(path); + + return { stat: fileStat, isSymbolicLink: isLink }; + } + + return { stat: linkStat!, isSymbolicLink: false }; } export function lstat(path: string): Promise { - return nfcall(fs.lstat, path); -} - -export function move(oldPath: string, newPath: string): Promise { - return nfcall(extfs.mv, oldPath, newPath); + return promisify(fs.lstat)(path); } export function rename(oldPath: string, newPath: string): Promise { - return nfcall(fs.rename, oldPath, newPath); + return promisify(fs.rename)(oldPath, newPath); } export function renameIgnoreError(oldPath: string, newPath: string): Promise { @@ -73,30 +205,26 @@ export function renameIgnoreError(oldPath: string, newPath: string): Promise { - return nfcall(fs.rmdir, path); -} - export function unlink(path: string): Promise { - return nfcall(fs.unlink, path); + return promisify(fs.unlink)(path); } export function symlink(target: string, path: string, type?: string): Promise { - return nfcall(fs.symlink, target, path, type); + return promisify(fs.symlink)(target, path, type); } export function readlink(path: string): Promise { - return nfcall(fs.readlink, path); + return promisify(fs.readlink)(path); } export function truncate(path: string, len: number): Promise { - return nfcall(fs.truncate, path, len); + return promisify(fs.truncate)(path, len); } export function readFile(path: string): Promise; export function readFile(path: string, encoding: string): Promise; export function readFile(path: string, encoding?: string): Promise { - return nfcall(fs.readFile, path, encoding); + return promisify(fs.readFile)(path, encoding); } // According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback) @@ -104,15 +232,15 @@ export function readFile(path: string, encoding?: string): Promise } = Object.create(null); -export function writeFile(path: string, data: string, options?: extfs.IWriteFileOptions): Promise; -export function writeFile(path: string, data: Buffer, options?: extfs.IWriteFileOptions): Promise; -export function writeFile(path: string, data: Uint8Array, options?: extfs.IWriteFileOptions): Promise; -export function writeFile(path: string, data: NodeJS.ReadableStream, options?: extfs.IWriteFileOptions): Promise; -export function writeFile(path: string, data: any, options?: extfs.IWriteFileOptions): Promise; -export function writeFile(path: string, data: any, options?: extfs.IWriteFileOptions): any { +export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise; +export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise; +export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise; +export function writeFile(path: string, data: NodeJS.ReadableStream, options?: IWriteFileOptions): Promise; +export function writeFile(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options?: IWriteFileOptions): Promise; +export function writeFile(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options?: IWriteFileOptions): Promise { const queueKey = toQueueKey(path); - return ensureWriteFileQueue(queueKey).queue(() => nfcall(extfs.writeFileAndFlush, path, data, options)); + return ensureWriteFileQueue(queueKey).queue(() => writeFileAndFlush(path, data, options)); } function toQueueKey(path: string): string { @@ -140,43 +268,235 @@ function ensureWriteFileQueue(queueKey: string): Queue { return writeFileQueue; } -/** -* Read a dir and return only subfolders -*/ -export function readDirsInDir(dirPath: string): Promise { - return readdir(dirPath).then(children => { - return Promise.all(children.map(c => dirExists(join(dirPath, c)))).then(exists => { - return children.filter((_, i) => exists[i]); +export interface IWriteFileOptions { + mode?: number; + flag?: string; + encoding?: { + charset: string; + addBOM: boolean; + }; +} + +interface IEnsuredWriteFileOptions extends IWriteFileOptions { + mode: number; + flag: string; +} + +let canFlush = true; +function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options: IWriteFileOptions | undefined): Promise { + const ensuredOptions = ensureWriteOptions(options); + + return new Promise((resolve, reject) => { + if (typeof data === 'string' || Buffer.isBuffer(data) || data instanceof Uint8Array) { + doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve()); + } else { + doWriteFileStreamAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve()); + } + }); +} + +function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: IEnsuredWriteFileOptions, callback: (error?: Error) => void): void { + + // finish only once + let finished = false; + const finish = (error?: Error) => { + if (!finished) { + finished = true; + + // in error cases we need to manually close streams + // if the write stream was successfully opened + if (error) { + if (isOpen) { + writer.once('close', () => callback(error)); + writer.destroy(); + } else { + callback(error); + } + } + + // otherwise just return without error + else { + callback(); + } + } + }; + + // create writer to target. we set autoClose: false because we want to use the streams + // file descriptor to call fs.fdatasync to ensure the data is flushed to disk + const writer = fs.createWriteStream(path, { mode: options.mode, flags: options.flag, autoClose: false }); + + // Event: 'open' + // Purpose: save the fd for later use and start piping + // Notes: will not be called when there is an error opening the file descriptor! + let fd: number; + let isOpen: boolean; + writer.once('open', descriptor => { + fd = descriptor; + isOpen = true; + + // if an encoding is provided, we need to pipe the stream through + // an encoder stream and forward the encoding related options + if (options.encoding) { + reader = reader.pipe(encodeStream(options.encoding.charset, { addBOM: options.encoding.addBOM })); + } + + // start data piping only when we got a successful open. this ensures that we do + // not consume the stream when an error happens and helps to fix this issue: + // https://github.com/Microsoft/vscode/issues/42542 + reader.pipe(writer); + }); + + // Event: 'error' + // Purpose: to return the error to the outside and to close the write stream (does not happen automatically) + reader.once('error', error => finish(error)); + writer.once('error', error => finish(error)); + + // Event: 'finish' + // Purpose: use fs.fdatasync to flush the contents to disk + // Notes: event is called when the writer has finished writing to the underlying resource. we must call writer.close() + // because we have created the WriteStream with autoClose: false + writer.once('finish', () => { + + // flush to disk + if (canFlush && isOpen) { + fs.fdatasync(fd, (syncError: Error) => { + + // In some exotic setups it is well possible that node fails to sync + // In that case we disable flushing and warn to the console + if (syncError) { + console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError); + canFlush = false; + } + + writer.destroy(); + }); + } else { + writer.destroy(); + } + }); + + // Event: 'close' + // Purpose: signal we are done to the outside + // Notes: event is called when the writer's filedescriptor is closed + writer.once('close', () => finish()); +} + +// Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk +// We do this in cases where we want to make sure the data is really on disk and +// not in some cache. +// +// See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194 +function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, options: IEnsuredWriteFileOptions, callback: (error?: Error) => void): void { + if (options.encoding) { + data = encode(data instanceof Uint8Array ? Buffer.from(data) : data, options.encoding.charset, { addBOM: options.encoding.addBOM }); + } + + if (!canFlush) { + return fs.writeFile(path, data, { mode: options.mode, flag: options.flag }, callback); + } + + // Open the file with same flags and mode as fs.writeFile() + fs.open(path, options.flag, options.mode, (openError, fd) => { + if (openError) { + return callback(openError); + } + + // It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open! + fs.writeFile(fd, data, writeError => { + if (writeError) { + return fs.close(fd, () => callback(writeError)); // still need to close the handle on error! + } + + // Flush contents (not metadata) of the file to disk + fs.fdatasync(fd, (syncError: Error) => { + + // In some exotic setups it is well possible that node fails to sync + // In that case we disable flushing and warn to the console + if (syncError) { + console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError); + canFlush = false; + } + + return fs.close(fd, closeError => callback(closeError)); + }); }); }); } -/** -* `path` exists and is a directory -*/ -export function dirExists(path: string): Promise { - return stat(path).then(stat => stat.isDirectory(), () => false); -} +export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void { + const ensuredOptions = ensureWriteOptions(options); -/** -* `path` exists and is a file. -*/ -export function fileExists(path: string): Promise { - return stat(path).then(stat => stat.isFile(), () => false); -} - -/** - * Deletes a path from disk. - */ -let _tmpDir: string | null = null; -function getTmpDir(): string { - if (!_tmpDir) { - _tmpDir = os.tmpdir(); + if (ensuredOptions.encoding) { + data = encode(data, ensuredOptions.encoding.charset, { addBOM: ensuredOptions.encoding.addBOM }); + } + + if (!canFlush) { + return fs.writeFileSync(path, data, { mode: ensuredOptions.mode, flag: ensuredOptions.flag }); + } + + // Open the file with same flags and mode as fs.writeFile() + const fd = fs.openSync(path, ensuredOptions.flag, ensuredOptions.mode); + + try { + + // It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open! + fs.writeFileSync(fd, data); + + // Flush contents (not metadata) of the file to disk + try { + fs.fdatasyncSync(fd); + } catch (syncError) { + console.warn('[node.js fs] fdatasyncSync is now disabled for this session because it failed: ', syncError); + canFlush = false; + } + } finally { + fs.closeSync(fd); } - return _tmpDir; } -export function del(path: string, tmp = getTmpDir()): Promise { - return nfcall(extfs.del, path, tmp); + +function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptions { + if (!options) { + return { mode: 0o666, flag: 'w' }; + } + + return { + mode: typeof options.mode === 'number' ? options.mode : 0o666, + flag: typeof options.flag === 'string' ? options.flag : 'w', + encoding: options.encoding + }; +} + +export async function readDirsInDir(dirPath: string): Promise { + const children = await readdir(dirPath); + const directories: string[] = []; + + for (const child of children) { + if (await dirExists(join(dirPath, child))) { + directories.push(child); + } + } + + return directories; +} + +export async function dirExists(path: string): Promise { + try { + const fileStat = await stat(path); + + return fileStat.isDirectory(); + } catch (error) { + return false; + } +} + +export async function fileExists(path: string): Promise { + try { + const fileStat = await stat(path); + + return fileStat.isFile(); + } catch (error) { + return false; + } } export function whenDeleted(path: string): Promise { @@ -200,6 +520,154 @@ export function whenDeleted(path: string): Promise { }); } -export function copy(source: string, target: string): Promise { - return nfcall(extfs.copy, source, target); +export async function move(source: string, target: string): Promise { + if (source === target) { + return Promise.resolve(); + } + + async function updateMtime(path: string): Promise { + const stat = await lstat(path); + if (stat.isDirectory() || stat.isSymbolicLink()) { + return Promise.resolve(); // only for files + } + + const fd = await promisify(fs.open)(path, 'a'); + try { + await promisify(fs.futimes)(fd, stat.atime, new Date()); + } catch (error) { + //ignore + } + + return promisify(fs.close)(fd); + } + + try { + await rename(source, target); + await updateMtime(target); + } catch (error) { + + // In two cases we fallback to classic copy and delete: + // + // 1.) The EXDEV error indicates that source and target are on different devices + // In this case, fallback to using a copy() operation as there is no way to + // rename() between different devices. + // + // 2.) The user tries to rename a file/folder that ends with a dot. This is not + // really possible to move then, at least on UNC devices. + if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || endsWith(source, '.')) { + await copy(source, target); + await rimraf(source, RimRafMode.MOVE); + await updateMtime(target); + } else { + throw error; + } + } } + +export async function copy(source: string, target: string, copiedSourcesIn?: { [path: string]: boolean }): Promise { + const copiedSources = copiedSourcesIn ? copiedSourcesIn : Object.create(null); + + const fileStat = await stat(source); + if (!fileStat.isDirectory()) { + return doCopyFile(source, target, fileStat.mode & 511); + } + + if (copiedSources[source]) { + return Promise.resolve(); // escape when there are cycles (can happen with symlinks) + } + + copiedSources[source] = true; // remember as copied + + // Create folder + await mkdirp(target, fileStat.mode & 511); + + // Copy each file recursively + const files = await readdir(source); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + await copy(join(source, file), join(target, file), copiedSources); + } +} + +async function doCopyFile(source: string, target: string, mode: number): Promise { + return new Promise((resolve, reject) => { + const reader = fs.createReadStream(source); + const writer = fs.createWriteStream(target, { mode }); + + let finished = false; + const finish = (error?: Error) => { + if (!finished) { + finished = true; + + // in error cases, pass to callback + if (error) { + return reject(error); + } + + // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 + fs.chmod(target, mode, error => error ? reject(error) : resolve()); + } + }; + + // handle errors properly + reader.once('error', error => finish(error)); + writer.once('error', error => finish(error)); + + // we are done (underlying fd has been closed) + writer.once('close', () => finish()); + + // start piping + reader.pipe(writer); + }); +} + +export async function mkdirp(path: string, mode?: number, token?: CancellationToken): Promise { + const mkdir = async () => { + try { + await promisify(fs.mkdir)(path, mode); + } catch (error) { + + // ENOENT: a parent folder does not exist yet + if (error.code === 'ENOENT') { + return Promise.reject(error); + } + + // Any other error: check if folder exists and + // return normally in that case if its a folder + try { + const fileStat = await stat(path); + if (!fileStat.isDirectory()) { + return Promise.reject(new Error(`'${path}' exists and is not a directory.`)); + } + } catch (statError) { + throw error; // rethrow original error + } + } + }; + + // stop at root + if (path === dirname(path)) { + return Promise.resolve(); + } + + try { + await mkdir(); + } catch (error) { + + // Respect cancellation + if (token && token.isCancellationRequested) { + return Promise.resolve(); + } + + // ENOENT: a parent folder does not exist yet, continue + // to create the parent folder and then try again. + if (error.code === 'ENOENT') { + await mkdirp(dirname(path), mode); + + return mkdir(); + } + + // Any other error + return Promise.reject(error); + } +} \ No newline at end of file diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index fe843a0129..934cb1cc90 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -4,20 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { exec } from 'child_process'; - +import { ProcessItem } from 'vs/base/common/processes'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -export interface ProcessItem { - name: string; - cmd: string; - pid: number; - ppid: number; - load: number; - mem: number; - - children?: ProcessItem[]; -} - export function listProcesses(rootPid: number): Promise { return new Promise((resolve, reject) => { @@ -181,7 +170,7 @@ export function listProcesses(rootPid: number): Promise { exec(CMD, { maxBuffer: 1000 * 1024, env: { LC_NUMERIC: 'en_US.UTF-8' } }, (err, stdout, stderr) => { if (err || stderr) { - reject(err || stderr.toString()); + reject(err || new Error(stderr.toString())); } else { const lines = stdout.toString().split('\n'); @@ -214,7 +203,7 @@ export function listProcesses(rootPid: number): Promise { exec(cmd, {}, (err, stdout, stderr) => { if (err || stderr) { - reject(err || stderr.toString()); + reject(err || new Error(stderr.toString())); } else { const cpuUsage = stdout.toString().split('\n'); for (let i = 0; i < pids.length; i++) { diff --git a/src/vs/base/node/storage.ts b/src/vs/base/node/storage.ts index 9b40e5da6a..3b12b4c8f4 100644 --- a/src/vs/base/node/storage.ts +++ b/src/vs/base/node/storage.ts @@ -64,7 +64,7 @@ export interface IStorage extends IDisposable { getNumber(key: string, fallbackValue: number): number; getNumber(key: string, fallbackValue?: number): number | undefined; - set(key: string, value: string | boolean | number): Promise; + set(key: string, value: string | boolean | number | undefined | null): Promise; delete(key: string): Promise; close(): Promise; @@ -152,7 +152,7 @@ export class Storage extends Disposable implements IStorage { return this.cache.size; } - init(): Promise { + async init(): Promise { if (this.state !== StorageState.None) { return Promise.resolve(); // either closed or already initialized } @@ -166,9 +166,7 @@ export class Storage extends Disposable implements IStorage { return Promise.resolve(); } - return this.database.getItems().then(items => { - this.cache = items; - }); + this.cache = await this.database.getItems(); } get(key: string, fallbackValue: string): string; @@ -207,7 +205,7 @@ export class Storage extends Disposable implements IStorage { return parseInt(value, 10); } - set(key: string, value: string | boolean | number): Promise { + set(key: string, value: string | boolean | number | null | undefined): Promise { if (this.state === StorageState.Closed) { return Promise.resolve(); // Return early if we are already closed } @@ -262,7 +260,7 @@ export class Storage extends Disposable implements IStorage { return this.flushDelayer.trigger(() => this.flushPending()); } - close(): Promise { + async close(): Promise { if (this.state === StorageState.Closed) { return Promise.resolve(); // return if already closed } @@ -276,8 +274,13 @@ export class Storage extends Disposable implements IStorage { // // Recovery: we pass our cache over as recovery option in case // the DB is not healthy. - const onDone = () => this.database.close(() => this.cache); - return this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */).then(onDone, onDone); + try { + await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */); + } catch (error) { + // Ignore + } + + await this.database.close(() => this.cache); } private flushPending(): Promise { @@ -344,24 +347,25 @@ export class SQLiteStorageDatabase implements IStorageDatabase { this.whenConnected = this.connect(path); } - getItems(): Promise> { - return this.whenConnected.then(connection => { - const items = new Map(); + async getItems(): Promise> { + const connection = await this.whenConnected; - return this.all(connection, 'SELECT * FROM ItemTable').then(rows => { - rows.forEach(row => items.set(row.key, row.value)); + const items = new Map(); - if (this.logger.isTracing) { - this.logger.trace(`[storage ${this.name}] getItems(): ${items.size} rows`); - } + const rows = await this.all(connection, 'SELECT * FROM ItemTable'); + rows.forEach(row => items.set(row.key, row.value)); - return items; - }); - }); + if (this.logger.isTracing) { + this.logger.trace(`[storage ${this.name}] getItems(): ${items.size} rows`); + } + + return items; } - updateItems(request: IUpdateRequest): Promise { - return this.whenConnected.then(connection => this.doUpdateItems(connection, request)); + async updateItems(request: IUpdateRequest): Promise { + const connection = await this.whenConnected; + + return this.doUpdateItems(connection, request); } private doUpdateItems(connection: IDatabaseConnection, request: IUpdateRequest): Promise { @@ -452,10 +456,12 @@ export class SQLiteStorageDatabase implements IStorageDatabase { }); } - close(recovery?: () => Map): Promise { + async close(recovery?: () => Map): Promise { this.logger.trace(`[storage ${this.name}] close()`); - return this.whenConnected.then(connection => this.doClose(connection, recovery)); + const connection = await this.whenConnected; + + return this.doClose(connection, recovery); } private doClose(connection: IDatabaseConnection, recovery?: () => Map): Promise { @@ -529,24 +535,23 @@ export class SQLiteStorageDatabase implements IStorageDatabase { return `${path}.backup`; } - checkIntegrity(full: boolean): Promise { + async checkIntegrity(full: boolean): Promise { this.logger.trace(`[storage ${this.name}] checkIntegrity(full: ${full})`); - return this.whenConnected.then(connection => { - return this.get(connection, full ? 'PRAGMA integrity_check' : 'PRAGMA quick_check').then(row => { - const integrity = full ? row['integrity_check'] : row['quick_check']; + const connection = await this.whenConnected; + const row = await this.get(connection, full ? 'PRAGMA integrity_check' : 'PRAGMA quick_check'); - if (connection.isErroneous) { - return `${integrity} (last error: ${connection.lastError})`; - } + const integrity = full ? row['integrity_check'] : row['quick_check']; - if (connection.isInMemory) { - return `${integrity} (in-memory!)`; - } + if (connection.isErroneous) { + return `${integrity} (last error: ${connection.lastError})`; + } - return integrity; - }); - }); + if (connection.isInMemory) { + return `${integrity} (in-memory!)`; + } + + return integrity; } private connect(path: string, retryOnBusy: boolean = true): Promise { diff --git a/src/vs/base/node/watcher.ts b/src/vs/base/node/watcher.ts new file mode 100644 index 0000000000..b51f73c6ea --- /dev/null +++ b/src/vs/base/node/watcher.ts @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { join, basename } from 'vs/base/common/path'; +import { watch } from 'fs'; +import { isMacintosh } from 'vs/base/common/platform'; +import { normalizeNFC } from 'vs/base/common/normalization'; +import { toDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { exists, readdir } from 'vs/base/node/pfs'; + +export function watchFile(path: string, onChange: (type: 'changed' | 'deleted', path: string) => void, onError: (error: string) => void): IDisposable { + return doWatchNonRecursive({ path, isDirectory: false }, onChange, onError); +} + +export function watchFolder(path: string, onChange: (type: 'added' | 'changed' | 'deleted', path: string) => void, onError: (error: string) => void): IDisposable { + return doWatchNonRecursive({ path, isDirectory: true }, onChange, onError); +} + +export const CHANGE_BUFFER_DELAY = 100; + +function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onChange: (type: 'added' | 'changed' | 'deleted', path: string) => void, onError: (error: string) => void): IDisposable { + const originalFileName = basename(file.path); + const mapPathToStatDisposable = new Map(); + + let disposed = false; + let watcherDisposables: IDisposable[] = [toDisposable(() => { + mapPathToStatDisposable.forEach(disposable => dispose(disposable)); + mapPathToStatDisposable.clear(); + })]; + + try { + + // Creating watcher can fail with an exception + const watcher = watch(file.path); + watcherDisposables.push(toDisposable(() => { + watcher.removeAllListeners(); + watcher.close(); + })); + + // Folder: resolve children to emit proper events + const folderChildren: Set = new Set(); + if (file.isDirectory) { + readdir(file.path).then(children => children.forEach(child => folderChildren.add(child))); + } + + watcher.on('error', (code: number, signal: string) => { + if (!disposed) { + onError(`Failed to watch ${file.path} for changes using fs.watch() (${code}, ${signal})`); + } + }); + + watcher.on('change', (type, raw) => { + if (disposed) { + return; // ignore if already disposed + } + + // Normalize file name + let changedFileName: string = ''; + if (raw) { // https://github.com/Microsoft/vscode/issues/38191 + changedFileName = raw.toString(); + if (isMacintosh) { + // Mac: uses NFD unicode form on disk, but we want NFC + // See also https://github.com/nodejs/node/issues/2165 + changedFileName = normalizeNFC(changedFileName); + } + } + + if (!changedFileName || (type !== 'change' && type !== 'rename')) { + return; // ignore unexpected events + } + + // File path: use path directly for files and join with changed file name otherwise + const changedFilePath = file.isDirectory ? join(file.path, changedFileName) : file.path; + + // File + if (!file.isDirectory) { + if (type === 'rename' || changedFileName !== originalFileName) { + // The file was either deleted or renamed. Many tools apply changes to files in an + // atomic way ("Atomic Save") by first renaming the file to a temporary name and then + // renaming it back to the original name. Our watcher will detect this as a rename + // and then stops to work on Mac and Linux because the watcher is applied to the + // inode and not the name. The fix is to detect this case and trying to watch the file + // again after a certain delay. + // In addition, we send out a delete event if after a timeout we detect that the file + // does indeed not exist anymore. + + const timeoutHandle = setTimeout(async () => { + const fileExists = await exists(changedFilePath); + + if (disposed) { + return; // ignore if disposed by now + } + + // File still exists, so emit as change event and reapply the watcher + if (fileExists) { + onChange('changed', changedFilePath); + + watcherDisposables = [doWatchNonRecursive(file, onChange, onError)]; + } + + // File seems to be really gone, so emit a deleted event + else { + onChange('deleted', changedFilePath); + } + }, CHANGE_BUFFER_DELAY); + + // Very important to dispose the watcher which now points to a stale inode + // and wire in a new disposable that tracks our timeout that is installed + dispose(watcherDisposables); + watcherDisposables = [toDisposable(() => clearTimeout(timeoutHandle))]; + } else { + onChange('changed', changedFilePath); + } + } + + // Folder + else { + + // Children add/delete + if (type === 'rename') { + + // Cancel any previous stats for this file path if existing + const statDisposable = mapPathToStatDisposable.get(changedFilePath); + if (statDisposable) { + dispose(statDisposable); + } + + // Wait a bit and try see if the file still exists on disk to decide on the resulting event + const timeoutHandle = setTimeout(async () => { + mapPathToStatDisposable.delete(changedFilePath); + + const fileExists = await exists(changedFilePath); + + if (disposed) { + return; // ignore if disposed by now + } + + // Figure out the correct event type: + // File Exists: either 'added' or 'changed' if known before + // File Does not Exist: always 'deleted' + let type: 'added' | 'deleted' | 'changed'; + if (fileExists) { + if (folderChildren.has(changedFileName)) { + type = 'changed'; + } else { + type = 'added'; + folderChildren.add(changedFileName); + } + } else { + folderChildren.delete(changedFileName); + type = 'deleted'; + } + + onChange(type, changedFilePath); + }, CHANGE_BUFFER_DELAY); + + mapPathToStatDisposable.set(changedFilePath, toDisposable(() => clearTimeout(timeoutHandle))); + } + + // Other events + else { + + // Figure out the correct event type: if this is the + // first time we see this child, it can only be added + let type: 'added' | 'changed'; + if (folderChildren.has(changedFileName)) { + type = 'changed'; + } else { + type = 'added'; + folderChildren.add(changedFileName); + } + + onChange(type, changedFilePath); + } + } + }); + } catch (error) { + exists(file.path).then(exists => { + if (exists && !disposed) { + onError(`Failed to watch ${file.path} for changes using fs.watch() (${error.toString()})`); + } + }); + } + + return toDisposable(() => { + disposed = true; + + watcherDisposables = dispose(watcherDisposables); + }); +} \ No newline at end of file diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 4f2cd263b9..bc2360baa2 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import { createWriteStream, WriteStream } from 'fs'; import { Readable } from 'stream'; -import { nfcall, ninvoke, Sequencer, createCancelablePromise } from 'vs/base/common/async'; +import { Sequencer, createCancelablePromise } from 'vs/base/common/async'; import { mkdirp, rimraf } from 'vs/base/node/pfs'; import { open as _openZip, Entry, ZipFile } from 'yauzl'; import * as yazl from 'yazl'; @@ -17,7 +17,7 @@ import { Event } from 'vs/base/common/event'; export interface IExtractOptions { overwrite?: boolean; - /** + /** * Source path within the ZIP archive. Only the files contained in this * path will be extracted. */ @@ -153,7 +153,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok return; } - const stream = ninvoke(zipfile, zipfile.openReadStream, entry); + const stream = openZipStream(zipfile, entry); const mode = modeFromEntry(entry); last = createCancelablePromise(token => throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options, token).then(() => readNextEntry(token)))).then(null!, e)); @@ -162,8 +162,27 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok } function openZip(zipFile: string, lazy: boolean = false): Promise { - return nfcall(_openZip, zipFile, lazy ? { lazyEntries: true } : undefined) - .then(undefined, err => Promise.reject(toExtractError(err))); + return new Promise((resolve, reject) => { + _openZip(zipFile, lazy ? { lazyEntries: true } : undefined, (error?: Error, zipfile?: ZipFile) => { + if (error) { + reject(toExtractError(error)); + } else { + resolve(zipfile); + } + }); + }); +} + +function openZipStream(zipFile: ZipFile, entry: Entry): Promise { + return new Promise((resolve, reject) => { + zipFile.openReadStream(entry, (error?: Error, stream?: Readable) => { + if (error) { + reject(toExtractError(error)); + } else { + resolve(stream); + } + }); + }); } export interface IFile { @@ -210,7 +229,7 @@ function read(zipPath: string, filePath: string): Promise { return new Promise((c, e) => { zipfile.on('entry', (entry: Entry) => { if (entry.fileName === filePath) { - ninvoke(zipfile, zipfile.openReadStream, entry).then(stream => c(stream), err => e(err)); + openZipStream(zipfile, entry).then(stream => c(stream), err => e(err)); } }); @@ -224,7 +243,7 @@ export function buffer(zipPath: string, filePath: string): Promise { return new Promise((c, e) => { const buffers: Buffer[] = []; stream.once('error', e); - stream.on('data', b => buffers.push(b as Buffer)); + stream.on('data', (b: Buffer) => buffers.push(b)); stream.on('end', () => c(Buffer.concat(buffers))); }); }); diff --git a/src/vs/base/parts/contextmenu/electron-browser/contextmenu.ts b/src/vs/base/parts/contextmenu/electron-browser/contextmenu.ts index c6e8d26149..7afb10ca21 100644 --- a/src/vs/base/parts/contextmenu/electron-browser/contextmenu.ts +++ b/src/vs/base/parts/contextmenu/electron-browser/contextmenu.ts @@ -37,7 +37,7 @@ export function popup(items: IContextMenuItem[], options?: IPopupOptions): void } function createItem(item: IContextMenuItem, processedItems: IContextMenuItem[]): ISerializableContextMenuItem { - const serializableItem = { + const serializableItem: ISerializableContextMenuItem = { id: processedItems.length, label: item.label, type: item.type, @@ -45,7 +45,7 @@ function createItem(item: IContextMenuItem, processedItems: IContextMenuItem[]): checked: item.checked, enabled: typeof item.enabled === 'boolean' ? item.enabled : true, visible: typeof item.visible === 'boolean' ? item.visible : true - } as ISerializableContextMenuItem; + }; processedItems.push(item); diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index 7fdf873004..5ef925cc69 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -17,6 +17,7 @@ export interface ISocket { onEnd(listener: () => void): IDisposable; write(buffer: VSBuffer): void; end(): void; + dispose(): void; } let emptyBuffer: VSBuffer | null = null; @@ -27,7 +28,7 @@ function getEmptyBuffer(): VSBuffer { return emptyBuffer; } -class ChunkStream { +export class ChunkStream { private _chunks: VSBuffer[]; private _totalLength: number; @@ -47,6 +48,15 @@ class ChunkStream { } public read(byteCount: number): VSBuffer { + return this._read(byteCount, true); + } + + public peek(byteCount: number): VSBuffer { + return this._read(byteCount, false); + } + + private _read(byteCount: number, advance: boolean): VSBuffer { + if (byteCount === 0) { return getEmptyBuffer(); } @@ -57,39 +67,53 @@ class ChunkStream { if (this._chunks[0].byteLength === byteCount) { // super fast path, precisely first chunk must be returned - const result = this._chunks.shift()!; - this._totalLength -= byteCount; + const result = this._chunks[0]; + if (advance) { + this._chunks.shift(); + this._totalLength -= byteCount; + } return result; } if (this._chunks[0].byteLength > byteCount) { // fast path, the reading is entirely within the first chunk const result = this._chunks[0].slice(0, byteCount); - this._chunks[0] = this._chunks[0].slice(byteCount); - this._totalLength -= byteCount; + if (advance) { + this._chunks[0] = this._chunks[0].slice(byteCount); + this._totalLength -= byteCount; + } return result; } let result = VSBuffer.alloc(byteCount); let resultOffset = 0; + let chunkIndex = 0; while (byteCount > 0) { - const chunk = this._chunks[0]; + const chunk = this._chunks[chunkIndex]; if (chunk.byteLength > byteCount) { // this chunk will survive - this._chunks[0] = chunk.slice(byteCount); - const chunkPart = chunk.slice(0, byteCount); result.set(chunkPart, resultOffset); resultOffset += byteCount; - this._totalLength -= byteCount; + + if (advance) { + this._chunks[chunkIndex] = chunk.slice(byteCount); + this._totalLength -= byteCount; + } + byteCount -= byteCount; } else { // this chunk will be entirely read - this._chunks.shift(); - result.set(chunk, resultOffset); resultOffset += chunk.byteLength; - this._totalLength -= chunk.byteLength; + + if (advance) { + this._chunks.shift(); + this._totalLength -= chunk.byteLength; + } else { + chunkIndex++; + } + byteCount -= chunk.byteLength; } } @@ -154,7 +178,7 @@ class ProtocolReader extends Disposable { private readonly _incomingData: ChunkStream; public lastReadTime: number; - private readonly _onMessage = new Emitter(); + private readonly _onMessage = this._register(new Emitter()); public readonly onMessage: Event = this._onMessage.event; private readonly _state = { diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index 34fb02613f..5a9b6f76d4 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -20,6 +20,10 @@ export class NodeSocket implements ISocket { this.socket = socket; } + public dispose(): void { + this.socket.destroy(); + } + public onData(_listener: (e: VSBuffer) => void): IDisposable { const listener = (buff: Buffer) => _listener(VSBuffer.wrap(buff)); this.socket.on('data', listener); diff --git a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts index b97e428182..75e7c50847 100644 --- a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts +++ b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts @@ -473,17 +473,16 @@ class Renderer implements IRenderer { } disposeTemplate(templateId: string, templateData: IQuickOpenEntryGroupTemplateData): void { - const data = templateData as IQuickOpenEntryGroupTemplateData; - data.actionBar.dispose(); - data.actionBar = null!; - data.container = null!; - data.entry = null!; - data.keybinding = null!; - data.detail = null!; - data.group = null!; - data.icon = null!; - data.label.dispose(); - data.label = null!; + templateData.actionBar.dispose(); + templateData.actionBar = null!; + templateData.container = null!; + templateData.entry = null!; + templateData.keybinding = null!; + templateData.detail = null!; + templateData.group = null!; + templateData.icon = null!; + templateData.label.dispose(); + templateData.label = null!; } } diff --git a/src/vs/base/parts/tree/browser/tree.ts b/src/vs/base/parts/tree/browser/tree.ts index 453d98c62e..454f8d2d24 100644 --- a/src/vs/base/parts/tree/browser/tree.ts +++ b/src/vs/base/parts/tree/browser/tree.ts @@ -108,61 +108,17 @@ export interface ITree { */ toggleExpansion(element: any, recursive?: boolean): Promise; - /** - * Toggles several element's expansion state. - */ - toggleExpansionAll(elements: any[]): Promise; - /** * Returns whether an element is expanded or not. */ isExpanded(element: any): boolean; - /** - * Returns a list of the currently expanded elements. - */ - getExpandedElements(): any[]; - /** * Reveals an element in the tree. The relativeTop is a value between 0 and 1. The closer to 0 the more the * element will scroll up to the top. */ reveal(element: any, relativeTop?: number): Promise; - /** - * Returns the relative top position of any given element, if visible. - * If not visible, returns a negative number or a number > 1. - * Useful when calling `reveal(element, relativeTop)`. - */ - getRelativeTop(element: any): number; - - /** - * Returns the top-most visible element. - */ - getFirstVisibleElement(): any; - - /** - * Returns a number between 0 and 1 representing how much the tree is scroll down. 0 means all the way - * to the top; 1 means all the way down. - */ - getScrollPosition(): number; - - /** - * Sets the scroll position with a number between 0 and 1 representing how much the tree is scroll down. 0 means all the way - * to the top; 1 means all the way down. - */ - setScrollPosition(pos: number): void; - - /** - * Returns the total height of the tree's content. - */ - getContentHeight(): number; - - /** - * Sets the tree's highlight to be the given element. - * Provide no arguments and it clears the tree's highlight. - */ - setHighlight(element?: any, eventPayload?: any): void; /** * Returns the currently highlighted element. diff --git a/src/vs/base/parts/tree/browser/treeImpl.ts b/src/vs/base/parts/tree/browser/treeImpl.ts index 817fca8b22..122010082f 100644 --- a/src/vs/base/parts/tree/browser/treeImpl.ts +++ b/src/vs/base/parts/tree/browser/treeImpl.ts @@ -181,33 +181,17 @@ export class Tree implements _.ITree { return this.model.toggleExpansion(element, recursive); } - public toggleExpansionAll(elements: any[]): Promise { - return this.model.toggleExpansionAll(elements); - } - public isExpanded(element: any): boolean { return this.model.isExpanded(element); } - public getExpandedElements(): any[] { - return this.model.getExpandedElements(); - } - public reveal(element: any, relativeTop: number | null = null): Promise { return this.model.reveal(element, relativeTop); } - public getRelativeTop(element: any): number { - const item = this.model.getItem(element); - return item ? this.view.getRelativeTop(item) : 0; - } - - public getFirstVisibleElement(): any { - return this.view.getFirstVisibleElement(); - } - - public getLastVisibleElement(): any { - return this.view.getLastVisibleElement(); + // {{SQL CARBON EDIT }} - add back deleted VS Code tree methods + public getExpandedElements(): any[] { + return this.model.getExpandedElements(); } public getScrollPosition(): number { @@ -221,10 +205,8 @@ export class Tree implements _.ITree { getContentHeight(): number { return this.view.getContentHeight(); } + // {{SQL CARBON EDIT }} - end block - public setHighlight(element?: any, eventPayload?: any): void { - this.model.setHighlight(element, eventPayload); - } public getHighlight(): any { return this.model.getHighlight(); diff --git a/src/vs/base/parts/tree/browser/treeModel.ts b/src/vs/base/parts/tree/browser/treeModel.ts index 9bb249181c..4c6515ab72 100644 --- a/src/vs/base/parts/tree/browser/treeModel.ts +++ b/src/vs/base/parts/tree/browser/treeModel.ts @@ -566,19 +566,6 @@ export class Item { return this.isAncestorOf(other) || other.isAncestorOf(this); } - public getHierarchy(): Item[] { - let result: Item[] = []; - let node: Item | null = this; - - do { - result.push(node); - node = node.parent; - } while (node); - - result.reverse(); - return result; - } - private isAncestorOf(startItem: Item): boolean { let item: Item | null = startItem; while (item) { diff --git a/src/vs/base/parts/tree/browser/treeUtils.ts b/src/vs/base/parts/tree/browser/treeUtils.ts index 82bcf9557d..44f05e1a3a 100644 --- a/src/vs/base/parts/tree/browser/treeUtils.ts +++ b/src/vs/base/parts/tree/browser/treeUtils.ts @@ -5,16 +5,6 @@ import * as _ from 'vs/base/parts/tree/browser/tree'; -export function collapseAll(tree: _.ITree, except?: any): void { - const nav = tree.getNavigator(); - let cur; - while (cur = nav.next()) { - if (!except || !isEqualOrParent(tree, except, cur)) { - tree.collapse(cur); - } - } -} - export function isEqualOrParent(tree: _.ITree, element: any, candidateParent: any): boolean { const nav = tree.getNavigator(element); @@ -26,11 +16,3 @@ export function isEqualOrParent(tree: _.ITree, element: any, candidateParent: an return false; } - -export function expandAll(tree: _.ITree): void { - const nav = tree.getNavigator(); - let cur; - while (cur = nav.next()) { - tree.expand(cur); - } -} diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index 7f1b7b8101..196165e2f1 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -658,27 +658,6 @@ export class TreeView extends HeightMap { } } - public getFirstVisibleElement(): any { - const firstIndex = this.indexAt(this.lastRenderTop); - let item = this.itemAtIndex(firstIndex); - if (!item) { - return item; - } - - const itemMidpoint = item.top + item.height / 2; - if (itemMidpoint < this.scrollTop) { - const nextItem = this.itemAtIndex(firstIndex + 1); - item = nextItem || item; - } - - return item.model.getElement(); - } - - public getLastVisibleElement(): any { - const item = this.itemAtIndex(this.indexAt(this.lastRenderTop + this.lastRenderHeight - 1)); - return item && item.model.getElement(); - } - private render(scrollTop: number, viewHeight: number, scrollLeft: number, viewWidth: number, scrollWidth: number): void { let i: number; let stop: number; @@ -1043,16 +1022,6 @@ export class TreeView extends HeightMap { } } - public getRelativeTop(item: Model.Item): number { - if (item && item.isVisible()) { - let viewItem = this.items[item.id]; - if (viewItem) { - return (viewItem.top - this.scrollTop) / (this.viewHeight - viewItem.height); - } - } - return -1; - } - private onItemReveal(e: Model.IItemRevealEvent): void { let item = e.item; let relativeTop = e.relativeTop; diff --git a/src/vs/base/test/browser/hash.test.ts b/src/vs/base/test/browser/hash.test.ts new file mode 100644 index 0000000000..adbedc411f --- /dev/null +++ b/src/vs/base/test/browser/hash.test.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 * as assert from 'assert'; +import { createSHA1 } from 'vs/base/browser/hash'; + +suite('Hash', () => { + test('computeSHA1Hash', async () => { + assert.equal(await createSHA1(''), 'da39a3ee5e6b4b0d3255bfef95601890afd80709'); + assert.equal(await createSHA1('hello world'), '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'); + assert.equal(await createSHA1('da39a3ee5e6b4b0d3255bfef95601890afd80709'), '10a34637ad661d98ba3344717656fcc76209c2f8'); + assert.equal(await createSHA1('2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'), 'd6b0d82cea4269b51572b8fab43adcee9fc3cf9a'); + assert.equal(await createSHA1('öäü_?ß()<>ÖÄÜ'), 'b64beaeff9e317b0193c8e40a2431b210388eba9'); + }); +}); \ No newline at end of file diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index 9160946a9b..333b4ed4e4 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { Iterator } from 'vs/base/common/iterator'; @@ -186,4 +186,42 @@ suite('ObjectTree', function () { assert.equal(navigator.last(), 2); }); }); + + test('traits are preserved according to string identity', function () { + const container = document.createElement('div'); + container.style.width = '200px'; + container.style.height = '200px'; + + const delegate = new class implements IListVirtualDelegate { + getHeight() { return 20; } + getTemplateId(): string { return 'default'; } + }; + + const renderer = new class implements ITreeRenderer { + readonly templateId = 'default'; + renderTemplate(container: HTMLElement): HTMLElement { + return container; + } + renderElement(element: ITreeNode, index: number, templateData: HTMLElement): void { + templateData.textContent = `${element.element}`; + } + disposeTemplate(): void { } + }; + + const identityProvider = new class implements IIdentityProvider { + getId(element: number): { toString(): string; } { + return `${element % 100}`; + } + }; + + const tree = new ObjectTree(container, delegate, [renderer], { identityProvider }); + tree.layout(200); + + tree.setChildren(null, [{ element: 0 }, { element: 1 }, { element: 2 }, { element: 3 }]); + tree.setFocus([1]); + assert.deepStrictEqual(tree.getFocus(), [1]); + + tree.setChildren(null, [{ element: 100 }, { element: 101 }, { element: 102 }, { element: 103 }]); + assert.deepStrictEqual(tree.getFocus(), [101]); + }); }); \ No newline at end of file diff --git a/src/vs/base/test/common/cancellation.test.ts b/src/vs/base/test/common/cancellation.test.ts index 36914fb46b..1998737415 100644 --- a/src/vs/base/test/common/cancellation.test.ts +++ b/src/vs/base/test/common/cancellation.test.ts @@ -94,4 +94,19 @@ suite('CancellationToken', function () { source.cancel(); assert.equal(count, 0); }); + + test('parent cancels child', function () { + + let parent = new CancellationTokenSource(); + let child = new CancellationTokenSource(parent.token); + + let count = 0; + child.token.onCancellationRequested(() => count += 1); + + parent.cancel(); + + assert.equal(count, 1); + assert.equal(child.token.isCancellationRequested, true); + assert.equal(parent.token.isCancellationRequested, true); + }); }); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index e22bb371e7..a475715729 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Event, Emitter, EventBufferer, EventMultiplexer, AsyncEmitter, IWaitUntil } from 'vs/base/common/event'; +import { Event, Emitter, EventBufferer, EventMultiplexer, AsyncEmitter, IWaitUntil, PauseableEmitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as Errors from 'vs/base/common/errors'; import { timeout } from 'vs/base/common/async'; @@ -331,6 +331,133 @@ suite('AsyncEmitter', function () { }); }); +suite('PausableEmitter', function () { + + test('basic', function () { + const data: number[] = []; + const emitter = new PauseableEmitter(); + + emitter.event(e => data.push(e)); + emitter.fire(1); + emitter.fire(2); + + assert.deepEqual(data, [1, 2]); + }); + + test('pause/resume - no merge', function () { + const data: number[] = []; + const emitter = new PauseableEmitter(); + + emitter.event(e => data.push(e)); + emitter.fire(1); + emitter.fire(2); + assert.deepEqual(data, [1, 2]); + + emitter.pause(); + emitter.fire(3); + emitter.fire(4); + assert.deepEqual(data, [1, 2]); + + emitter.resume(); + assert.deepEqual(data, [1, 2, 3, 4]); + emitter.fire(5); + assert.deepEqual(data, [1, 2, 3, 4, 5]); + }); + + test('pause/resume - merge', function () { + const data: number[] = []; + const emitter = new PauseableEmitter({ merge: (a) => a.reduce((p, c) => p + c, 0) }); + + emitter.event(e => data.push(e)); + emitter.fire(1); + emitter.fire(2); + assert.deepEqual(data, [1, 2]); + + emitter.pause(); + emitter.fire(3); + emitter.fire(4); + assert.deepEqual(data, [1, 2]); + + emitter.resume(); + assert.deepEqual(data, [1, 2, 7]); + + emitter.fire(5); + assert.deepEqual(data, [1, 2, 7, 5]); + }); + + test('double pause/resume', function () { + const data: number[] = []; + const emitter = new PauseableEmitter(); + + emitter.event(e => data.push(e)); + emitter.fire(1); + emitter.fire(2); + assert.deepEqual(data, [1, 2]); + + emitter.pause(); + emitter.pause(); + emitter.fire(3); + emitter.fire(4); + assert.deepEqual(data, [1, 2]); + + emitter.resume(); + assert.deepEqual(data, [1, 2]); + + emitter.resume(); + assert.deepEqual(data, [1, 2, 3, 4]); + + emitter.resume(); + assert.deepEqual(data, [1, 2, 3, 4]); + }); + + test('resume, no pause', function () { + const data: number[] = []; + const emitter = new PauseableEmitter(); + + emitter.event(e => data.push(e)); + emitter.fire(1); + emitter.fire(2); + assert.deepEqual(data, [1, 2]); + + emitter.resume(); + emitter.fire(3); + assert.deepEqual(data, [1, 2, 3]); + }); + + test('nested pause', function () { + const data: number[] = []; + const emitter = new PauseableEmitter(); + + let once = true; + emitter.event(e => { + data.push(e); + + if (once) { + emitter.pause(); + once = false; + } + }); + emitter.event(e => { + data.push(e); + }); + + emitter.pause(); + emitter.fire(1); + emitter.fire(2); + assert.deepEqual(data, []); + + emitter.resume(); + assert.deepEqual(data, [1, 1]); // paused after first event + + emitter.resume(); + assert.deepEqual(data, [1, 1, 2, 2]); // remaing event delivered + + emitter.fire(3); + assert.deepEqual(data, [1, 1, 2, 2, 3, 3]); + + }); +}); + suite('Event utils', () => { suite('EventBufferer', () => { @@ -777,4 +904,5 @@ suite('Event utils', () => { listener.dispose(); }); + }); diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index 5455959936..d8bea0acca 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -16,7 +16,6 @@ suite('Paths', () => { }); test('getRoot', () => { - assert.equal(extpath.getRoot('/user/far'), '/'); assert.equal(extpath.getRoot('\\\\server\\share\\some\\path'), '//server/share/'); assert.equal(extpath.getRoot('//server/share/some/path'), '//server/share/'); @@ -65,4 +64,54 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('tes"t.txt')); } }); + + test('sanitizeFilePath', () => { + if (platform.isWindows) { + assert.equal(extpath.sanitizeFilePath('.', 'C:\\the\\cwd'), 'C:\\the\\cwd'); + assert.equal(extpath.sanitizeFilePath('', 'C:\\the\\cwd'), 'C:\\the\\cwd'); + + assert.equal(extpath.sanitizeFilePath('C:', 'C:\\the\\cwd'), 'C:\\'); + assert.equal(extpath.sanitizeFilePath('C:\\', 'C:\\the\\cwd'), 'C:\\'); + assert.equal(extpath.sanitizeFilePath('C:\\\\', 'C:\\the\\cwd'), 'C:\\'); + + assert.equal(extpath.sanitizeFilePath('C:\\folder\\my.txt', 'C:\\the\\cwd'), 'C:\\folder\\my.txt'); + assert.equal(extpath.sanitizeFilePath('C:\\folder\\my', 'C:\\the\\cwd'), 'C:\\folder\\my'); + assert.equal(extpath.sanitizeFilePath('C:\\folder\\..\\my', 'C:\\the\\cwd'), 'C:\\my'); + assert.equal(extpath.sanitizeFilePath('C:\\folder\\my\\', 'C:\\the\\cwd'), 'C:\\folder\\my'); + assert.equal(extpath.sanitizeFilePath('C:\\folder\\my\\\\\\', 'C:\\the\\cwd'), 'C:\\folder\\my'); + + assert.equal(extpath.sanitizeFilePath('my.txt', 'C:\\the\\cwd'), 'C:\\the\\cwd\\my.txt'); + assert.equal(extpath.sanitizeFilePath('my.txt\\', 'C:\\the\\cwd'), 'C:\\the\\cwd\\my.txt'); + + assert.equal(extpath.sanitizeFilePath('\\\\localhost\\folder\\my', 'C:\\the\\cwd'), '\\\\localhost\\folder\\my'); + assert.equal(extpath.sanitizeFilePath('\\\\localhost\\folder\\my\\', 'C:\\the\\cwd'), '\\\\localhost\\folder\\my'); + } else { + assert.equal(extpath.sanitizeFilePath('.', '/the/cwd'), '/the/cwd'); + assert.equal(extpath.sanitizeFilePath('', '/the/cwd'), '/the/cwd'); + assert.equal(extpath.sanitizeFilePath('/', '/the/cwd'), '/'); + + assert.equal(extpath.sanitizeFilePath('/folder/my.txt', '/the/cwd'), '/folder/my.txt'); + assert.equal(extpath.sanitizeFilePath('/folder/my', '/the/cwd'), '/folder/my'); + assert.equal(extpath.sanitizeFilePath('/folder/../my', '/the/cwd'), '/my'); + assert.equal(extpath.sanitizeFilePath('/folder/my/', '/the/cwd'), '/folder/my'); + assert.equal(extpath.sanitizeFilePath('/folder/my///', '/the/cwd'), '/folder/my'); + + assert.equal(extpath.sanitizeFilePath('my.txt', '/the/cwd'), '/the/cwd/my.txt'); + assert.equal(extpath.sanitizeFilePath('my.txt/', '/the/cwd'), '/the/cwd/my.txt'); + } + }); + + test('isRoot', () => { + if (platform.isWindows) { + assert.ok(extpath.isRootOrDriveLetter('c:')); + assert.ok(extpath.isRootOrDriveLetter('D:')); + assert.ok(extpath.isRootOrDriveLetter('D:/')); + assert.ok(extpath.isRootOrDriveLetter('D:\\')); + assert.ok(!extpath.isRootOrDriveLetter('D:\\path')); + assert.ok(!extpath.isRootOrDriveLetter('D:/path')); + } else { + assert.ok(extpath.isRootOrDriveLetter('/')); + assert.ok(!extpath.isRootOrDriveLetter('/path')); + } + }); }); diff --git a/src/vs/base/test/common/linkedList.test.ts b/src/vs/base/test/common/linkedList.test.ts index efaa2ac3dd..cb24cee50d 100644 --- a/src/vs/base/test/common/linkedList.test.ts +++ b/src/vs/base/test/common/linkedList.test.ts @@ -52,6 +52,14 @@ suite('LinkedList', function () { disp = list.push(2); disp(); assertElements(list, 0, 1); + + list = new LinkedList(); + list.push(0); + list.push(1); + disp = list.push(2); + disp(); + disp(); + assertElements(list, 0, 1); }); test('Push/toArray', () => { diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index b243131f9e..ffc18da589 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -189,29 +189,4 @@ suite('Types', () => { assert.throws(() => types.validateConstraints(['2'], [types.isNumber])); assert.throws(() => types.validateConstraints([1, 'test', true], [Number, String, Number])); }); - - test('create', () => { - let zeroConstructor = function () { /**/ }; - - assert(types.create(zeroConstructor) instanceof zeroConstructor); - assert(types.isObject(types.create(zeroConstructor))); - - let manyArgConstructor = function (this: any, foo: any, bar: any) { - this.foo = foo; - this.bar = bar; - }; - - let foo = {}; - let bar = 'foo'; - - assert(types.create(manyArgConstructor) instanceof manyArgConstructor); - assert(types.isObject(types.create(manyArgConstructor))); - - assert(types.create(manyArgConstructor, foo, bar) instanceof manyArgConstructor); - assert(types.isObject(types.create(manyArgConstructor, foo, bar))); - - let obj = types.create(manyArgConstructor, foo, bar); - assert.strictEqual(obj.foo, foo); - assert.strictEqual(obj.bar, bar); - }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/node/extfs/extfs.test.ts b/src/vs/base/test/node/extfs/extfs.test.ts deleted file mode 100644 index 07d331c0e6..0000000000 --- a/src/vs/base/test/node/extfs/extfs.test.ts +++ /dev/null @@ -1,615 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import { Readable } from 'stream'; -import { canNormalize } from 'vs/base/common/normalization'; -import { isLinux, isWindows } from 'vs/base/common/platform'; -import * as uuid from 'vs/base/common/uuid'; -import * as extfs from 'vs/base/node/extfs'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; - -const ignore = () => { }; - -const mkdirp = (path: string, mode: number, callback: (error: any) => void) => { - extfs.mkdirp(path, mode).then(() => callback(null), error => callback(error)); -}; - -const chunkSize = 64 * 1024; -const readError = 'Error while reading'; -function toReadable(value: string, throwError?: boolean): Readable { - const totalChunks = Math.ceil(value.length / chunkSize); - const stringChunks: string[] = []; - - for (let i = 0, j = 0; i < totalChunks; ++i, j += chunkSize) { - stringChunks[i] = value.substr(j, chunkSize); - } - - let counter = 0; - return new Readable({ - read: function () { - if (throwError) { - this.emit('error', new Error(readError)); - } - - let res!: string; - let canPush = true; - while (canPush && (res = stringChunks[counter++])) { - canPush = this.push(res); - } - - // EOS - if (!res) { - this.push(null); - } - }, - encoding: 'utf8' - }); -} - -suite('Extfs', () => { - - test('mkdirp', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); // 493 = 0755 - }); - - test('stat link', function (done) { - if (isWindows) { - // Symlinks are not the same on win, and we can not create them programitically without admin privileges - return done(); - } - - const id1 = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id1); - const directory = path.join(parentDir, 'extfs', id1); - - const id2 = uuid.generateUuid(); - const symbolicLink = path.join(parentDir, 'extfs', id2); - - mkdirp(directory, 493, error => { - if (error) { - return done(error); - } - - fs.symlinkSync(directory, symbolicLink); - - extfs.statLink(directory, (error, statAndIsLink) => { - if (error) { - return done(error); - } - - assert.ok(!statAndIsLink!.isSymbolicLink); - - extfs.statLink(symbolicLink, (error, statAndIsLink) => { - if (error) { - return done(error); - } - - assert.ok(statAndIsLink!.isSymbolicLink); - extfs.delSync(directory); - done(); - }); - }); - }); - }); - - test('delSync - swallows file not found error', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - extfs.delSync(newDir); - - assert.ok(!fs.existsSync(newDir)); - }); - - test('delSync - simple', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - extfs.delSync(newDir); - - assert.ok(!fs.existsSync(newDir)); - done(); - }); // 493 = 0755 - }); - - test('delSync - recursive folder structure', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); - - extfs.delSync(newDir); - - assert.ok(!fs.existsSync(newDir)); - done(); - }); // 493 = 0755 - }); - - test('copy, move and delete', function (done) { - const id = uuid.generateUuid(); - const id2 = uuid.generateUuid(); - const sourceDir = getPathFromAmdModule(require, './fixtures'); - const parentDir = path.join(os.tmpdir(), 'vsctests', 'extfs'); - const targetDir = path.join(parentDir, id); - const targetDir2 = path.join(parentDir, id2); - - extfs.copy(sourceDir, targetDir, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(targetDir)); - assert.ok(fs.existsSync(path.join(targetDir, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir, 'site.css'))); - assert.ok(fs.existsSync(path.join(targetDir, 'examples'))); - assert.ok(fs.statSync(path.join(targetDir, 'examples')).isDirectory()); - assert.ok(fs.existsSync(path.join(targetDir, 'examples', 'small.jxs'))); - - extfs.mv(targetDir, targetDir2, error => { - if (error) { - return done(error); - } - - assert.ok(!fs.existsSync(targetDir)); - assert.ok(fs.existsSync(targetDir2)); - assert.ok(fs.existsSync(path.join(targetDir2, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'site.css'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'examples'))); - assert.ok(fs.statSync(path.join(targetDir2, 'examples')).isDirectory()); - assert.ok(fs.existsSync(path.join(targetDir2, 'examples', 'small.jxs'))); - - extfs.mv(path.join(targetDir2, 'index.html'), path.join(targetDir2, 'index_moved.html'), error => { - if (error) { - return done(error); - } - - assert.ok(!fs.existsSync(path.join(targetDir2, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'index_moved.html'))); - - extfs.del(parentDir, os.tmpdir(), error => { - if (error) { - return done(error); - } - }, error => { - if (error) { - return done(error); - } - assert.ok(!fs.existsSync(parentDir)); - done(); - }); - }); - }); - }); - }); - - test('readdir', function (done) { - if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id, 'öäü'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - extfs.readdir(path.join(parentDir, 'extfs', id), (error, children) => { - assert.equal(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); // 493 = 0755 - } else { - done(); - } - }); - - test('writeFileAndFlush (string)', function (done) { - const smallData = 'Hello World'; - const bigData = (new Array(100 * 1024)).join('Large String\n'); - - testWriteFileAndFlush(smallData, smallData, bigData, bigData, done); - }); - - test('writeFileAndFlush (Buffer)', function (done) { - const smallData = 'Hello World'; - const bigData = (new Array(100 * 1024)).join('Large String\n'); - - testWriteFileAndFlush(Buffer.from(smallData), smallData, Buffer.from(bigData), bigData, done); - }); - - test('writeFileAndFlush (UInt8Array)', function (done) { - const smallData = 'Hello World'; - const bigData = (new Array(100 * 1024)).join('Large String\n'); - - testWriteFileAndFlush(new TextEncoder().encode(smallData), smallData, new TextEncoder().encode(bigData), bigData, done); - }); - - test('writeFileAndFlush (stream)', function (done) { - const smallData = 'Hello World'; - const bigData = (new Array(100 * 1024)).join('Large String\n'); - - testWriteFileAndFlush(toReadable(smallData), smallData, toReadable(bigData), bigData, done); - }); - - function testWriteFileAndFlush( - smallData: string | Buffer | NodeJS.ReadableStream | Uint8Array, - smallDataValue: string, - bigData: string | Buffer | NodeJS.ReadableStream | Uint8Array, - bigDataValue: string, - done: (error: Error | null) => void - ): void { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - extfs.writeFileAndFlush(testFile, smallData, null!, error => { - if (error) { - return done(error); - } - - assert.equal(fs.readFileSync(testFile), smallDataValue); - - extfs.writeFileAndFlush(testFile, bigData, null!, error => { - if (error) { - return done(error); - } - - assert.equal(fs.readFileSync(testFile), bigDataValue); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); - } - - test('writeFileAndFlush (file stream)', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const sourceFile = getPathFromAmdModule(require, './fixtures/index.html'); - const newDir = path.join(parentDir, 'extfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - extfs.writeFileAndFlush(testFile, fs.createReadStream(sourceFile), null!, error => { - if (error) { - return done(error); - } - - assert.equal(fs.readFileSync(testFile).toString(), fs.readFileSync(sourceFile).toString()); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); - - test('writeFileAndFlush (string, error handling)', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! - - extfs.writeFileAndFlush(testFile, 'Hello World', null!, error => { - if (!error) { - return done(new Error('Expected error for writing to readonly file')); - } - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); - - test('writeFileAndFlush (stream, error handling EISDIR)', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! - - const readable = toReadable('Hello World'); - extfs.writeFileAndFlush(testFile, readable, null!, error => { - if (!error || (error).code !== 'EISDIR') { - return done(new Error('Expected EISDIR error for writing to folder but got: ' + (error ? (error).code : 'no error'))); - } - - // verify that the stream is still consumable (for https://github.com/Microsoft/vscode/issues/42542) - assert.equal(readable.read(), 'Hello World'); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); - - test('writeFileAndFlush (stream, error handling READERROR)', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - extfs.writeFileAndFlush(testFile, toReadable('Hello World', true /* throw error */), null!, error => { - if (!error || error.message !== readError) { - return done(new Error('Expected error for writing to folder')); - } - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); - - test('writeFileAndFlush (stream, error handling EACCES)', function (done) { - if (isLinux) { - return done(); // somehow this test fails on Linux in our TFS builds - } - - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - fs.writeFileSync(testFile, ''); - fs.chmodSync(testFile, 33060); // make readonly - - extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null!, error => { - if (!error || !((error).code !== 'EACCES' || (error).code !== 'EPERM')) { - return done(new Error('Expected EACCES/EPERM error for writing to folder but got: ' + (error ? (error).code : 'no error'))); - } - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); - - test('writeFileAndFlush (file stream, error handling)', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const sourceFile = getPathFromAmdModule(require, './fixtures/index.html'); - const newDir = path.join(parentDir, 'extfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! - - extfs.writeFileAndFlush(testFile, fs.createReadStream(sourceFile), null!, error => { - if (!error) { - return done(new Error('Expected error for writing to folder')); - } - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); - - test('writeFileAndFlushSync', function (done) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } - - assert.ok(fs.existsSync(newDir)); - - extfs.writeFileAndFlushSync(testFile, 'Hello World', null!); - assert.equal(fs.readFileSync(testFile), 'Hello World'); - - const largeString = (new Array(100 * 1024)).join('Large String\n'); - - extfs.writeFileAndFlushSync(testFile, largeString, null!); - assert.equal(fs.readFileSync(testFile), largeString); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - - test('realcase', (done) => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - mkdirp(newDir, 493, error => { - - // {{SQL CARBON EDIT}} don't run this test case on Windows as this fails in VSO - // assume case insensitive file system - if (process.platform === 'darwin') { - const upper = newDir.toUpperCase(); - const real = extfs.realcaseSync(upper); - - if (real) { // can be null in case of permission errors - assert.notEqual(real, upper); - assert.equal(real.toUpperCase(), upper); - assert.equal(real, newDir); - } - } - - // linux, unix, etc. -> assume case sensitive file system - else if (process.platform !== 'win32') { - const real = extfs.realcaseSync(newDir); - assert.equal(real, newDir); - } - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - - test('realpath', (done) => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - mkdirp(newDir, 493, error => { - - extfs.realpath(newDir, (error, realpath) => { - assert.ok(realpath); - assert.ok(!error); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); - - test('realpathSync', (done) => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - mkdirp(newDir, 493, error => { - let realpath!: string; - try { - realpath = extfs.realpathSync(newDir); - } catch (error) { - assert.ok(!error); - } - assert.ok(realpath!); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - - test('mkdirp cancellation', (done) => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - const source = new CancellationTokenSource(); - - const mkdirpPromise = extfs.mkdirp(newDir, 493, source.token); - source.cancel(); - - return mkdirpPromise.then(res => { - assert.equal(res, false); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - - test('sanitizeFilePath', () => { - if (isWindows) { - assert.equal(extfs.sanitizeFilePath('.', 'C:\\the\\cwd'), 'C:\\the\\cwd'); - assert.equal(extfs.sanitizeFilePath('', 'C:\\the\\cwd'), 'C:\\the\\cwd'); - - assert.equal(extfs.sanitizeFilePath('C:', 'C:\\the\\cwd'), 'C:\\'); - assert.equal(extfs.sanitizeFilePath('C:\\', 'C:\\the\\cwd'), 'C:\\'); - assert.equal(extfs.sanitizeFilePath('C:\\\\', 'C:\\the\\cwd'), 'C:\\'); - - assert.equal(extfs.sanitizeFilePath('C:\\folder\\my.txt', 'C:\\the\\cwd'), 'C:\\folder\\my.txt'); - assert.equal(extfs.sanitizeFilePath('C:\\folder\\my', 'C:\\the\\cwd'), 'C:\\folder\\my'); - assert.equal(extfs.sanitizeFilePath('C:\\folder\\..\\my', 'C:\\the\\cwd'), 'C:\\my'); - assert.equal(extfs.sanitizeFilePath('C:\\folder\\my\\', 'C:\\the\\cwd'), 'C:\\folder\\my'); - assert.equal(extfs.sanitizeFilePath('C:\\folder\\my\\\\\\', 'C:\\the\\cwd'), 'C:\\folder\\my'); - - assert.equal(extfs.sanitizeFilePath('my.txt', 'C:\\the\\cwd'), 'C:\\the\\cwd\\my.txt'); - assert.equal(extfs.sanitizeFilePath('my.txt\\', 'C:\\the\\cwd'), 'C:\\the\\cwd\\my.txt'); - - assert.equal(extfs.sanitizeFilePath('\\\\localhost\\folder\\my', 'C:\\the\\cwd'), '\\\\localhost\\folder\\my'); - assert.equal(extfs.sanitizeFilePath('\\\\localhost\\folder\\my\\', 'C:\\the\\cwd'), '\\\\localhost\\folder\\my'); - } else { - assert.equal(extfs.sanitizeFilePath('.', '/the/cwd'), '/the/cwd'); - assert.equal(extfs.sanitizeFilePath('', '/the/cwd'), '/the/cwd'); - assert.equal(extfs.sanitizeFilePath('/', '/the/cwd'), '/'); - - assert.equal(extfs.sanitizeFilePath('/folder/my.txt', '/the/cwd'), '/folder/my.txt'); - assert.equal(extfs.sanitizeFilePath('/folder/my', '/the/cwd'), '/folder/my'); - assert.equal(extfs.sanitizeFilePath('/folder/../my', '/the/cwd'), '/my'); - assert.equal(extfs.sanitizeFilePath('/folder/my/', '/the/cwd'), '/folder/my'); - assert.equal(extfs.sanitizeFilePath('/folder/my///', '/the/cwd'), '/folder/my'); - - assert.equal(extfs.sanitizeFilePath('my.txt', '/the/cwd'), '/the/cwd/my.txt'); - assert.equal(extfs.sanitizeFilePath('my.txt/', '/the/cwd'), '/the/cwd/my.txt'); - } - }); -}); diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts new file mode 100644 index 0000000000..e435d62480 --- /dev/null +++ b/src/vs/base/test/node/extpath.test.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as os from 'os'; +import * as path from 'vs/base/common/path'; +import * as uuid from 'vs/base/common/uuid'; +import * as pfs from 'vs/base/node/pfs'; +import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; + +suite('Extpath', () => { + + test('realcase', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extpath', id); + + await pfs.mkdirp(newDir, 493); + + // assume case insensitive file system + if (process.platform === 'win32' || process.platform === 'darwin') { + const upper = newDir.toUpperCase(); + const real = realcaseSync(upper); + + if (real) { // can be null in case of permission errors + assert.notEqual(real, upper); + assert.equal(real.toUpperCase(), upper); + assert.equal(real, newDir); + } + } + + // linux, unix, etc. -> assume case sensitive file system + else { + const real = realcaseSync(newDir); + assert.equal(real, newDir); + } + + await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + }); + + test('realpath', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extpath', id); + + await pfs.mkdirp(newDir, 493); + + const realpathVal = await realpath(newDir); + assert.ok(realpathVal); + + await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + }); + + test('realpathSync', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extpath', id); + + await pfs.mkdirp(newDir, 493); + + let realpath!: string; + try { + realpath = realpathSync(newDir); + } catch (error) { + assert.ok(!error); + } + assert.ok(realpath!); + + await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + }); +}); diff --git a/src/vs/base/test/node/flow.test.ts b/src/vs/base/test/node/flow.test.ts deleted file mode 100644 index 14365d2cf2..0000000000 --- a/src/vs/base/test/node/flow.test.ts +++ /dev/null @@ -1,488 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as flow from 'vs/base/node/flow'; - -const loop = flow.loop; -const sequence = flow.sequence; -const parallel = flow.parallel; - -suite('Flow', () => { - function assertCounterEquals(counter: number, expected: number): void { - assert.ok(counter === expected, 'Expected ' + expected + ' assertions, but got ' + counter); - } - - function syncThrowsError(callback: any): void { - callback(new Error('foo'), null); - } - - function syncSequenceGetThrowsError(value: any, callback: any) { - sequence( - function onError(error) { - callback(error, null); - }, - - function getFirst(this: any) { - syncThrowsError(this); - }, - - function handleFirst(first: number) { - //Foo - } - ); - } - - function syncGet(value: any, callback: any): void { - callback(null, value); - } - - function syncGetError(value: any, callback: any): void { - callback(new Error(''), null); - } - - function asyncGet(value: any, callback: any): void { - process.nextTick(function () { - callback(null, value); - }); - } - - function asyncGetError(value: any, callback: any): void { - process.nextTick(function () { - callback(new Error(''), null); - }); - } - - test('loopSync', function (done: () => void) { - const elements = ['1', '2', '3']; - loop(elements, function (element, callback, index, total) { - assert.ok(index === 0 || index === 1 || index === 2); - assert.deepEqual(3, total); - callback(null, element); - }, function (error, result) { - assert.equal(error, null); - assert.deepEqual(result, elements); - - done(); - }); - }); - - test('loopByFunctionSync', function (done: () => void) { - const elements = function (callback: Function) { - callback(null, ['1', '2', '3']); - }; - - loop(elements, function (element, callback) { - callback(null, element); - }, function (error, result) { - assert.equal(error, null); - assert.deepEqual(result, ['1', '2', '3']); - - done(); - }); - }); - - test('loopByFunctionAsync', function (done: () => void) { - const elements = function (callback: Function) { - process.nextTick(function () { - callback(null, ['1', '2', '3']); - }); - }; - - loop(elements, function (element, callback) { - callback(null, element); - }, function (error, result) { - assert.equal(error, null); - assert.deepEqual(result, ['1', '2', '3']); - - done(); - }); - }); - - test('loopSyncErrorByThrow', function (done: () => void) { - const elements = ['1', '2', '3']; - loop(elements, function (element, callback) { - if (element === '2') { - throw new Error('foo'); - } else { - callback(null, element); - } - }, function (error, result) { - assert.ok(error); - assert.ok(!result); - - done(); - }); - }); - - test('loopSyncErrorByCallback', function (done: () => void) { - const elements = ['1', '2', '3']; - loop(elements, function (element, callback) { - if (element === '2') { - callback(new Error('foo'), null); - } else { - callback(null, element); - } - }, function (error, result) { - assert.ok(error); - assert.ok(!result); - - done(); - }); - }); - - test('loopAsync', function (done: () => void) { - const elements = ['1', '2', '3']; - loop(elements, function (element, callback) { - process.nextTick(function () { - callback(null, element); - }); - }, function (error, result) { - assert.equal(error, null); - assert.deepEqual(result, elements); - - done(); - }); - }); - - test('loopAsyncErrorByCallback', function (done: () => void) { - const elements = ['1', '2', '3']; - loop(elements, function (element, callback) { - process.nextTick(function () { - if (element === '2') { - callback(new Error('foo'), null); - } else { - callback(null, element); - } - }); - }, function (error, result) { - assert.ok(error); - assert.ok(!result); - - done(); - }); - }); - - test('sequenceSync', function (done: () => void) { - let assertionCount = 0; - let errorCount = 0; - - sequence( - function onError(error) { - errorCount++; - }, - - function getFirst(this: any) { - syncGet('1', this); - }, - - function handleFirst(this: any, first: number) { - assert.deepEqual('1', first); - assertionCount++; - syncGet('2', this); - }, - - function handleSecond(this: any, second: any) { - assert.deepEqual('2', second); - assertionCount++; - syncGet(null, this); - }, - - function handleThird(third: any) { - assert.ok(!third); - assertionCount++; - - assertCounterEquals(assertionCount, 3); - assertCounterEquals(errorCount, 0); - done(); - } - ); - }); - - test('sequenceAsync', function (done: () => void) { - let assertionCount = 0; - let errorCount = 0; - - sequence( - function onError(error) { - errorCount++; - }, - - function getFirst(this: any) { - asyncGet('1', this); - }, - - function handleFirst(this: any, first: number) { - assert.deepEqual('1', first); - assertionCount++; - asyncGet('2', this); - }, - - function handleSecond(this: any, second: number) { - assert.deepEqual('2', second); - assertionCount++; - asyncGet(null, this); - }, - - function handleThird(third: number) { - assert.ok(!third); - assertionCount++; - - assertCounterEquals(assertionCount, 3); - assertCounterEquals(errorCount, 0); - done(); - } - ); - }); - - test('sequenceSyncErrorByThrow', function (done: () => void) { - let assertionCount = 0; - let errorCount = 0; - - sequence( - function onError(error) { - errorCount++; - - assertCounterEquals(assertionCount, 1); - assertCounterEquals(errorCount, 1); - done(); - }, - - function getFirst(this: any) { - syncGet('1', this); - }, - - function handleFirst(this: any, first: number) { - assert.deepEqual('1', first); - assertionCount++; - syncGet('2', this); - }, - - function handleSecond(second: number) { - if (true) { - throw new Error(''); - } - // assertionCount++; - // syncGet(null, this); - }, - - function handleThird(third: number) { - throw new Error('We should not be here'); - } - ); - }); - - test('sequenceSyncErrorByCallback', function (done: () => void) { - let assertionCount = 0; - let errorCount = 0; - - sequence( - function onError(error) { - errorCount++; - - assertCounterEquals(assertionCount, 1); - assertCounterEquals(errorCount, 1); - done(); - }, - - function getFirst(this: any) { - syncGet('1', this); - }, - - function handleFirst(this: any, first: number) { - assert.deepEqual('1', first); - assertionCount++; - syncGetError('2', this); - }, - - function handleSecond(second: number) { - throw new Error('We should not be here'); - } - ); - }); - - test('sequenceAsyncErrorByThrow', function (done: () => void) { - let assertionCount = 0; - let errorCount = 0; - - sequence( - function onError(error) { - errorCount++; - - assertCounterEquals(assertionCount, 1); - assertCounterEquals(errorCount, 1); - done(); - }, - - function getFirst(this: any) { - asyncGet('1', this); - }, - - function handleFirst(this: any, first: number) { - assert.deepEqual('1', first); - assertionCount++; - asyncGet('2', this); - }, - - function handleSecond(second: number) { - if (true) { - throw new Error(''); - } - // assertionCount++; - // asyncGet(null, this); - }, - - function handleThird(third: number) { - throw new Error('We should not be here'); - } - ); - }); - - test('sequenceAsyncErrorByCallback', function (done: () => void) { - let assertionCount = 0; - let errorCount = 0; - - sequence( - function onError(error) { - errorCount++; - - assertCounterEquals(assertionCount, 1); - assertCounterEquals(errorCount, 1); - done(); - }, - - function getFirst(this: any) { - asyncGet('1', this); - }, - - function handleFirst(this: any, first: number) { - assert.deepEqual('1', first); - assertionCount++; - asyncGetError('2', this); - }, - - function handleSecond(second: number) { - throw new Error('We should not be here'); - } - ); - }); - - test('syncChainedSequenceError', function (done: () => void) { - sequence( - function onError(error) { - done(); - }, - - function getFirst(this: any) { - syncSequenceGetThrowsError('1', this); - } - ); - }); - - test('tolerateBooleanResults', function (done: () => void) { - let assertionCount = 0; - let errorCount = 0; - - sequence( - function onError(error) { - errorCount++; - }, - - function getFirst(this: any) { - this(true); - }, - - function getSecond(this: any, result: boolean) { - assert.equal(result, true); - this(false); - }, - - function last(result: boolean) { - assert.equal(result, false); - assertionCount++; - - assertCounterEquals(assertionCount, 1); - assertCounterEquals(errorCount, 0); - done(); - } - ); - }); - - test('loopTolerateBooleanResults', function (done: () => void) { - let elements = ['1', '2', '3']; - loop(elements, function (element, callback) { - process.nextTick(function () { - (callback)(true); - }); - }, function (error, result) { - assert.equal(error, null); - assert.deepEqual(result, [true, true, true]); - - done(); - }); - }); - - test('parallel', function (done: () => void) { - let elements = [1, 2, 3, 4, 5]; - let sum = 0; - - parallel(elements, function (element, callback) { - sum += element; - callback(null!, element * element); - }, function (errors, result) { - assert.ok(!errors); - - assert.deepEqual(sum, 15); - assert.deepEqual(result, [1, 4, 9, 16, 25]); - - done(); - }); - }); - - test('parallel - setTimeout', function (done: () => void) { - let elements = [1, 2, 3, 4, 5]; - let timeouts = [10, 30, 5, 0, 4]; - let sum = 0; - - parallel(elements, function (element, callback) { - setTimeout(function () { - sum += element; - callback(null!, element * element); - }, timeouts.pop()); - }, function (errors, result) { - assert.ok(!errors); - - assert.deepEqual(sum, 15); - assert.deepEqual(result, [1, 4, 9, 16, 25]); - - done(); - }); - }); - - test('parallel - with error', function (done: () => void) { - const elements = [1, 2, 3, 4, 5]; - const timeouts = [10, 30, 5, 0, 4]; - let sum = 0; - - parallel(elements, function (element, callback) { - setTimeout(function () { - if (element === 4) { - callback(new Error('error!'), null!); - } else { - sum += element; - callback(null!, element * element); - } - }, timeouts.pop()); - }, function (errors, result) { - assert.ok(errors); - assert.deepEqual(errors, [null, null, null, new Error('error!'), null]); - - assert.deepEqual(sum, 11); - assert.deepEqual(result, [1, 4, 9, null, 25]); - - done(); - }); - }); -}); \ No newline at end of file diff --git a/src/vs/base/test/node/glob.test.ts b/src/vs/base/test/node/glob.test.ts index 6d211bb8ac..6e951010dc 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/node/glob.test.ts @@ -455,7 +455,7 @@ suite('Glob', () => { expression = { '**/*.js': { - } + } as any }; assert.strictEqual('**/*.js', glob.match(expression, 'test.js', hasSibling)); @@ -474,7 +474,7 @@ suite('Glob', () => { '**/*.js': { when: '$(basename).ts' }, '**/*.as': true, '**/*.foo': false, - '**/*.bananas': { bananas: true } + '**/*.bananas': { bananas: true } as any }; assert.strictEqual('**/*.js', glob.match(expression, 'test.js', hasSibling)); @@ -691,7 +691,7 @@ suite('Glob', () => { }); test('expression with other falsy value', function () { - let expr = { '**/*.js': 0 }; + let expr = { '**/*.js': 0 } as any; assert.strictEqual(glob.match(expr, 'foo.js'), '**/*.js'); }); diff --git a/src/vs/base/test/node/pfs.test.ts b/src/vs/base/test/node/pfs.test.ts deleted file mode 100644 index fa2311935a..0000000000 --- a/src/vs/base/test/node/pfs.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as os from 'os'; - -import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; - -import * as uuid from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; -import { timeout } from 'vs/base/common/async'; - -suite('PFS', () => { - - test('writeFile', () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'writefile.txt'); - - return pfs.mkdirp(newDir, 493).then(() => { - assert.ok(fs.existsSync(newDir)); - - return pfs.writeFile(testFile, 'Hello World', null!).then(() => { - assert.equal(fs.readFileSync(testFile), 'Hello World'); - - return pfs.del(parentDir, os.tmpdir()); - }); - }); - }); - - test('writeFile - parallel write on different files works', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile1 = path.join(newDir, 'writefile1.txt'); - const testFile2 = path.join(newDir, 'writefile2.txt'); - const testFile3 = path.join(newDir, 'writefile3.txt'); - const testFile4 = path.join(newDir, 'writefile4.txt'); - const testFile5 = path.join(newDir, 'writefile5.txt'); - - return pfs.mkdirp(newDir, 493).then(() => { - assert.ok(fs.existsSync(newDir)); - - return Promise.all([ - pfs.writeFile(testFile1, 'Hello World 1', null!), - pfs.writeFile(testFile2, 'Hello World 2', null!), - pfs.writeFile(testFile3, 'Hello World 3', null!), - pfs.writeFile(testFile4, 'Hello World 4', null!), - pfs.writeFile(testFile5, 'Hello World 5', null!) - ]).then(() => { - assert.equal(fs.readFileSync(testFile1), 'Hello World 1'); - assert.equal(fs.readFileSync(testFile2), 'Hello World 2'); - assert.equal(fs.readFileSync(testFile3), 'Hello World 3'); - assert.equal(fs.readFileSync(testFile4), 'Hello World 4'); - assert.equal(fs.readFileSync(testFile5), 'Hello World 5'); - - return pfs.del(parentDir, os.tmpdir()); - }); - }); - }); - - test('writeFile - parallel write on same files works and is sequentalized', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'writefile.txt'); - - return pfs.mkdirp(newDir, 493).then(() => { - assert.ok(fs.existsSync(newDir)); - - return Promise.all([ - pfs.writeFile(testFile, 'Hello World 1', undefined), - pfs.writeFile(testFile, 'Hello World 2', undefined), - timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 3', undefined)), - pfs.writeFile(testFile, 'Hello World 4', undefined), - timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 5', undefined)) - ]).then(() => { - assert.equal(fs.readFileSync(testFile), 'Hello World 5'); - - return pfs.del(parentDir, os.tmpdir()); - }); - }); - }); - - test('rimraf - simple', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - return pfs.mkdirp(newDir, 493).then(() => { - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - return pfs.rimraf(newDir).then(() => { - assert.ok(!fs.existsSync(newDir)); - }); - }); - }); - - test('rimraf - recursive folder structure', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - return pfs.mkdirp(newDir, 493).then(() => { - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); - - return pfs.rimraf(newDir).then(() => { - assert.ok(!fs.existsSync(newDir)); - }); - }); - }); - - test('moveIgnoreError', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extfs', id); - - return pfs.mkdirp(newDir, 493).then(() => { - return pfs.renameIgnoreError(path.join(newDir, 'foo'), path.join(newDir, 'bar')).then(() => { - - return pfs.del(parentDir, os.tmpdir()); - }, error => { - assert.fail(error); - - return Promise.reject(error); - }); - }); - }); -}); diff --git a/src/vs/base/test/node/extfs/fixtures/examples/company.jxs b/src/vs/base/test/node/pfs/fixtures/examples/company.jxs similarity index 100% rename from src/vs/base/test/node/extfs/fixtures/examples/company.jxs rename to src/vs/base/test/node/pfs/fixtures/examples/company.jxs diff --git a/src/vs/base/test/node/extfs/fixtures/examples/conway.jxs b/src/vs/base/test/node/pfs/fixtures/examples/conway.jxs similarity index 100% rename from src/vs/base/test/node/extfs/fixtures/examples/conway.jxs rename to src/vs/base/test/node/pfs/fixtures/examples/conway.jxs diff --git a/src/vs/base/test/node/extfs/fixtures/examples/employee.jxs b/src/vs/base/test/node/pfs/fixtures/examples/employee.jxs similarity index 100% rename from src/vs/base/test/node/extfs/fixtures/examples/employee.jxs rename to src/vs/base/test/node/pfs/fixtures/examples/employee.jxs diff --git a/src/vs/base/test/node/extfs/fixtures/examples/small.jxs b/src/vs/base/test/node/pfs/fixtures/examples/small.jxs similarity index 100% rename from src/vs/base/test/node/extfs/fixtures/examples/small.jxs rename to src/vs/base/test/node/pfs/fixtures/examples/small.jxs diff --git a/src/vs/base/test/node/extfs/fixtures/index.html b/src/vs/base/test/node/pfs/fixtures/index.html similarity index 100% rename from src/vs/base/test/node/extfs/fixtures/index.html rename to src/vs/base/test/node/pfs/fixtures/index.html diff --git a/src/vs/base/test/node/extfs/fixtures/site.css b/src/vs/base/test/node/pfs/fixtures/site.css similarity index 93% rename from src/vs/base/test/node/extfs/fixtures/site.css rename to src/vs/base/test/node/pfs/fixtures/site.css index b7e5283202..f7b51e752b 100644 --- a/src/vs/base/test/node/extfs/fixtures/site.css +++ b/src/vs/base/test/node/pfs/fixtures/site.css @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. + * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /*---------------------------------------------------------- diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts new file mode 100644 index 0000000000..4f09ad2b56 --- /dev/null +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -0,0 +1,612 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as os from 'os'; +import * as path from 'vs/base/common/path'; +import * as fs from 'fs'; +import { Readable } from 'stream'; +import * as uuid from 'vs/base/common/uuid'; +import * as pfs from 'vs/base/node/pfs'; +import { timeout } from 'vs/base/common/async'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { isWindows, isLinux } from 'vs/base/common/platform'; +import { canNormalize } from 'vs/base/common/normalization'; +import { VSBuffer } from 'vs/base/common/buffer'; + +const chunkSize = 64 * 1024; +const readError = 'Error while reading'; +function toReadable(value: string, throwError?: boolean): Readable { + const totalChunks = Math.ceil(value.length / chunkSize); + const stringChunks: string[] = []; + + for (let i = 0, j = 0; i < totalChunks; ++i, j += chunkSize) { + stringChunks[i] = value.substr(j, chunkSize); + } + + let counter = 0; + return new Readable({ + read: function () { + if (throwError) { + this.emit('error', new Error(readError)); + } + + let res!: string; + let canPush = true; + while (canPush && (res = stringChunks[counter++])) { + canPush = this.push(res); + } + + // EOS + if (!res) { + this.push(null); + } + }, + encoding: 'utf8' + }); +} + +suite('PFS', () => { + + test('writeFile', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'writefile.txt'); + + await pfs.mkdirp(newDir, 493); + assert.ok(fs.existsSync(newDir)); + + await pfs.writeFile(testFile, 'Hello World', (null!)); + assert.equal(fs.readFileSync(testFile), 'Hello World'); + + await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + }); + + test('writeFile - parallel write on different files works', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile1 = path.join(newDir, 'writefile1.txt'); + const testFile2 = path.join(newDir, 'writefile2.txt'); + const testFile3 = path.join(newDir, 'writefile3.txt'); + const testFile4 = path.join(newDir, 'writefile4.txt'); + const testFile5 = path.join(newDir, 'writefile5.txt'); + + await pfs.mkdirp(newDir, 493); + assert.ok(fs.existsSync(newDir)); + + await Promise.all([ + pfs.writeFile(testFile1, 'Hello World 1', (null!)), + pfs.writeFile(testFile2, 'Hello World 2', (null!)), + pfs.writeFile(testFile3, 'Hello World 3', (null!)), + pfs.writeFile(testFile4, 'Hello World 4', (null!)), + pfs.writeFile(testFile5, 'Hello World 5', (null!)) + ]); + assert.equal(fs.readFileSync(testFile1), 'Hello World 1'); + assert.equal(fs.readFileSync(testFile2), 'Hello World 2'); + assert.equal(fs.readFileSync(testFile3), 'Hello World 3'); + assert.equal(fs.readFileSync(testFile4), 'Hello World 4'); + assert.equal(fs.readFileSync(testFile5), 'Hello World 5'); + + await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + }); + + test('writeFile - parallel write on same files works and is sequentalized', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'writefile.txt'); + + await pfs.mkdirp(newDir, 493); + assert.ok(fs.existsSync(newDir)); + + await Promise.all([ + pfs.writeFile(testFile, 'Hello World 1', undefined), + pfs.writeFile(testFile, 'Hello World 2', undefined), + timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 3', undefined)), + pfs.writeFile(testFile, 'Hello World 4', undefined), + timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 5', undefined)) + ]); + assert.equal(fs.readFileSync(testFile), 'Hello World 5'); + + await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + }); + + test('rimraf - simple - unlink', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + + await pfs.rimraf(newDir); + assert.ok(!fs.existsSync(newDir)); + }); + + test('rimraf - simple - move', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + + await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); + assert.ok(!fs.existsSync(newDir)); + }); + + test('rimraf - recursive folder structure - unlink', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(path.join(newDir, 'somefolder')); + fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); + + await pfs.rimraf(newDir); + assert.ok(!fs.existsSync(newDir)); + }); + + test('rimraf - recursive folder structure - move', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(path.join(newDir, 'somefolder')); + fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); + + await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); + assert.ok(!fs.existsSync(newDir)); + }); + + test('rimraf - simple ends with dot - move', async () => { + const id = `${uuid.generateUuid()}.`; + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + + await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); + assert.ok(!fs.existsSync(newDir)); + }); + + test('rimraf - simple ends with dot slash/backslash - move', async () => { + const id = `${uuid.generateUuid()}.`; + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + + await pfs.rimraf(`${newDir}${path.sep}`, pfs.RimRafMode.MOVE); + assert.ok(!fs.existsSync(newDir)); + }); + + test('rimrafSync - swallows file not found error', function () { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + pfs.rimrafSync(newDir); + + assert.ok(!fs.existsSync(newDir)); + }); + + test('rimrafSync - simple', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + + pfs.rimrafSync(newDir); + + assert.ok(!fs.existsSync(newDir)); + }); + + test('rimrafSync - recursive folder structure', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + + fs.mkdirSync(path.join(newDir, 'somefolder')); + fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); + + pfs.rimrafSync(newDir); + + assert.ok(!fs.existsSync(newDir)); + }); + + test('moveIgnoreError', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + try { + await pfs.renameIgnoreError(path.join(newDir, 'foo'), path.join(newDir, 'bar')); + return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + } + catch (error) { + assert.fail(error); + return Promise.reject(error); + } + }); + + test('copy, move and delete', async () => { + const id = uuid.generateUuid(); + const id2 = uuid.generateUuid(); + const sourceDir = getPathFromAmdModule(require, './fixtures'); + const parentDir = path.join(os.tmpdir(), 'vsctests', 'pfs'); + const targetDir = path.join(parentDir, id); + const targetDir2 = path.join(parentDir, id2); + + await pfs.copy(sourceDir, targetDir); + + assert.ok(fs.existsSync(targetDir)); + assert.ok(fs.existsSync(path.join(targetDir, 'index.html'))); + assert.ok(fs.existsSync(path.join(targetDir, 'site.css'))); + assert.ok(fs.existsSync(path.join(targetDir, 'examples'))); + assert.ok(fs.statSync(path.join(targetDir, 'examples')).isDirectory()); + assert.ok(fs.existsSync(path.join(targetDir, 'examples', 'small.jxs'))); + + await pfs.move(targetDir, targetDir2); + + assert.ok(!fs.existsSync(targetDir)); + assert.ok(fs.existsSync(targetDir2)); + assert.ok(fs.existsSync(path.join(targetDir2, 'index.html'))); + assert.ok(fs.existsSync(path.join(targetDir2, 'site.css'))); + assert.ok(fs.existsSync(path.join(targetDir2, 'examples'))); + assert.ok(fs.statSync(path.join(targetDir2, 'examples')).isDirectory()); + assert.ok(fs.existsSync(path.join(targetDir2, 'examples', 'small.jxs'))); + + await pfs.move(path.join(targetDir2, 'index.html'), path.join(targetDir2, 'index_moved.html')); + + assert.ok(!fs.existsSync(path.join(targetDir2, 'index.html'))); + assert.ok(fs.existsSync(path.join(targetDir2, 'index_moved.html'))); + + await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + + assert.ok(!fs.existsSync(parentDir)); + }); + + test('mkdirp', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + + assert.ok(fs.existsSync(newDir)); + + return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + }); + + test('mkdirp cancellation', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + const source = new CancellationTokenSource(); + + const mkdirpPromise = pfs.mkdirp(newDir, 493, source.token); + source.cancel(); + + await mkdirpPromise; + + assert.ok(!fs.existsSync(newDir)); + + return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + }); + + test('readDirsInDir', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + + await pfs.mkdirp(newDir, 493); + + fs.mkdirSync(path.join(newDir, 'somefolder1')); + fs.mkdirSync(path.join(newDir, 'somefolder2')); + fs.mkdirSync(path.join(newDir, 'somefolder3')); + fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + + const result = await pfs.readDirsInDir(newDir); + assert.equal(result.length, 3); + assert.ok(result.indexOf('somefolder1') !== -1); + assert.ok(result.indexOf('somefolder2') !== -1); + assert.ok(result.indexOf('somefolder3') !== -1); + + await pfs.rimraf(newDir); + }); + + test('stat link', async () => { + if (isWindows) { + return Promise.resolve(); // Symlinks are not the same on win, and we can not create them programitically without admin privileges + } + + const id1 = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id1); + const directory = path.join(parentDir, 'pfs', id1); + + const id2 = uuid.generateUuid(); + const symbolicLink = path.join(parentDir, 'pfs', id2); + + await pfs.mkdirp(directory, 493); + + fs.symlinkSync(directory, symbolicLink); + + let statAndIsLink = await pfs.statLink(directory); + assert.ok(!statAndIsLink!.isSymbolicLink); + + statAndIsLink = await pfs.statLink(symbolicLink); + assert.ok(statAndIsLink!.isSymbolicLink); + + pfs.rimrafSync(directory); + }); + + test('readdir', async () => { + if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id, 'öäü'); + + await pfs.mkdirp(newDir, 493); + + assert.ok(fs.existsSync(newDir)); + + const children = await pfs.readdir(path.join(parentDir, 'pfs', id)); + assert.equal(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so + + await pfs.rimraf(parentDir); + } + }); + + test('writeFile (string)', async () => { + const smallData = 'Hello World'; + const bigData = (new Array(100 * 1024)).join('Large String\n'); + + return testWriteFileAndFlush(smallData, smallData, bigData, bigData); + }); + + test('writeFile (Buffer)', async () => { + const smallData = 'Hello World'; + const bigData = (new Array(100 * 1024)).join('Large String\n'); + + return testWriteFileAndFlush(Buffer.from(smallData), smallData, Buffer.from(bigData), bigData); + }); + + test('writeFile (UInt8Array)', async () => { + const smallData = 'Hello World'; + const bigData = (new Array(100 * 1024)).join('Large String\n'); + + return testWriteFileAndFlush(VSBuffer.fromString(smallData).buffer, smallData, VSBuffer.fromString(bigData).buffer, bigData); + }); + + test('writeFile (stream)', async () => { + const smallData = 'Hello World'; + const bigData = (new Array(100 * 1024)).join('Large String\n'); + + return testWriteFileAndFlush(toReadable(smallData), smallData, toReadable(bigData), bigData); + }); + + async function testWriteFileAndFlush( + smallData: string | Buffer | NodeJS.ReadableStream | Uint8Array, + smallDataValue: string, + bigData: string | Buffer | NodeJS.ReadableStream | Uint8Array, + bigDataValue: string + ): Promise { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + await pfs.mkdirp(newDir, 493); + assert.ok(fs.existsSync(newDir)); + + await pfs.writeFile(testFile, smallData); + assert.equal(fs.readFileSync(testFile), smallDataValue); + + await pfs.writeFile(testFile, bigData); + assert.equal(fs.readFileSync(testFile), bigDataValue); + + await pfs.rimraf(parentDir); + } + + test('writeFile (file stream)', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const sourceFile = getPathFromAmdModule(require, './fixtures/index.html'); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + await pfs.mkdirp(newDir, 493); + assert.ok(fs.existsSync(newDir)); + + await pfs.writeFile(testFile, fs.createReadStream(sourceFile)); + assert.equal(fs.readFileSync(testFile).toString(), fs.readFileSync(sourceFile).toString()); + + await pfs.rimraf(parentDir); + }); + + test('writeFile (string, error handling)', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + await pfs.mkdirp(newDir, 493); + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + let expectedError: Error | undefined; + try { + await pfs.writeFile(testFile, 'Hello World'); + } catch (error) { + expectedError = error; + } + + assert.ok(expectedError); + + await pfs.rimraf(parentDir); + }); + + test('writeFile (stream, error handling EISDIR)', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + await pfs.mkdirp(newDir, 493); + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + const readable = toReadable('Hello World'); + + let expectedError: Error | undefined; + try { + await pfs.writeFile(testFile, readable); + } catch (error) { + expectedError = error; + } + + if (!expectedError || (expectedError).code !== 'EISDIR') { + return Promise.reject(new Error('Expected EISDIR error for writing to folder but got: ' + (expectedError ? (expectedError).code : 'no error'))); + } + + // verify that the stream is still consumable (for https://github.com/Microsoft/vscode/issues/42542) + assert.equal(readable.read(), 'Hello World'); + + await pfs.rimraf(parentDir); + }); + + test('writeFile (stream, error handling READERROR)', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + await pfs.mkdirp(newDir, 493); + assert.ok(fs.existsSync(newDir)); + + let expectedError: Error | undefined; + try { + await pfs.writeFile(testFile, toReadable('Hello World', true /* throw error */)); + } catch (error) { + expectedError = error; + } + + if (!expectedError || expectedError.message !== readError) { + return Promise.reject(new Error('Expected error for writing to folder')); + } + + await pfs.rimraf(parentDir); + }); + + test('writeFile (stream, error handling EACCES)', async () => { + if (isLinux) { + return Promise.resolve(); // somehow this test fails on Linux in our TFS builds + } + + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + await pfs.mkdirp(newDir, 493); + + assert.ok(fs.existsSync(newDir)); + + fs.writeFileSync(testFile, ''); + fs.chmodSync(testFile, 33060); // make readonly + + let expectedError: Error | undefined; + try { + await pfs.writeFile(testFile, toReadable('Hello World')); + } catch (error) { + expectedError = error; + } + + if (!expectedError || !((expectedError).code !== 'EACCES' || (expectedError).code !== 'EPERM')) { + return Promise.reject(new Error('Expected EACCES/EPERM error for writing to folder but got: ' + (expectedError ? (expectedError).code : 'no error'))); + } + + await pfs.rimraf(parentDir); + }); + + test('writeFile (file stream, error handling)', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const sourceFile = getPathFromAmdModule(require, './fixtures/index.html'); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + await pfs.mkdirp(newDir, 493); + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + let expectedError: Error | undefined; + try { + await pfs.writeFile(testFile, fs.createReadStream(sourceFile)); + } catch (error) { + expectedError = error; + } + + if (!expectedError) { + return Promise.reject(new Error('Expected error for writing to folder')); + } + + await pfs.rimraf(parentDir); + }); + + test('writeFileSync', async () => { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'pfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + await pfs.mkdirp(newDir, 493); + + assert.ok(fs.existsSync(newDir)); + + pfs.writeFileSync(testFile, 'Hello World'); + assert.equal(fs.readFileSync(testFile), 'Hello World'); + + const largeString = (new Array(100 * 1024)).join('Large String\n'); + + pfs.writeFileSync(testFile, largeString); + assert.equal(fs.readFileSync(testFile), largeString); + + await pfs.rimraf(parentDir); + }); +}); diff --git a/src/vs/base/test/node/storage/storage.test.ts b/src/vs/base/test/node/storage/storage.test.ts index 2b0d6d5645..ec7b8e6a4f 100644 --- a/src/vs/base/test/node/storage/storage.test.ts +++ b/src/vs/base/test/node/storage/storage.test.ts @@ -8,7 +8,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; import { equal, ok } from 'assert'; -import { mkdirp, del, writeFile, exists, unlink } from 'vs/base/node/pfs'; +import { mkdirp, writeFile, exists, unlink, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { isWindows } from 'vs/base/common/platform'; @@ -92,7 +92,7 @@ suite('Storage Library', () => { equal(deletePromiseResolved, true); await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('external changes', async () => { @@ -148,7 +148,7 @@ suite('Storage Library', () => { equal(changes.size, 0); await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('close flushes data', async () => { @@ -202,7 +202,7 @@ suite('Storage Library', () => { ok(!storage.get('bar')); await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('conflicting updates', async () => { @@ -244,7 +244,7 @@ suite('Storage Library', () => { ok(setAndDeletePromiseResolved); await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('corrupt DB recovers', async () => { @@ -274,7 +274,7 @@ suite('Storage Library', () => { equal(storage.get('foo'), 'bar'); await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); }); @@ -372,7 +372,7 @@ suite('SQLite Storage Library', () => { await testDBBasics(join(storageDir, 'storage.db')); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('basics (open multiple times)', async () => { @@ -383,7 +383,7 @@ suite('SQLite Storage Library', () => { await testDBBasics(join(storageDir, 'storage.db')); await testDBBasics(join(storageDir, 'storage.db')); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('basics (corrupt DB falls back to empty DB)', async () => { @@ -401,7 +401,7 @@ suite('SQLite Storage Library', () => { ok(expectedError); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('basics (corrupt DB restores from previous backup)', async () => { @@ -439,7 +439,7 @@ suite('SQLite Storage Library', () => { equal(recoveryCalled, false); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('basics (corrupt DB falls back to empty DB if backup is corrupt)', async () => { @@ -468,7 +468,7 @@ suite('SQLite Storage Library', () => { await testDBBasics(storagePath); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('basics (DB that becomes corrupt during runtime stores all state from cache on close)', async () => { @@ -536,7 +536,7 @@ suite('SQLite Storage Library', () => { equal(recoveryCalled, false); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('real world example', async function () { @@ -627,7 +627,7 @@ suite('SQLite Storage Library', () => { await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('very large item value', async function () { @@ -682,7 +682,7 @@ suite('SQLite Storage Library', () => { await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('multiple concurrent writes execute in sequence', async () => { @@ -739,7 +739,7 @@ suite('SQLite Storage Library', () => { await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('lots of INSERT & DELETE (below inline max)', async () => { @@ -771,7 +771,7 @@ suite('SQLite Storage Library', () => { await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); test('lots of INSERT & DELETE (above inline max)', async () => { @@ -803,6 +803,6 @@ suite('SQLite Storage Library', () => { await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); }); diff --git a/src/vs/base/test/node/utils.ts b/src/vs/base/test/node/utils.ts index e79ef08cdf..00f0fb0d9d 100644 --- a/src/vs/base/test/node/utils.ts +++ b/src/vs/base/test/node/utils.ts @@ -6,7 +6,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, del } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; export interface ITestFileResult { testFile: string; @@ -22,7 +22,7 @@ export function testFile(folder: string, file: string): Promise return mkdirp(newDir, 493).then(() => { return { testFile, - cleanUp: () => del(parentDir, tmpdir()) - } as ITestFileResult; + cleanUp: () => rimraf(parentDir, RimRafMode.MOVE) + }; }); } diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 44c58a46c6..ded8df0ffe 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -40,6 +40,7 @@ import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { normalizeGitHubUrl } from 'vs/code/electron-browser/issue/issueReporterUtil'; import { Button } from 'vs/base/browser/ui/button/button'; import { withUndefinedAsNull } from 'vs/base/common/types'; +import { SystemInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; const MAX_URL_LENGTH = platform.isWindows ? 2081 : 5400; @@ -79,11 +80,12 @@ export class IssueReporter extends Disposable { this.initServices(configuration); + const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION; this.issueReporterModel = new IssueReporterModel({ issueType: configuration.data.issueType || IssueType.Bug, versionInfo: { vscodeVersion: `${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`, - os: `${os.type()} ${os.arch()} ${os.release()}` + os: `${os.type()} ${os.arch()} ${os.release()}${isSnap ? ' snap' : ''}` }, extensionsDisabled: !!this.environmentService.disableExtensions, }); @@ -104,7 +106,7 @@ export class IssueReporter extends Disposable { this.updatePreviewButtonState(); }); - ipcRenderer.on('vscode:issueSystemInfoResponse', (_: unknown, info: any) => { + ipcRenderer.on('vscode:issueSystemInfoResponse', (_: unknown, info: SystemInfo) => { this.logService.trace('issueReporter: Received system data'); this.issueReporterModel.update({ systemInfo: info }); this.receivedSystemInfo = true; @@ -903,19 +905,19 @@ export class IssueReporter extends Disposable { private updateSystemInfo(state: IssueReporterModelData) { const target = document.querySelector('.block-system .block-info'); if (target) { - let tableHtml = ''; - Object.keys(state.systemInfo).forEach(k => { - const data = typeof state.systemInfo[k] === 'object' - ? Object.keys(state.systemInfo[k]).map(key => `${key}: ${state.systemInfo[k][key]}`).join('
') - : state.systemInfo[k]; + const systemInfo = state.systemInfo!; + let renderedData = ` + + + + + + + + +
CPUs${systemInfo.cpus}
GPU Status${Object.keys(systemInfo.gpuStatus).map(key => `${key}: ${systemInfo.gpuStatus[key]}`).join('
')}
Load (avg)${systemInfo.load}
Memory (System)${systemInfo.memory}
Process Argv${systemInfo.processArgs}
Screen Reader${systemInfo.screenReader}
VM${systemInfo.vmHint}
`; - tableHtml += ` - - ${k} - ${data} - `; - }); - target.innerHTML = `${tableHtml}
`; + target.innerHTML = renderedData; } } diff --git a/src/vs/code/electron-browser/issue/issueReporterModel.ts b/src/vs/code/electron-browser/issue/issueReporterModel.ts index 78bb06dd93..c12fb1beb2 100644 --- a/src/vs/code/electron-browser/issue/issueReporterModel.ts +++ b/src/vs/code/electron-browser/issue/issueReporterModel.ts @@ -5,13 +5,14 @@ import { assign } from 'vs/base/common/objects'; import { IssueType, ISettingSearchResult, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; +import { SystemInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; export interface IssueReporterData { issueType: IssueType; issueDescription?: string; versionInfo?: any; - systemInfo?: any; + systemInfo?: SystemInfo; processInfo?: any; workspaceInfo?: any; @@ -149,13 +150,16 @@ ${this.getInfos()} |---|---| `; - Object.keys(this._data.systemInfo).forEach(k => { - const data = typeof this._data.systemInfo[k] === 'object' - ? Object.keys(this._data.systemInfo[k]).map(key => `${key}: ${this._data.systemInfo[k][key]}`).join('
') - : this._data.systemInfo[k]; + if (this._data.systemInfo) { - md += `|${k}|${data}|\n`; - }); + md += `|CPUs|${this._data.systemInfo.cpus}| +|GPU Status|${Object.keys(this._data.systemInfo.gpuStatus).map(key => `${key}: ${this._data.systemInfo!.gpuStatus[key]}`).join('
')}| +|Load (avg)|${this._data.systemInfo.load}| +|Memory (System)|${this._data.systemInfo.memory}| +|Process Argv|${this._data.systemInfo.processArgs}| +|Screen Reader|${this._data.systemInfo.screenReader}| +|VM|${this._data.systemInfo.vmHint}|`; + } md += '\n'; diff --git a/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts b/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts index 24022ebe5a..dd996606af 100644 --- a/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts +++ b/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts @@ -44,7 +44,13 @@ Extensions: none const issueReporterModel = new IssueReporterModel({ issueType: 0, systemInfo: { - 'GPU Status': { + os: 'Darwin', + cpus: 'Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)', + memory: '16.00GB', + vmHint: '0%', + processArgs: '', + screenReader: 'no', + gpuStatus: { '2d_canvas': 'enabled', 'checker_imaging': 'disabled_off' } @@ -65,8 +71,13 @@ OS version: undefined |Item|Value| |---|---| +|CPUs|Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (8 x 2800)| |GPU Status|2d_canvas: enabled
checker_imaging: disabled_off| - +|Load (avg)|undefined| +|Memory (System)|16.00GB| +|Process Argv|| +|Screen Reader|no| +|VM|0%| Extensions: none `); }); diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index 3dd7526f97..8e4b913dcb 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/processExplorer'; -import { listProcesses, ProcessItem } from 'vs/base/node/ps'; +import { listProcesses } from 'vs/base/node/ps'; import { webFrame, ipcRenderer, clipboard } from 'electron'; import { repeat } from 'vs/base/common/strings'; import { totalmem } from 'os'; @@ -15,6 +15,7 @@ import * as browser from 'vs/base/browser/browser'; 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'; let processList: any[]; let mapPidToWindowTitle = new Map(); diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts index dcf114a63e..290adb27d3 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts @@ -20,7 +20,7 @@ export class LogsDataCleaner extends Disposable { } private cleanUpOldLogsSoon(): void { - let handle: any = setTimeout(() => { + let handle: NodeJS.Timeout | undefined = setTimeout(() => { handle = undefined; const currentLog = basename(this.environmentService.logsPath); @@ -35,6 +35,11 @@ export class LogsDataCleaner extends Disposable { }).then(null, onUnexpectedError); }, 10 * 1000); - this._register(toDisposable(() => clearTimeout(handle))); + this._register(toDisposable(() => { + if (handle) { + clearTimeout(handle); + handle = undefined; + } + })); } } diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts index 3d89b05b3f..6c9973f789 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts @@ -5,7 +5,7 @@ import { basename, dirname, join } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { readdir, rimraf, stat } from 'vs/base/node/pfs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import product from 'vs/platform/product/node/product'; @@ -41,13 +41,13 @@ export class NodeCachedDataCleaner { const nodeCachedDataRootDir = dirname(this._environmentService.nodeCachedDataDir); const nodeCachedDataCurrent = basename(this._environmentService.nodeCachedDataDir); - let handle: any = setTimeout(() => { + let handle: NodeJS.Timeout | undefined = setTimeout(() => { handle = undefined; readdir(nodeCachedDataRootDir).then(entries => { const now = Date.now(); - const deletes: Promise[] = []; + const deletes: Promise[] = []; entries.forEach(entry => { // name check @@ -76,8 +76,11 @@ export class NodeCachedDataCleaner { }, 30 * 1000); - this._disposables.push({ - dispose() { clearTimeout(handle); } - }); + this._disposables.push(toDisposable(() => { + if (handle) { + clearTimeout(handle); + handle = undefined; + } + })); } } diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts index 0bbde5f5a1..db03b8f82c 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts @@ -24,7 +24,7 @@ export class StorageDataCleaner extends Disposable { } private cleanUpStorageSoon(): void { - let handle: any = setTimeout(() => { + let handle: NodeJS.Timeout | undefined = setTimeout(() => { handle = undefined; // Leverage the backup workspace file to find out which empty workspace is currently in use to @@ -52,6 +52,11 @@ export class StorageDataCleaner extends Disposable { }).then(null, onUnexpectedError); }, 30 * 1000); - this._register(toDisposable(() => clearTimeout(handle))); + this._register(toDisposable(() => { + if (handle) { + clearTimeout(handle); + handle = undefined; + } + })); } } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index e1a930b7f6..81e3adba08 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -44,7 +44,7 @@ import { isUndefinedOrNull, withUndefinedAsNull } from 'vs/base/common/types'; import { KeyboardLayoutMonitor } from 'vs/code/electron-main/keyboard'; import { URI } from 'vs/base/common/uri'; import { WorkspacesChannel } from 'vs/platform/workspaces/node/workspacesIpc'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesMainService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { getMachineId } from 'vs/base/node/id'; import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; @@ -53,7 +53,7 @@ import { IIssueService } from 'vs/platform/issue/common/issue'; import { IssueChannel } from 'vs/platform/issue/node/issueIpc'; import { IssueService } from 'vs/platform/issue/electron-main/issueService'; import { LogLevelSetterChannel } from 'vs/platform/log/node/logIpc'; -import * as errors from 'vs/base/common/errors'; +import { setUnexpectedErrorHandler, onUnexpectedError } from 'vs/base/common/errors'; import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener'; import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; import { connectRemoteAgentManagement, ManagementPersistentConnection, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -81,6 +81,8 @@ import { URLService } from 'vs/platform/url/common/urlService'; import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { nodeWebSocketFactory } from 'vs/platform/remote/node/nodeWebSocketFactory'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { statSync } from 'fs'; export class CodeApplication extends Disposable { @@ -114,9 +116,9 @@ export class CodeApplication extends Disposable { private registerListeners(): void { // We handle uncaught exceptions here to prevent electron from opening a dialog to the user - errors.setUnexpectedErrorHandler(err => this.onUnexpectedError(err)); + setUnexpectedErrorHandler(err => this.onUnexpectedError(err)); process.on('uncaughtException', err => this.onUnexpectedError(err)); - process.on('unhandledRejection', (reason: any, promise: Promise) => errors.onUnexpectedError(reason)); + process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason)); // Contextmenu via IPC support registerContextMenuListener(); @@ -141,7 +143,7 @@ export class CodeApplication extends Disposable { // Security related measures (https://electronjs.org/docs/tutorial/security) // DO NOT CHANGE without consulting the documentation - app.on('web-contents-created', (event: any, contents) => { + app.on('web-contents-created', (event: Electron.Event, contents) => { contents.on('will-attach-webview', (event: Electron.Event, webPreferences, params) => { const isValidWebviewSource = (source: string): boolean => { @@ -190,13 +192,13 @@ export class CodeApplication extends Disposable { }); let macOpenFileURIs: IURIToOpen[] = []; - let runningTimeout: any = null; + let runningTimeout: NodeJS.Timeout | null = null; app.on('open-file', (event: Event, path: string) => { this.logService.trace('App#open-file: ', path); event.preventDefault(); // Keep in array because more might come! - macOpenFileURIs.push({ uri: URI.file(path) }); + macOpenFileURIs.push(getURIToOpenFromPathSync(path)); // Clear previous handler if any if (runningTimeout !== null) { @@ -245,7 +247,7 @@ export class CodeApplication extends Disposable { }); }); - ipc.on('vscode:broadcast', (event: Event, windowId: number, broadcast: { channel: string; payload: any; }) => { + ipc.on('vscode:broadcast', (event: Event, windowId: number, broadcast: { channel: string; payload: object; }) => { if (this.windowsMainService && broadcast.channel && !isUndefinedOrNull(broadcast.payload)) { this.logService.trace('IPC#vscode:broadcast', broadcast.channel, broadcast.payload); @@ -257,6 +259,13 @@ export class CodeApplication extends Disposable { } }); + ipc.on('vscode:extensionHostDebug', (_: Event, windowId: number, broadcast: any) => { + if (this.windowsMainService) { + // Send to all windows (except sender window) + this.windowsMainService.sendToAll('vscode:extensionHostDebug', broadcast, [windowId]); + } + }); + ipc.on('vscode:toggleDevTools', (event: Event) => event.sender.toggleDevTools()); ipc.on('vscode:openDevTools', (event: Event) => event.sender.openDevTools()); @@ -290,7 +299,7 @@ export class CodeApplication extends Disposable { } } - private onBroadcast(event: string, payload: any): void { + private onBroadcast(event: string, payload: object): void { // Theme changes if (event === 'vscode:changeColorTheme' && typeof payload === 'string') { @@ -571,7 +580,7 @@ export class CodeApplication extends Disposable { this.windowsMainService.ready(this.userEnv); // Open our first window - const macOpenFiles = (global).macOpenFiles as string[]; + const macOpenFiles: string[] = (global).macOpenFiles; const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; const hasCliArgs = hasArgs(args._); const hasFolderURIs = hasArgs(args['folder-uri']); @@ -597,7 +606,7 @@ export class CodeApplication extends Disposable { return this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, - urisToOpen: macOpenFiles.map(file => ({ uri: URI.file(file) })), + urisToOpen: macOpenFiles.map(getURIToOpenFromPathSync), noRecentEntry, waitMarkerFileURI, initialStartup: true @@ -715,7 +724,7 @@ export class CodeApplication extends Disposable { } const resolvedAuthorities = new Map(); - ipc.on('vscode:remoteAuthorityResolved', (event: any, data: ResolvedAuthority) => { + ipc.on('vscode:remoteAuthorityResolved', (event: Electron.Event, data: ResolvedAuthority) => { this.logService.info('Received resolved authority', data.authority); resolvedAuthorities.set(data.authority, data); // Make sure to close and remove any existing connections @@ -763,15 +772,28 @@ export class CodeApplication extends Disposable { const channel = rawClient.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); // TODO@alex don't use call directly, wrap it around a `RemoteExtensionsFileSystemProvider` - const fileContents = await channel.call('readFile', [uri]); - callback(Buffer.from(fileContents)); + const fileContents = await channel.call('readFile', [uri]); + callback(fileContents.buffer); } else { callback(undefined); } } catch (err) { - errors.onUnexpectedError(err); + onUnexpectedError(err); callback(undefined); } }); } } + +function getURIToOpenFromPathSync(path: string): IURIToOpen { + try { + const fileStat = statSync(path); + if (fileStat.isDirectory()) { + return { folderUri: URI.file(path) }; + } else if (hasWorkspaceFileExtension(path)) { + return { workspaceUri: URI.file(path) }; + } + } catch (error) { + } + return { fileUri: URI.file(path) }; +} diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index b56771c5fc..b8caac5c9f 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -125,7 +125,7 @@ function setupIPC(accessor: ServicesAccessor): Promise { // Show a warning dialog after some timeout if it takes long to talk to the other instance // Skip this if we are running with --wait where it is expected that we wait for a while. // Also skip when gathering diagnostics (--status) which can take a longer time. - let startupWarningDialogHandle: any; + let startupWarningDialogHandle: NodeJS.Timeout; if (!environmentService.wait && !environmentService.status && !environmentService.args['upload-logs']) { startupWarningDialogHandle = setTimeout(() => { showStartupWarningDialog( @@ -140,12 +140,10 @@ function setupIPC(accessor: ServicesAccessor): Promise { // Process Info if (environmentService.args.status) { - return service.getMainProcessInfo().then(info => { - return instantiationService.invokeFunction(accessor => { - return accessor.get(IDiagnosticsService).getDiagnostics(info).then(diagnostics => { - console.log(diagnostics); - return Promise.reject(new ExpectedError()); - }); + return instantiationService.invokeFunction(accessor => { + return accessor.get(IDiagnosticsService).getDiagnostics(service).then(diagnostics => { + console.log(diagnostics); + return Promise.reject(new ExpectedError()); }); }); } @@ -316,17 +314,17 @@ function createServices(args: ParsedArgs, bufferLogService: BufferLogService): I return new InstantiationService(services, true); } -function initServices(environmentService: IEnvironmentService, stateService: StateService): Promise { +function initServices(environmentService: IEnvironmentService, stateService: StateService): Promise { // Ensure paths for environment service exist - const environmentServiceInitialization = Promise.all([ + const environmentServiceInitialization = Promise.all([ environmentService.extensionsPath, environmentService.nodeCachedDataDir, environmentService.logsPath, environmentService.globalStorageHome, environmentService.workspaceStorageHome, environmentService.backupHome - ].map((path): undefined | Promise => path ? mkdirp(path) : undefined)); + ].map((path): undefined | Promise => path ? mkdirp(path) : undefined)); // State service const stateServiceInitialization = stateService.init(); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 82642d4bf8..daecb89464 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -26,6 +26,7 @@ import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/node/ import { getBackgroundColor } from 'vs/code/electron-main/theme'; import { RunOnceScheduler } from 'vs/base/common/async'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { endsWith } from 'vs/base/common/strings'; export interface IWindowCreationOptions { state: IWindowState; @@ -61,7 +62,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private static readonly MAX_URL_LENGTH = 2 * 1024 * 1024; // https://cs.chromium.org/chromium/src/url/url_constants.cc?l=32 private hiddenTitleBarStyle: boolean; - private showTimeoutHandle: any; + private showTimeoutHandle: NodeJS.Timeout; private _id: number; private _win: Electron.BrowserWindow; private _lastFocusTime: number; @@ -314,7 +315,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; - this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details: any, cb: any) => { + this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => { this.marketplaceHeadersPromise.then(headers => { const requestHeaders = objects.assign(details.requestHeaders, headers); cb({ cancel: false, requestHeaders }); @@ -328,7 +329,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => { if (details.url.indexOf('.svg') > 0) { const uri = URI.parse(details.url); - if (uri && !uri.scheme.match(/file/i) && (uri.path as any).endsWith('.svg')) { + if (uri && !uri.scheme.match(/file/i) && endsWith(uri.path, '.svg')) { return callback({ cancel: true }); } } @@ -336,8 +337,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { return callback({}); }); - this._win.webContents.session.webRequest.onHeadersReceived(null!, (details: any, callback: any) => { - const contentType: string[] = (details.responseHeaders['content-type'] || details.responseHeaders['Content-Type']) as any; + this._win.webContents.session.webRequest.onHeadersReceived(null!, (details, callback) => { + const contentType: string[] = (details.responseHeaders['content-type'] || details.responseHeaders['Content-Type']); if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { return callback({ cancel: true }); } @@ -1048,9 +1049,16 @@ export class CodeWindow extends Disposable implements ICodeWindow { } } + let title: string; + if (typeof item.title === 'string') { + title = item.title; + } else { + title = item.title.value; + } + return { id: item.id, - label: !icon ? item.title as string : undefined, + label: !icon ? title : undefined, icon }; }); diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 9197c41bb3..689c15703b 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -7,33 +7,33 @@ import * as fs from 'fs'; import { basename, normalize, join, dirname } from 'vs/base/common/path'; import { localize } from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; -import { assign, mixin, equals } from 'vs/base/common/objects'; +import { assign, mixin } from 'vs/base/common/objects'; import { IBackupMainService, IEmptyWindowBackupInfo } from 'vs/platform/backup/common/backup'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { IStateService } from 'vs/platform/state/common/state'; import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; import { hasArgs, asArray } from 'vs/platform/environment/node/argv'; -import { ipcMain as ipc, screen, BrowserWindow, dialog, systemPreferences } from 'electron'; +import { ipcMain as ipc, screen, BrowserWindow, dialog, systemPreferences, FileFilter } from 'electron'; import { parseLineAndColumnAware } from 'vs/code/node/paths'; import { ILifecycleService, UnloadReason, LifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, IPathsToWaitFor, IEnterWorkspaceResult, IMessageBoxResult, INewWindowOptions, IURIToOpen, URIType, OpenDialogOptions } from 'vs/platform/windows/common/windows'; +import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, IPathsToWaitFor, IEnterWorkspaceResult, IMessageBoxResult, INewWindowOptions, IURIToOpen, isFileToOpen, isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri } from 'vs/code/node/windowsFinder'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/node/product'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IHistoryMainService, IRecent } from 'vs/platform/history/common/history'; import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; import { normalizeNFC } from 'vs/base/common/normalization'; import { URI } from 'vs/base/common/uri'; import { Queue } from 'vs/base/common/async'; -import { exists } from 'vs/base/node/pfs'; +import { exists, dirExists } from 'vs/base/node/pfs'; import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, originalFSPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/code/electron-main/windowsStateStorage'; @@ -87,7 +87,6 @@ interface IOpenBrowserWindowOptions { interface IPathParseOptions { ignoreFileNotFound?: boolean; gotoLineMode?: boolean; - forceOpenWorkspaceAsFile?: boolean; remoteAuthority?: string; } @@ -206,7 +205,7 @@ export class WindowsManager implements IWindowsMainService { this.windowsState.openedWindows = []; } - this.dialogs = new Dialogs(environmentService, telemetryService, stateService, this); + this.dialogs = new Dialogs(stateService, this); this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, this); } @@ -219,7 +218,7 @@ export class WindowsManager implements IWindowsMainService { private registerListeners(): void { // React to workbench ready events from windows - ipc.on('vscode:workbenchReady', (event: any, windowId: number) => { + ipc.on('vscode:workbenchReady', (event: Electron.Event, windowId: number) => { this.logService.trace('IPC#vscode-workbenchReady'); const win = this.getWindowById(windowId); @@ -843,26 +842,26 @@ export class WindowsManager implements IWindowsMainService { private doExtractPathsFromAPI(openConfig: IOpenConfiguration): IPathToOpen[] { const pathsToOpen: IPathToOpen[] = []; const cli = openConfig.cli; - const parseOptions: IPathParseOptions = { gotoLineMode: cli && cli.goto, forceOpenWorkspaceAsFile: openConfig.forceOpenWorkspaceAsFile }; + const parseOptions: IPathParseOptions = { gotoLineMode: cli && cli.goto }; for (const pathToOpen of openConfig.urisToOpen || []) { if (!pathToOpen) { continue; } - const path = this.parseUri(pathToOpen.uri, pathToOpen.typeHint, parseOptions); + const path = this.parseUri(pathToOpen, parseOptions); if (path) { path.label = pathToOpen.label; pathsToOpen.push(path); } else { - + const uri = resourceFromURIToOpen(pathToOpen); // Warn about the invalid URI or path let message, detail; - if (pathToOpen.uri.scheme === Schemas.file) { + if (uri.scheme === Schemas.file) { message = localize('pathNotExistTitle', "Path does not exist"); - detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", pathToOpen.uri.fsPath); + detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", uri.fsPath); } else { message = localize('uriInvalidTitle', "URI can not be opened"); - detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", pathToOpen.uri.toString()); + detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()); } const options: Electron.MessageBoxOptions = { title: product.nameLong, @@ -885,19 +884,25 @@ export class WindowsManager implements IWindowsMainService { // folder uris const folderUris = asArray(cli['folder-uri']); - for (let folderUri of folderUris) { - const path = this.parseUri(this.argToUri(folderUri), 'folder', parseOptions); - if (path) { - pathsToOpen.push(path); + for (let f of folderUris) { + const folderUri = this.argToUri(f); + if (folderUri) { + const path = this.parseUri({ folderUri }, parseOptions); + if (path) { + pathsToOpen.push(path); + } } } // file uris const fileUris = asArray(cli['file-uri']); - for (let fileUri of fileUris) { - const path = this.parseUri(this.argToUri(fileUri), 'file'); - if (path) { - pathsToOpen.push(path); + for (let f of fileUris) { + const fileUri = this.argToUri(f); + if (fileUri) { + const path = this.parseUri({ fileUri }, parseOptions); + if (path) { + pathsToOpen.push(path); + } } } @@ -944,12 +949,12 @@ export class WindowsManager implements IWindowsMainService { const windowsToOpen: IPathToOpen[] = []; for (const openedWindow of openedWindows) { if (openedWindow.workspace) { // Workspaces - const pathToOpen = this.parseUri(openedWindow.workspace.configPath, 'file', { remoteAuthority: openedWindow.remoteAuthority }); + const pathToOpen = this.parseUri({ workspaceUri: openedWindow.workspace.configPath }, { remoteAuthority: openedWindow.remoteAuthority }); if (pathToOpen && pathToOpen.workspace) { windowsToOpen.push(pathToOpen); } } else if (openedWindow.folderUri) { // Folders - const pathToOpen = this.parseUri(openedWindow.folderUri, 'folder', { remoteAuthority: openedWindow.remoteAuthority }); + const pathToOpen = this.parseUri({ folderUri: openedWindow.folderUri }, { remoteAuthority: openedWindow.remoteAuthority }); if (pathToOpen && pathToOpen.folderUri) { windowsToOpen.push(pathToOpen); } @@ -975,7 +980,7 @@ export class WindowsManager implements IWindowsMainService { restoreWindows = 'all'; // always reopen all windows when an update was applied } else { const windowConfig = this.configurationService.getValue('window'); - restoreWindows = ((windowConfig && windowConfig.restoreWindows) || 'one') as RestoreWindowsSetting; + restoreWindows = ((windowConfig && windowConfig.restoreWindows) || 'one'); if (['all', 'folders', 'one', 'none'].indexOf(restoreWindows) === -1) { restoreWindows = 'one'; @@ -999,12 +1004,13 @@ export class WindowsManager implements IWindowsMainService { return undefined; } - private parseUri(uri: URI | undefined, typeHint?: URIType, options: IPathParseOptions = {}): IPathToOpen | undefined { - if (!uri || !uri.scheme) { + private parseUri(uriToOpen: IURIToOpen, options: IPathParseOptions = {}): IPathToOpen | undefined { + if (!uriToOpen) { return undefined; } + let uri = resourceFromURIToOpen(uriToOpen); if (uri.scheme === Schemas.file) { - return this.parsePath(uri.fsPath, options); + return this.parsePath(uri.fsPath, options, isFileToOpen(uriToOpen)); } // open remote if either specified in the cli or if it's a remotehost URI @@ -1013,21 +1019,12 @@ export class WindowsManager implements IWindowsMainService { // normalize URI uri = normalizePath(uri); - // remove trailing slash if (hasTrailingPathSeparator(uri)) { uri = removeTrailingPathSeparator(uri); - if (!typeHint) { - typeHint = 'folder'; - } } - // if there's no type hint - if (!typeHint && (hasWorkspaceFileExtension(uri.path) || options.gotoLineMode)) { - typeHint = 'file'; - } - - if (typeHint === 'file') { + if (isFileToOpen(uriToOpen)) { if (options.gotoLineMode) { const parsedPath = parseLineAndColumnAware(uri.path); return { @@ -1037,16 +1034,15 @@ export class WindowsManager implements IWindowsMainService { remoteAuthority }; } - if (hasWorkspaceFileExtension(uri.path) && !options.forceOpenWorkspaceAsFile) { - return { - workspace: getWorkspaceIdentifier(uri), - remoteAuthority - }; - } return { fileUri: uri, remoteAuthority }; + } else if (isWorkspaceToOpen(uriToOpen)) { + return { + workspace: getWorkspaceIdentifier(uri), + remoteAuthority + }; } return { folderUri: uri, @@ -1054,7 +1050,7 @@ export class WindowsManager implements IWindowsMainService { }; } - private parsePath(anyPath: string, options: IPathParseOptions): IPathToOpen | undefined { + private parsePath(anyPath: string, options: IPathParseOptions, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { if (!anyPath) { return undefined; } @@ -1079,7 +1075,7 @@ export class WindowsManager implements IWindowsMainService { if (candidateStat.isFile()) { // Workspace (unless disabled via flag) - if (!options.forceOpenWorkspaceAsFile) { + if (!forceOpenWorkspaceAsFile) { const workspace = this.workspacesMainService.resolveLocalWorkspaceSync(URI.file(candidate)); if (workspace) { return { workspace: { id: workspace.id, configPath: workspace.configPath }, remoteAuthority: workspace.remoteAuthority }; @@ -1452,7 +1448,7 @@ export class WindowsManager implements IWindowsMainService { // Compute x/y based on display bounds // Note: important to use Math.round() because Electron does not seem to be too happy about // display coordinates that are not absolute numbers. - let state = defaultWindowState() as INewWindowState; + let state = defaultWindowState(); state.x = Math.round(displayToUse.bounds.x + (displayToUse.bounds.width / 2) - (state.width! / 2)); state.y = Math.round(displayToUse.bounds.y + (displayToUse.bounds.height / 2) - (state.height! / 2)); @@ -1482,7 +1478,7 @@ export class WindowsManager implements IWindowsMainService { state = this.ensureNoOverlap(state); } - state.hasDefaultState = true; // flag as default state + (state as INewWindowState).hasDefaultState = true; // flag as default state return state; } @@ -1537,10 +1533,6 @@ export class WindowsManager implements IWindowsMainService { return result; } - pickWorkspaceAndOpen(options: INativeOpenDialogOptions): void { - this.workspacesManager.pickWorkspaceAndOpen(options); - } - focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow { const lastActive = this.getLastActiveWindow(); if (lastActive) { @@ -1717,49 +1709,83 @@ export class WindowsManager implements IWindowsMainService { this._onWindowClose.fire(win.id); } - pickFileFolderAndOpen(options: INativeOpenDialogOptions): void { - this.doPickAndOpen(options, true /* pick folders */, true /* pick files */); + async pickFileFolderAndOpen(options: INativeOpenDialogOptions): Promise { + const title = localize('open', "Open"); + const paths = await this.dialogs.pick({ ...options, pickFolders: true, pickFiles: true, title }); + if (paths) { + this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFileFolder', options.telemetryExtraData); + const urisToOpen = await Promise.all(paths.map(path => { + return dirExists(path).then(isDir => isDir ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) }); + })); + this.open({ + context: OpenContext.DIALOG, + contextWindowId: options.windowId, + cli: this.environmentService.args, + urisToOpen, + forceNewWindow: options.forceNewWindow + }); + } } - pickFolderAndOpen(options: INativeOpenDialogOptions): void { - this.doPickAndOpen(options, true /* pick folders */, false /* pick files */); + async pickFolderAndOpen(options: INativeOpenDialogOptions): Promise { + const title = localize('openFolder', "Open Folder"); + const paths = await this.dialogs.pick({ ...options, pickFolders: true, title }); + if (paths) { + this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFolder', options.telemetryExtraData); + this.open({ + context: OpenContext.DIALOG, + contextWindowId: options.windowId, + cli: this.environmentService.args, + urisToOpen: paths.map(path => ({ folderUri: URI.file(path) })), + forceNewWindow: options.forceNewWindow + }); + } } - pickFileAndOpen(options: INativeOpenDialogOptions): void { - this.doPickAndOpen(options, false /* pick folders */, true /* pick files */); + async pickFileAndOpen(options: INativeOpenDialogOptions): Promise { + const title = localize('openFile', "Open File"); + const paths = await this.dialogs.pick({ ...options, pickFiles: true, title }); + if (paths) { + this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFile', options.telemetryExtraData); + this.open({ + context: OpenContext.DIALOG, + contextWindowId: options.windowId, + cli: this.environmentService.args, + urisToOpen: paths.map(path => ({ fileUri: URI.file(path) })), + forceNewWindow: options.forceNewWindow + }); + } } - private doPickAndOpen(options: INativeOpenDialogOptions, pickFolders: boolean, pickFiles: boolean): void { - const internalOptions = options as IInternalNativeOpenDialogOptions; - - internalOptions.pickFolders = pickFolders; - internalOptions.pickFiles = pickFiles; - - const dialogOptions: OpenDialogOptions = internalOptions.dialogOptions || Object.create(null); - internalOptions.dialogOptions = dialogOptions; - - if (!internalOptions.dialogOptions.title) { - if (pickFolders && pickFiles) { - internalOptions.dialogOptions.title = localize('open', "Open"); - } else if (pickFolders) { - internalOptions.dialogOptions.title = localize('openFolder', "Open Folder"); - } else { - internalOptions.dialogOptions.title = localize('openFile', "Open File"); - } + async pickWorkspaceAndOpen(options: INativeOpenDialogOptions): Promise { + const title = localize('openWorkspaceTitle', "Open Workspace"); + const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")); + const filters = WORKSPACE_FILTER; + const paths = await this.dialogs.pick({ ...options, pickFiles: true, title, filters, buttonLabel }); + if (paths) { + this.sendPickerTelemetry(paths, options.telemetryEventName || 'openWorkspace', options.telemetryExtraData); + this.open({ + context: OpenContext.DIALOG, + contextWindowId: options.windowId, + cli: this.environmentService.args, + urisToOpen: paths.map(path => ({ workspaceUri: URI.file(path) })), + forceNewWindow: options.forceNewWindow + }); } - if (!internalOptions.telemetryEventName) { - if (pickFolders && pickFiles) { - // __GDPR__TODO__ classify event - internalOptions.telemetryEventName = 'openFileFolder'; - } else if (pickFolders) { - internalOptions.telemetryEventName = 'openFolder'; - } else { - internalOptions.telemetryEventName = 'openFile'; - } - } + } - this.dialogs.pickAndOpen(internalOptions); + private sendPickerTelemetry(paths: string[], telemetryEventName: string, telemetryExtraData?: ITelemetryData) { + + const numberOfPaths = paths ? paths.length : 0; + + // Telemetry + // __GDPR__TODO__ Dynamic event names and dynamic properties. Can not be registered statically. + this.telemetryService.publicLog(telemetryEventName, { + ...telemetryExtraData, + outcome: numberOfPaths ? 'success' : 'canceled', + numberOfPaths + }); } showMessageBox(options: Electron.MessageBoxOptions, win?: ICodeWindow): Promise { @@ -1793,65 +1819,42 @@ export class WindowsManager implements IWindowsMainService { } interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions { + pickFolders?: boolean; pickFiles?: boolean; + + title: string; + buttonLabel?: string; + filters?: FileFilter[]; } class Dialogs { private static readonly workingDirPickerStorageKey = 'pickerWorkingDir'; - private readonly mapWindowToDialogQueue: Map>; - private readonly noWindowDialogQueue: Queue; + private readonly mapWindowToDialogQueue: Map>; + private readonly noWindowDialogQueue: Queue; constructor( - private readonly environmentService: IEnvironmentService, - private readonly telemetryService: ITelemetryService, private readonly stateService: IStateService, - private readonly windowsMainService: IWindowsMainService, + private readonly windowsMainService: IWindowsMainService ) { - this.mapWindowToDialogQueue = new Map>(); - this.noWindowDialogQueue = new Queue(); + this.mapWindowToDialogQueue = new Map>(); + this.noWindowDialogQueue = new Queue(); } - pickAndOpen(options: INativeOpenDialogOptions): void { - this.getFileOrFolderUris(options).then(paths => { - const numberOfPaths = paths ? paths.length : 0; - - // Telemetry - if (options.telemetryEventName) { - // __GDPR__TODO__ Dynamic event names and dynamic properties. Can not be registered statically. - this.telemetryService.publicLog(options.telemetryEventName, { - ...options.telemetryExtraData, - outcome: numberOfPaths ? 'success' : 'canceled', - numberOfPaths - }); - } - - // Open - if (numberOfPaths) { - this.windowsMainService.open({ - context: OpenContext.DIALOG, - contextWindowId: options.windowId, - cli: this.environmentService.args, - urisToOpen: paths, - forceNewWindow: options.forceNewWindow, - forceOpenWorkspaceAsFile: options.dialogOptions && !equals(options.dialogOptions.filters, WORKSPACE_FILTER) - }); - } - }); - } - - private getFileOrFolderUris(options: IInternalNativeOpenDialogOptions): Promise { + pick(options: IInternalNativeOpenDialogOptions): Promise { // Ensure dialog options - const dialogOptions = options.dialogOptions || Object.create(null); - options.dialogOptions = dialogOptions; + const dialogOptions: Electron.OpenDialogOptions = { + title: options.title, + buttonLabel: options.buttonLabel, + filters: options.filters + }; // Ensure defaultPath - if (!dialogOptions.defaultPath) { - dialogOptions.defaultPath = this.stateService.getItem(Dialogs.workingDirPickerStorageKey); - } + dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem(Dialogs.workingDirPickerStorageKey); + // Ensure properties if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') { @@ -1878,13 +1881,7 @@ class Dialogs { // Remember path in storage for next time this.stateService.setItem(Dialogs.workingDirPickerStorageKey, dirname(paths[0])); - - const result: IURIToOpen[] = []; - for (const path of paths) { - result.push({ uri: URI.file(path) }); - } - - return result; + return paths; } return undefined; @@ -2057,21 +2054,13 @@ class WorkspacesManager { return { workspace, backupPath }; } - pickWorkspaceAndOpen(options: INativeOpenDialogOptions): void { - const window = (typeof options.windowId === 'number' ? this.windowsMainService.getWindowById(options.windowId) : undefined) || this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); - - this.windowsMainService.pickFileAndOpen({ - windowId: window ? window.id : undefined, - dialogOptions: { - buttonLabel: mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")), - title: localize('openWorkspaceTitle', "Open Workspace"), - filters: WORKSPACE_FILTER, - properties: ['openFile'], - defaultPath: options.dialogOptions && options.dialogOptions.defaultPath - }, - forceNewWindow: options.forceNewWindow, - telemetryEventName: options.telemetryEventName, - telemetryExtraData: options.telemetryExtraData - }); - } } + +function resourceFromURIToOpen(u: IURIToOpen) { + if (isWorkspaceToOpen(u)) { + return u.workspaceUri; + } else if (isFolderToOpen(u)) { + return u.folderUri; + } + return u.fileUri; +} \ No newline at end of file diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 1cb0c26e96..0c049c37ff 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -13,11 +13,10 @@ import pkg from 'vs/platform/product/node/package'; import * as paths from 'vs/base/common/path'; import * as os from 'os'; import * as fs from 'fs'; -import { whenDeleted } from 'vs/base/node/pfs'; +import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; import { resolveTerminalEncoding } from 'vs/base/node/encoding'; import * as iconv from 'iconv-lite'; -import { writeFileAndFlushSync } from 'vs/base/node/extfs'; import { isWindows } from 'vs/base/common/platform'; import { ProfilingSession, Target } from 'v8-inspect-profiler'; @@ -98,9 +97,9 @@ export async function main(argv: string[]): Promise { // prevent removing alternate data streams // (see https://github.com/Microsoft/vscode/issues/6363) fs.truncateSync(target, 0); - writeFileAndFlushSync(target, data, { flag: 'r+' }); + writeFileSync(target, data, { flag: 'r+' }); } else { - writeFileAndFlushSync(target, data); + writeFileSync(target, data); } // Restore previous mode as needed diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 6d325e6a70..8e3d0cb7e2 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -76,7 +76,7 @@ export class Main { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService ) { } - async run(argv: ParsedArgs): Promise { + async run(argv: ParsedArgs): Promise { if (argv['install-source']) { await this.setInstallSource(argv['install-source']); @@ -95,11 +95,11 @@ export class Main { } } - private setInstallSource(installSource: string): Promise { + private setInstallSource(installSource: string): Promise { return writeFile(this.environmentService.installSourcePath, installSource.slice(0, 30)); } - private async listExtensions(showVersions: boolean): Promise { + private async listExtensions(showVersions: boolean): Promise { const extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); extensions.forEach(e => console.log(getId(e.manifest, showVersions))); } @@ -227,7 +227,7 @@ export class Main { } } - private async uninstallExtension(extensions: string[]): Promise { + private async uninstallExtension(extensions: string[]): Promise { async function getExtensionId(extensionDescription: string): Promise { if (!/\.vsix$/i.test(extensionDescription)) { return extensionDescription; diff --git a/src/vs/code/node/paths.ts b/src/vs/code/node/paths.ts index df64888dc2..414da1f06a 100644 --- a/src/vs/code/node/paths.ts +++ b/src/vs/code/node/paths.ts @@ -10,7 +10,6 @@ import * as extpath from 'vs/base/common/extpath'; import * as platform from 'vs/base/common/platform'; import * as types from 'vs/base/common/types'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; -import { sanitizeFilePath } from 'vs/base/node/extfs'; export function validatePaths(args: ParsedArgs): ParsedArgs { @@ -45,7 +44,7 @@ function doValidatePaths(args: string[], gotoLineMode?: boolean): string[] { pathCandidate = preparePath(cwd, pathCandidate); } - const sanitizedFilePath = sanitizeFilePath(pathCandidate, cwd); + const sanitizedFilePath = extpath.sanitizeFilePath(pathCandidate, cwd); const basename = path.basename(sanitizedFilePath); if (basename /* can be empty if code is opened on root */ && !extpath.isValidBasename(basename)) { diff --git a/src/vs/code/node/shellEnv.ts b/src/vs/code/node/shellEnv.ts index d3db137192..59a0955e6e 100644 --- a/src/vs/code/node/shellEnv.ts +++ b/src/vs/code/node/shellEnv.ts @@ -29,9 +29,9 @@ function getUnixShellEnvironment(): Promise { const buffers: Buffer[] = []; child.on('error', () => resolve({})); - child.stdout.on('data', b => buffers.push(b as Buffer)); + child.stdout.on('data', b => buffers.push(b)); - child.on('close', (code: number, signal: any) => { + child.on('close', code => { if (code !== 0) { return reject(new Error('Failed to get environment')); } diff --git a/src/vs/editor/browser/core/editorState.ts b/src/vs/editor/browser/core/editorState.ts index ada53b59a2..d0dde2218f 100644 --- a/src/vs/editor/browser/core/editorState.ts +++ b/src/vs/editor/browser/core/editorState.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as strings from 'vs/base/common/strings'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ITextModel } from 'vs/editor/common/model'; export const enum CodeEditorStateFlag { Value = 1, @@ -71,6 +74,56 @@ export class EditorState { } } +/** + * A cancellation token source that cancels when the editor changes as expressed + * by the provided flags + */ +export class EditorStateCancellationTokenSource extends CancellationTokenSource { + + private readonly _listener: IDisposable[] = []; + + constructor(readonly editor: IActiveCodeEditor, flags: CodeEditorStateFlag, parent?: CancellationToken) { + super(parent); + + if (flags & CodeEditorStateFlag.Position) { + this._listener.push(editor.onDidChangeCursorPosition(_ => this.cancel())); + } + if (flags & CodeEditorStateFlag.Selection) { + this._listener.push(editor.onDidChangeCursorSelection(_ => this.cancel())); + } + if (flags & CodeEditorStateFlag.Scroll) { + this._listener.push(editor.onDidScrollChange(_ => this.cancel())); + } + if (flags & CodeEditorStateFlag.Value) { + this._listener.push(editor.onDidChangeModel(_ => this.cancel())); + this._listener.push(editor.onDidChangeModelContent(_ => this.cancel())); + } + } + + dispose() { + dispose(this._listener); + super.dispose(); + } +} + +/** + * A cancellation token source that cancels when the provided model changes + */ +export class TextModelCancellationTokenSource extends CancellationTokenSource { + + private _listener: IDisposable; + + constructor(model: ITextModel, parent?: CancellationToken) { + super(parent); + this._listener = model.onDidChangeContent(() => this.cancel()); + } + + dispose() { + this._listener.dispose(); + super.dispose(); + } +} + export class StableEditorScrollState { public static capture(editor: ICodeEditor): StableEditorScrollState { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 984ca4f4db..ff182783df 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1628,8 +1628,6 @@ class EditorContextKeysManager extends Disposable { export class EditorModeContext extends Disposable { - private readonly _editor: CodeEditorWidget; - private readonly _langId: IContextKey; private readonly _hasCompletionItemProvider: IContextKey; private readonly _hasCodeActionsProvider: IContextKey; @@ -1651,37 +1649,36 @@ export class EditorModeContext extends Disposable { private readonly _isInWalkThrough: IContextKey; constructor( - editor: CodeEditorWidget, - contextKeyService: IContextKeyService + private readonly _editor: CodeEditorWidget, + private readonly _contextKeyService: IContextKeyService ) { super(); - this._editor = editor; - this._langId = EditorContextKeys.languageId.bindTo(contextKeyService); - this._hasCompletionItemProvider = EditorContextKeys.hasCompletionItemProvider.bindTo(contextKeyService); - this._hasCodeActionsProvider = EditorContextKeys.hasCodeActionsProvider.bindTo(contextKeyService); - this._hasCodeLensProvider = EditorContextKeys.hasCodeLensProvider.bindTo(contextKeyService); - this._hasDefinitionProvider = EditorContextKeys.hasDefinitionProvider.bindTo(contextKeyService); - this._hasDeclarationProvider = EditorContextKeys.hasDeclarationProvider.bindTo(contextKeyService); - this._hasImplementationProvider = EditorContextKeys.hasImplementationProvider.bindTo(contextKeyService); - this._hasTypeDefinitionProvider = EditorContextKeys.hasTypeDefinitionProvider.bindTo(contextKeyService); - this._hasHoverProvider = EditorContextKeys.hasHoverProvider.bindTo(contextKeyService); - this._hasDocumentHighlightProvider = EditorContextKeys.hasDocumentHighlightProvider.bindTo(contextKeyService); - this._hasDocumentSymbolProvider = EditorContextKeys.hasDocumentSymbolProvider.bindTo(contextKeyService); - this._hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(contextKeyService); - this._hasRenameProvider = EditorContextKeys.hasRenameProvider.bindTo(contextKeyService); - this._hasSignatureHelpProvider = EditorContextKeys.hasSignatureHelpProvider.bindTo(contextKeyService); - this._hasDocumentFormattingProvider = EditorContextKeys.hasDocumentFormattingProvider.bindTo(contextKeyService); - this._hasDocumentSelectionFormattingProvider = EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo(contextKeyService); - this._hasMultipleDocumentFormattingProvider = EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo(contextKeyService); - this._hasMultipleDocumentSelectionFormattingProvider = EditorContextKeys.hasMultipleDocumentSelectionFormattingProvider.bindTo(contextKeyService); - this._isInWalkThrough = EditorContextKeys.isInEmbeddedEditor.bindTo(contextKeyService); + this._langId = EditorContextKeys.languageId.bindTo(_contextKeyService); + this._hasCompletionItemProvider = EditorContextKeys.hasCompletionItemProvider.bindTo(_contextKeyService); + this._hasCodeActionsProvider = EditorContextKeys.hasCodeActionsProvider.bindTo(_contextKeyService); + this._hasCodeLensProvider = EditorContextKeys.hasCodeLensProvider.bindTo(_contextKeyService); + this._hasDefinitionProvider = EditorContextKeys.hasDefinitionProvider.bindTo(_contextKeyService); + this._hasDeclarationProvider = EditorContextKeys.hasDeclarationProvider.bindTo(_contextKeyService); + this._hasImplementationProvider = EditorContextKeys.hasImplementationProvider.bindTo(_contextKeyService); + this._hasTypeDefinitionProvider = EditorContextKeys.hasTypeDefinitionProvider.bindTo(_contextKeyService); + this._hasHoverProvider = EditorContextKeys.hasHoverProvider.bindTo(_contextKeyService); + this._hasDocumentHighlightProvider = EditorContextKeys.hasDocumentHighlightProvider.bindTo(_contextKeyService); + this._hasDocumentSymbolProvider = EditorContextKeys.hasDocumentSymbolProvider.bindTo(_contextKeyService); + this._hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(_contextKeyService); + this._hasRenameProvider = EditorContextKeys.hasRenameProvider.bindTo(_contextKeyService); + this._hasSignatureHelpProvider = EditorContextKeys.hasSignatureHelpProvider.bindTo(_contextKeyService); + this._hasDocumentFormattingProvider = EditorContextKeys.hasDocumentFormattingProvider.bindTo(_contextKeyService); + this._hasDocumentSelectionFormattingProvider = EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo(_contextKeyService); + this._hasMultipleDocumentFormattingProvider = EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo(_contextKeyService); + this._hasMultipleDocumentSelectionFormattingProvider = EditorContextKeys.hasMultipleDocumentSelectionFormattingProvider.bindTo(_contextKeyService); + this._isInWalkThrough = EditorContextKeys.isInEmbeddedEditor.bindTo(_contextKeyService); const update = () => this._update(); // update when model/mode changes - this._register(editor.onDidChangeModel(update)); - this._register(editor.onDidChangeModelLanguage(update)); + this._register(_editor.onDidChangeModel(update)); + this._register(_editor.onDidChangeModelLanguage(update)); // update when registries change this._register(modes.CompletionProviderRegistry.onDidChange(update)); @@ -1708,23 +1705,25 @@ export class EditorModeContext extends Disposable { } reset() { - this._langId.reset(); - this._hasCompletionItemProvider.reset(); - this._hasCodeActionsProvider.reset(); - this._hasCodeLensProvider.reset(); - this._hasDefinitionProvider.reset(); - this._hasDeclarationProvider.reset(); - this._hasImplementationProvider.reset(); - this._hasTypeDefinitionProvider.reset(); - this._hasHoverProvider.reset(); - this._hasDocumentHighlightProvider.reset(); - this._hasDocumentSymbolProvider.reset(); - this._hasReferenceProvider.reset(); - this._hasRenameProvider.reset(); - this._hasDocumentFormattingProvider.reset(); - this._hasDocumentSelectionFormattingProvider.reset(); - this._hasSignatureHelpProvider.reset(); - this._isInWalkThrough.reset(); + this._contextKeyService.bufferChangeEvents(() => { + this._langId.reset(); + this._hasCompletionItemProvider.reset(); + this._hasCodeActionsProvider.reset(); + this._hasCodeLensProvider.reset(); + this._hasDefinitionProvider.reset(); + this._hasDeclarationProvider.reset(); + this._hasImplementationProvider.reset(); + this._hasTypeDefinitionProvider.reset(); + this._hasHoverProvider.reset(); + this._hasDocumentHighlightProvider.reset(); + this._hasDocumentSymbolProvider.reset(); + this._hasReferenceProvider.reset(); + this._hasRenameProvider.reset(); + this._hasDocumentFormattingProvider.reset(); + this._hasDocumentSelectionFormattingProvider.reset(); + this._hasSignatureHelpProvider.reset(); + this._isInWalkThrough.reset(); + }); } private _update() { @@ -1733,25 +1732,27 @@ export class EditorModeContext extends Disposable { this.reset(); return; } - this._langId.set(model.getLanguageIdentifier().language); - this._hasCompletionItemProvider.set(modes.CompletionProviderRegistry.has(model)); - this._hasCodeActionsProvider.set(modes.CodeActionProviderRegistry.has(model)); - this._hasCodeLensProvider.set(modes.CodeLensProviderRegistry.has(model)); - this._hasDefinitionProvider.set(modes.DefinitionProviderRegistry.has(model)); - this._hasDeclarationProvider.set(modes.DeclarationProviderRegistry.has(model)); - this._hasImplementationProvider.set(modes.ImplementationProviderRegistry.has(model)); - this._hasTypeDefinitionProvider.set(modes.TypeDefinitionProviderRegistry.has(model)); - this._hasHoverProvider.set(modes.HoverProviderRegistry.has(model)); - this._hasDocumentHighlightProvider.set(modes.DocumentHighlightProviderRegistry.has(model)); - this._hasDocumentSymbolProvider.set(modes.DocumentSymbolProviderRegistry.has(model)); - this._hasReferenceProvider.set(modes.ReferenceProviderRegistry.has(model)); - this._hasRenameProvider.set(modes.RenameProviderRegistry.has(model)); - this._hasSignatureHelpProvider.set(modes.SignatureHelpProviderRegistry.has(model)); - this._hasDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.has(model) || modes.DocumentRangeFormattingEditProviderRegistry.has(model)); - this._hasDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.has(model)); - this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length > 1 || modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); - this._hasMultipleDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); - this._isInWalkThrough.set(model.uri.scheme === Schemas.walkThroughSnippet); + this._contextKeyService.bufferChangeEvents(() => { + this._langId.set(model.getLanguageIdentifier().language); + this._hasCompletionItemProvider.set(modes.CompletionProviderRegistry.has(model)); + this._hasCodeActionsProvider.set(modes.CodeActionProviderRegistry.has(model)); + this._hasCodeLensProvider.set(modes.CodeLensProviderRegistry.has(model)); + this._hasDefinitionProvider.set(modes.DefinitionProviderRegistry.has(model)); + this._hasDeclarationProvider.set(modes.DeclarationProviderRegistry.has(model)); + this._hasImplementationProvider.set(modes.ImplementationProviderRegistry.has(model)); + this._hasTypeDefinitionProvider.set(modes.TypeDefinitionProviderRegistry.has(model)); + this._hasHoverProvider.set(modes.HoverProviderRegistry.has(model)); + this._hasDocumentHighlightProvider.set(modes.DocumentHighlightProviderRegistry.has(model)); + this._hasDocumentSymbolProvider.set(modes.DocumentSymbolProviderRegistry.has(model)); + this._hasReferenceProvider.set(modes.ReferenceProviderRegistry.has(model)); + this._hasRenameProvider.set(modes.RenameProviderRegistry.has(model)); + this._hasSignatureHelpProvider.set(modes.SignatureHelpProviderRegistry.has(model)); + this._hasDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.has(model) || modes.DocumentRangeFormattingEditProviderRegistry.has(model)); + this._hasDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.has(model)); + this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length > 1 || modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); + this._hasMultipleDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); + this._isInWalkThrough.set(model.uri.scheme === Schemas.walkThroughSnippet); + }); } } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 4915e17222..4e5b14b4b7 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -894,11 +894,19 @@ export class TextModel extends Disposable implements model.ITextModel { * @param strict Do NOT allow a position inside a high-low surrogate pair */ private _isValidPosition(lineNumber: number, column: number, strict: boolean): boolean { - if (isNaN(lineNumber)) { + if (typeof lineNumber !== 'number' || typeof column !== 'number') { return false; } - if (lineNumber < 1) { + if (isNaN(lineNumber) || isNaN(column)) { + return false; + } + + if (lineNumber < 1 || column < 1) { + return false; + } + + if ((lineNumber | 0) !== lineNumber || (column | 0) !== column) { return false; } @@ -907,14 +915,6 @@ export class TextModel extends Disposable implements model.ITextModel { return false; } - if (isNaN(column)) { - return false; - } - - if (column < 1) { - return false; - } - const maxColumn = this.getLineMaxColumn(lineNumber); if (column > maxColumn) { return false; diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 0b5d8065f3..1c8c8e75fd 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -460,6 +460,11 @@ export interface CompletionItem { * A command that should be run upon acceptance of this item. */ command?: Command; + + /** + * @internal + */ + [key: string]: any; } export interface CompletionList { @@ -1265,19 +1270,19 @@ export interface CommentThread2 { resource: string | null; range: IRange; label: string; - comments: Comment[]; - onDidChangeComments: Event; + comments: Comment[] | undefined; + onDidChangeComments: Event; collapsibleState?: CommentThreadCollapsibleState; input?: CommentInput; onDidChangeInput: Event; acceptInputCommand?: Command; - additionalCommands: Command[]; + additionalCommands?: Command[]; deleteCommand?: Command; - onDidChangeAcceptInputCommand: Event; - onDidChangeAdditionalCommands: Event; + onDidChangeAcceptInputCommand: Event; + onDidChangeAdditionalCommands: Event; onDidChangeRange: Event; onDidChangeLabel: Event; - onDidChangeCollasibleState: Event; + onDidChangeCollasibleState: Event; } /** @@ -1299,7 +1304,7 @@ export interface CommentThread { threadId: string | null; resource: string | null; range: IRange; - comments: Comment[]; + comments: Comment[] | undefined; collapsibleState?: CommentThreadCollapsibleState; reply?: Command; } diff --git a/src/vs/editor/common/services/resourceConfiguration.ts b/src/vs/editor/common/services/resourceConfiguration.ts index a46e84a761..7b51da53fd 100644 --- a/src/vs/editor/common/services/resourceConfiguration.ts +++ b/src/vs/editor/common/services/resourceConfiguration.ts @@ -43,5 +43,5 @@ export interface ITextResourcePropertiesService { /** * Returns the End of Line characters for the given resource */ - getEOL(resource: URI | null | undefined, language?: string): string; + getEOL(resource: URI | undefined, language?: string): string; } \ No newline at end of file diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index be7a590b7f..a3e57504f3 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { equals, flatten, isNonEmptyArray, mergeSort } from 'vs/base/common/arrays'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; @@ -14,6 +14,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { CodeAction, CodeActionContext, CodeActionProviderRegistry, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CodeActionFilter, CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind } from './codeActionTrigger'; +import { TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState'; export class CodeActionSet { @@ -55,14 +56,12 @@ export function getCodeActions( trigger: trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic }; - const chainedCancellation = new CancellationTokenSource(); - token.onCancellationRequested(() => chainedCancellation.cancel()); - + const cts = new TextModelCancellationTokenSource(model, token); const providers = getCodeActionProviders(model, filter); const promises = providers.map(provider => { - return Promise.resolve(provider.provideCodeActions(model, rangeOrSelection, codeActionContext, chainedCancellation.token)).then(providedCodeActions => { - if (!Array.isArray(providedCodeActions)) { + return Promise.resolve(provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token)).then(providedCodeActions => { + if (cts.token.isCancellationRequested || !Array.isArray(providedCodeActions)) { return []; } return providedCodeActions.filter(action => action && filtersAction(filter, action)); @@ -79,7 +78,7 @@ export function getCodeActions( const listener = CodeActionProviderRegistry.onDidChange(() => { const newProviders = CodeActionProviderRegistry.all(model); if (!equals(newProviders, providers)) { - chainedCancellation.cancel(); + cts.cancel(); } }); @@ -88,6 +87,7 @@ export function getCodeActions( .then(actions => new CodeActionSet(actions)) .finally(() => { listener.dispose(); + cts.dispose(); }); } diff --git a/src/vs/editor/contrib/codelens/codeLensCache.ts b/src/vs/editor/contrib/codelens/codeLensCache.ts new file mode 100644 index 0000000000..0119def520 --- /dev/null +++ b/src/vs/editor/contrib/codelens/codeLensCache.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITextModel } from 'vs/editor/common/model'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ICodeLensData } from 'vs/editor/contrib/codelens/codelens'; +import { LRUCache, values } from 'vs/base/common/map'; +import { ICodeLensSymbol, CodeLensProvider } from 'vs/editor/common/modes'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { Range } from 'vs/editor/common/core/range'; + +export const ICodeLensCache = createDecorator('ICodeLensCache'); + +export interface ICodeLensCache { + _serviceBrand: any; + put(model: ITextModel, data: ICodeLensData[]): void; + get(model: ITextModel): ICodeLensData[] | undefined; + delete(model: ITextModel): void; +} + +interface ISerializedCacheData { + lineCount: number; + lines: number[]; +} + +class CacheItem { + + constructor( + readonly lineCount: number, + readonly data: ICodeLensData[] + ) { } +} + +class CodeLensCache implements ICodeLensCache { + + _serviceBrand: any; + + private readonly _fakeProvider = new class implements CodeLensProvider { + provideCodeLenses(): ICodeLensSymbol[] { + throw new Error('not supported'); + } + }; + + private readonly _cache = new LRUCache(20, 0.75); + + constructor(@IStorageService storageService: IStorageService) { + + const key = 'codelens/cache'; + + // restore lens data on start + const raw = storageService.get(key, StorageScope.WORKSPACE, '{}'); + this._deserialize(raw); + + // store lens data on shutdown + const listener = storageService.onWillSaveState(() => { + storageService.store(key, this._serialize(), StorageScope.WORKSPACE); + listener.dispose(); + }); + } + + put(model: ITextModel, data: ICodeLensData[]): void { + const item = new CacheItem(model.getLineCount(), data.map(item => { + return { + symbol: item.symbol, + provider: this._fakeProvider + }; + })); + this._cache.set(model.uri.toString(), item); + } + + get(model: ITextModel) { + const item = this._cache.get(model.uri.toString()); + return item && item.lineCount === model.getLineCount() ? item.data : undefined; + } + + delete(model: ITextModel): void { + this._cache.delete(model.uri.toString()); + } + + // --- persistence + + private _serialize(): string { + const data: Record = Object.create(null); + this._cache.forEach((value, key) => { + const lines = new Set(); + for (const d of value.data) { + lines.add(d.symbol.range.startLineNumber); + } + data[key] = { + lineCount: value.lineCount, + lines: values(lines) + }; + }); + return JSON.stringify(data); + } + + private _deserialize(raw: string): void { + try { + const data: Record = JSON.parse(raw); + for (const key in data) { + const element = data[key]; + const symbols: ICodeLensData[] = []; + for (const line of element.lines) { + symbols.push({ + provider: this._fakeProvider, + symbol: { range: new Range(line, 1, line, 11) } + }); + } + this._cache.set(key, new CacheItem(element.lineCount, symbols)); + } + } catch { + // ignore... + } + } +} + +registerSingleton(ICodeLensCache, CodeLensCache); diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 552c4e952b..b2b61a2f40 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancelablePromise, RunOnceScheduler, createCancelablePromise } from 'vs/base/common/async'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { CancelablePromise, RunOnceScheduler, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; +import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; @@ -17,6 +17,7 @@ import { ICodeLensData, getCodeLensData } from 'vs/editor/contrib/codelens/codel import { CodeLens, CodeLensHelper } from 'vs/editor/contrib/codelens/codelensWidget'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ICodeLensCache } from 'vs/editor/contrib/codelens/codeLensCache'; export class CodeLensContribution implements editorCommon.IEditorContribution { @@ -35,7 +36,8 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { constructor( private readonly _editor: editorBrowser.ICodeEditor, @ICommandService private readonly _commandService: ICommandService, - @INotificationService private readonly _notificationService: INotificationService + @INotificationService private readonly _notificationService: INotificationService, + @ICodeLensCache private readonly _codeLensCache: ICodeLensCache ) { this._isEnabled = this._editor.getConfiguration().contribInfo.codeLens; @@ -93,7 +95,23 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { return; } + const cachedLenses = this._codeLensCache.get(model); + if (cachedLenses) { + this._renderCodeLensSymbols(cachedLenses); + } + if (!CodeLensProviderRegistry.has(model)) { + // no provider -> return but check with + // cached lenses. they expire after 30 seconds + if (cachedLenses) { + this._localToDispose.push(disposableTimeout(() => { + const cachedLensesNow = this._codeLensCache.get(model); + if (cachedLenses === cachedLensesNow) { + this._codeLensCache.delete(model); + this._onModelChange(); + } + }, 30 * 1000)); + } return; } @@ -106,7 +124,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { this._detectVisibleLenses = new RunOnceScheduler(() => { this._onViewportChanged(); - }, 500); + }, 250); const scheduler = new RunOnceScheduler(() => { const counterValue = ++this._modelChangeCounter; @@ -116,8 +134,9 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { this._currentFindCodeLensSymbolsPromise = createCancelablePromise(token => getCodeLensData(model, token)); - this._currentFindCodeLensSymbolsPromise.then((result) => { + this._currentFindCodeLensSymbolsPromise.then(result => { if (counterValue === this._modelChangeCounter) { // only the last one wins + this._codeLensCache.put(model, result); this._renderCodeLensSymbols(result); this._detectVisibleLenses.schedule(); } @@ -312,7 +331,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { if (!request.symbol.command && typeof request.provider.resolveCodeLens === 'function') { return Promise.resolve(request.provider.resolveCodeLens(model, request.symbol, token)).then(symbol => { resolvedSymbols[i] = symbol; - }); + }, onUnexpectedExternalError); } else { resolvedSymbols[i] = request.symbol; return Promise.resolve(undefined); diff --git a/src/vs/editor/contrib/codelens/codelensWidget.css b/src/vs/editor/contrib/codelens/codelensWidget.css index 16f2a7acee..6234ee6958 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.css +++ b/src/vs/editor/contrib/codelens/codelensWidget.css @@ -32,14 +32,11 @@ opacity: 0; } -@keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } } -@-moz-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } } -@-o-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } } -@-webkit-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } } +@keyframes fadein { + 0% { opacity: 0; visibility: visible;} + 100% { opacity: 1; } +} .monaco-editor .codelens-decoration.fadein { - -webkit-animation: fadein 0.5s linear; - -moz-animation: fadein 0.5s linear; - -o-animation: fadein 0.5s linear; - animation: fadein 0.5s linear; + animation: fadein 0.1s linear; } diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index fe38ca4d63..9701a319f4 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -58,13 +58,14 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { private readonly _id: string; private readonly _domNode: HTMLElement; private readonly _editor: editorBrowser.ICodeEditor; + private readonly _commands = new Map(); private _widgetPosition: editorBrowser.IContentWidgetPosition; - private _commands: { [id: string]: Command } = Object.create(null); constructor( editor: editorBrowser.ICodeEditor, - symbolRange: Range + symbolRange: Range, + data: ICodeLensData[] ) { this._id = 'codeLensWidget' + (++CodeLensContentWidget._idPool); this._editor = editor; @@ -74,9 +75,8 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { this._domNode = document.createElement('span'); this._domNode.innerHTML = ' '; dom.addClass(this._domNode, 'codelens-decoration'); - dom.addClass(this._domNode, 'invisible-cl'); this.updateHeight(); - this.updateVisibility(); + this.withCommands(data.map(data => data.symbol), false); } updateHeight(): void { @@ -88,15 +88,9 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { this._domNode.innerHTML = ' '; } - updateVisibility(): void { - if (this.isVisible()) { - dom.removeClass(this._domNode, 'invisible-cl'); - dom.addClass(this._domNode, 'fadein'); - } - } + withCommands(inSymbols: Array, animate: boolean): void { + this._commands.clear(); - withCommands(inSymbols: Array): void { - this._commands = Object.create(null); const symbols = coalesce(inSymbols); if (isFalsyOrEmpty(symbols)) { this._domNode.innerHTML = 'no commands'; @@ -111,7 +105,7 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { let part: string; if (command.id) { part = `${title}`; - this._commands[i] = command; + this._commands.set(String(i), command); } else { part = `${title}`; } @@ -119,13 +113,17 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { } } + const wasEmpty = this._domNode.innerHTML === '' || this._domNode.innerHTML === ' '; this._domNode.innerHTML = html.join(' | '); this._editor.layoutContentWidget(this); + if (wasEmpty && animate) { + dom.addClass(this._domNode, 'fadein'); + } } getCommand(link: HTMLLinkElement): Command | undefined { return link.parentElement === this._domNode - ? this._commands[link.id] + ? this._commands.get(link.id) : undefined; } @@ -228,7 +226,7 @@ export class CodeLens { }); if (range) { - this._contentWidget = new CodeLensContentWidget(editor, range); + this._contentWidget = new CodeLensContentWidget(editor, range, this._data); this._viewZone = new CodeLensViewZone(range.startLineNumber - 1, updateCallback); this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone); @@ -273,7 +271,6 @@ export class CodeLens { } computeIfNecessary(model: ITextModel): ICodeLensData[] | null { - this._contentWidget.updateVisibility(); // trigger the fade in if (!this._contentWidget.isVisible()) { return null; } @@ -289,7 +286,7 @@ export class CodeLens { } updateCommands(symbols: Array): void { - this._contentWidget.withCommands(symbols); + this._contentWidget.withCommands(symbols, true); for (let i = 0; i < this._data.length; i++) { const resolved = symbols[i]; if (resolved) { diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index 89b41e4c71..54cfaa9be8 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -274,9 +274,7 @@ export class OutlineModel extends TreeElement { static _create(textModel: ITextModel, token: CancellationToken): Promise { - const chainedCancellation = new CancellationTokenSource(); - token.onCancellationRequested(() => chainedCancellation.cancel()); - + const cts = new CancellationTokenSource(token); const result = new OutlineModel(textModel); const provider = DocumentSymbolProviderRegistry.ordered(textModel); const promises = provider.map((provider, index) => { @@ -284,7 +282,7 @@ export class OutlineModel extends TreeElement { let id = TreeElement.findId(`provider_${index}`, result); let group = new OutlineGroup(id, result, provider, index); - return Promise.resolve(provider.provideDocumentSymbols(result.textModel, chainedCancellation.token)).then(result => { + return Promise.resolve(provider.provideDocumentSymbols(result.textModel, cts.token)).then(result => { for (const info of result || []) { OutlineModel._makeOutlineElement(info, group); } @@ -304,12 +302,12 @@ export class OutlineModel extends TreeElement { const listener = DocumentSymbolProviderRegistry.onDidChange(() => { const newProvider = DocumentSymbolProviderRegistry.ordered(textModel); if (!equals(newProvider, provider)) { - chainedCancellation.cancel(); + cts.cancel(); } }); return Promise.all(promises).then(() => { - if (chainedCancellation.token.isCancellationRequested && !token.isCancellationRequested) { + if (cts.token.isCancellationRequested && !token.isCancellationRequested) { return OutlineModel._create(textModel, token); } else { return result._compact(); diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 84bbf11eb6..bbee8c4ae7 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -764,16 +764,12 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas appendWholeWordsLabel: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand), appendRegexLabel: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand), validation: (value: string): InputBoxMessage | null => { - if (value.length === 0) { - return null; - } - if (!this._findInput.getRegex()) { + if (value.length === 0 || !this._findInput.getRegex()) { return null; } try { - /* tslint:disable:no-unused-expression */ + /* tslint:disable-next-line:no-unused-expression */ new RegExp(value); - /* tslint:enable:no-unused-expression */ return null; } catch (e) { return { content: e.message }; diff --git a/src/vs/editor/contrib/find/simpleFindWidget.ts b/src/vs/editor/contrib/find/simpleFindWidget.ts index 5d26b6f437..61cbcdc127 100644 --- a/src/vs/editor/contrib/find/simpleFindWidget.ts +++ b/src/vs/editor/contrib/find/simpleFindWidget.ts @@ -11,6 +11,7 @@ import { Widget } from 'vs/base/browser/ui/widget'; import { Delayer } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; +import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { SimpleButton } from 'vs/editor/contrib/find/findWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; @@ -44,6 +45,18 @@ export abstract class SimpleFindWidget extends Widget { this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewService, { label: NLS_FIND_INPUT_LABEL, placeholder: NLS_FIND_INPUT_PLACEHOLDER, + validation: (value: string): InputBoxMessage | null => { + if (value.length === 0 || !this._findInput.getRegex()) { + return null; + } + try { + /* tslint:disable-next-line:no-unused-expression */ + new RegExp(value); + return null; + } catch (e) { + return { content: e.message }; + } + } }, contextKeyService, showOptionButtons)); // Find History with update delayer diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index cbccda55a1..76ec9c7513 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -5,10 +5,10 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; -import { CodeEditorStateFlag, EditorState } from 'vs/editor/browser/core/editorState'; +import { CodeEditorStateFlag, EditorState, EditorStateCancellationTokenSource, TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState'; import { IActiveCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerLanguageCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; @@ -227,26 +227,24 @@ export async function formatDocumentWithProvider( const workerService = accessor.get(IEditorWorkerService); let model: ITextModel; - let validate: () => boolean; + let cts: CancellationTokenSource; if (isCodeEditor(editorOrModel)) { model = editorOrModel.getModel(); - const state = new EditorState(editorOrModel, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position); - validate = () => state.validate(editorOrModel); + cts = new EditorStateCancellationTokenSource(editorOrModel, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position, token); } else { model = editorOrModel; - const versionNow = editorOrModel.getVersionId(); - validate = () => versionNow === editorOrModel.getVersionId(); + cts = new TextModelCancellationTokenSource(editorOrModel, token); } const rawEdits = await provider.provideDocumentFormattingEdits( model, model.getFormattingOptions(), - token + cts.token ); const edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); - if (!validate()) { + if (cts.token.isCancellationRequested) { return true; } diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinition.ts b/src/vs/editor/contrib/goToDefinition/goToDefinition.ts index bb24b0e4f4..bc5de99e2a 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinition.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinition.ts @@ -22,10 +22,10 @@ function getDefinitions( const provider = registry.ordered(model); // get results - const promises = provider.map((provider): Promise => { + const promises = provider.map((provider): Promise => { return Promise.resolve(provide(provider, model, position)).then(undefined, err => { onUnexpectedExternalError(err); - return null; + return undefined; }); }); return Promise.all(promises) diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts index a0480c7932..24f098041b 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts @@ -28,6 +28,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IProgressService } from 'vs/platform/progress/common/progress'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition, getDeclarationsAtPosition } from './goToDefinition'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { EditorStateCancellationTokenSource, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; export class DefinitionActionConfig { @@ -61,9 +62,11 @@ export class DefinitionAction extends EditorAction { const model = editor.getModel(); const pos = editor.getPosition(); - const definitionPromise = this._getTargetLocationForPosition(model, pos, CancellationToken.None).then(async references => { + const cts = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position); - if (model.isDisposed() || editor.getModel() !== model) { + const definitionPromise = this._getTargetLocationForPosition(model, pos, cts.token).then(async references => { + + if (cts.token.isCancellationRequested || model.isDisposed() || editor.getModel() !== model) { // new model, no more model return; } @@ -105,6 +108,8 @@ export class DefinitionAction extends EditorAction { }, (err) => { // report an error notificationService.error(err); + }).finally(() => { + cts.dispose(); }); progressService.showWhile(definitionPromise, 250); diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index 27394356b9..507565b8de 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -886,6 +886,8 @@ export abstract class AbstractCaseAction extends EditorAction { return; } + let wordSeparators = editor.getConfiguration().wordSeparators; + let commands: ICommand[] = []; for (let i = 0, len = selections.length; i < len; i++) { @@ -900,12 +902,12 @@ export abstract class AbstractCaseAction extends EditorAction { let wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn); let text = model.getValueInRange(wordRange); - commands.push(new ReplaceCommandThatPreservesSelection(wordRange, this._modifyText(text), + commands.push(new ReplaceCommandThatPreservesSelection(wordRange, this._modifyText(text, wordSeparators), new Selection(cursor.lineNumber, cursor.column, cursor.lineNumber, cursor.column))); } else { let text = model.getValueInRange(selection); - commands.push(new ReplaceCommandThatPreservesSelection(selection, this._modifyText(text), selection)); + commands.push(new ReplaceCommandThatPreservesSelection(selection, this._modifyText(text, wordSeparators), selection)); } } @@ -914,7 +916,7 @@ export abstract class AbstractCaseAction extends EditorAction { editor.pushUndoStop(); } - protected abstract _modifyText(text: string): string; + protected abstract _modifyText(text: string, wordSeparators: string): string; } export class UpperCaseAction extends AbstractCaseAction { @@ -927,7 +929,7 @@ export class UpperCaseAction extends AbstractCaseAction { }); } - protected _modifyText(text: string): string { + protected _modifyText(text: string, wordSeparators: string): string { return text.toLocaleUpperCase(); } } @@ -942,11 +944,48 @@ export class LowerCaseAction extends AbstractCaseAction { }); } - protected _modifyText(text: string): string { + protected _modifyText(text: string, wordSeparators: string): string { return text.toLocaleLowerCase(); } } +export class TitleCaseAction extends AbstractCaseAction { + constructor() { + super({ + id: 'editor.action.transformToTitlecase', + label: nls.localize('editor.transformToTitlecase', "Transform to Title Case"), + alias: 'Transform to Title Case', + precondition: EditorContextKeys.writable + }); + } + + protected _modifyText(text: string, wordSeparators: string): string { + const separators = '\r\n\t ' + wordSeparators; + const excludedChars = separators.split(''); + + let title = ''; + let startUpperCase = true; + + for (let i = 0; i < text.length; i++) { + let currentChar = text[i]; + + if (excludedChars.indexOf(currentChar) >= 0) { + startUpperCase = true; + + title += currentChar; + } else if (startUpperCase) { + startUpperCase = false; + + title += currentChar.toLocaleUpperCase(); + } else { + title += currentChar.toLocaleLowerCase(); + } + } + + return title; + } +} + registerEditorAction(CopyLinesUpAction); registerEditorAction(CopyLinesDownAction); registerEditorAction(MoveLinesUpAction); @@ -965,3 +1004,4 @@ registerEditorAction(JoinLinesAction); registerEditorAction(TransposeAction); registerEditorAction(UpperCaseAction); registerEditorAction(LowerCaseAction); +registerEditorAction(TitleCaseAction); diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index 130c20779e..5e82412597 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -9,7 +9,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction } from 'vs/editor/contrib/linesOperations/linesOperations'; +import { TitleCaseAction, DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction } from 'vs/editor/contrib/linesOperations/linesOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; @@ -529,6 +529,7 @@ suite('Editor Contrib - Line Operations', () => { let model = editor.getModel()!; let uppercaseAction = new UpperCaseAction(); let lowercaseAction = new LowerCaseAction(); + let titlecaseAction = new TitleCaseAction(); editor.setSelection(new Selection(1, 1, 1, 12)); uppercaseAction.run(null!, editor); @@ -550,15 +551,63 @@ suite('Editor Contrib - Line Operations', () => { assert.equal(model.getLineContent(1), 'hello world', '007'); assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 4, 1, 4).toString(), '008'); + editor.setSelection(new Selection(1, 1, 1, 12)); + titlecaseAction.run(null!, editor); + assert.equal(model.getLineContent(1), 'Hello World', '009'); + assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 1, 1, 12).toString(), '010'); + editor.setSelection(new Selection(2, 1, 2, 6)); uppercaseAction.run(null!, editor); - assert.equal(model.getLineContent(2), 'ÖÇŞĞÜ', '009'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 6).toString(), '010'); + assert.equal(model.getLineContent(2), 'ÖÇŞĞÜ', '011'); + assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 6).toString(), '012'); editor.setSelection(new Selection(2, 1, 2, 6)); lowercaseAction.run(null!, editor); - assert.equal(model.getLineContent(2), 'öçşğü', '011'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 6).toString(), '012'); + assert.equal(model.getLineContent(2), 'öçşğü', '013'); + assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 6).toString(), '014'); + + editor.setSelection(new Selection(2, 1, 2, 6)); + titlecaseAction.run(null!, editor); + assert.equal(model.getLineContent(2), 'Öçşğü', '015'); + assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 6).toString(), '016'); + } + ); + + withTestCodeEditor( + [ + 'foO baR BaZ', + 'foO\'baR\'BaZ', + 'foO[baR]BaZ', + 'foO`baR~BaZ', + 'foO^baR%BaZ', + 'foO$baR!BaZ' + ], {}, (editor) => { + let model = editor.getModel()!; + let titlecaseAction = new TitleCaseAction(); + + editor.setSelection(new Selection(1, 1, 1, 12)); + titlecaseAction.run(null!, editor); + assert.equal(model.getLineContent(1), 'Foo Bar Baz'); + + editor.setSelection(new Selection(2, 1, 2, 12)); + titlecaseAction.run(null!, editor); + assert.equal(model.getLineContent(2), 'Foo\'Bar\'Baz'); + + editor.setSelection(new Selection(3, 1, 3, 12)); + titlecaseAction.run(null!, editor); + assert.equal(model.getLineContent(3), 'Foo[Bar]Baz'); + + editor.setSelection(new Selection(4, 1, 4, 12)); + titlecaseAction.run(null!, editor); + assert.equal(model.getLineContent(4), 'Foo`Bar~Baz'); + + editor.setSelection(new Selection(5, 1, 5, 12)); + titlecaseAction.run(null!, editor); + assert.equal(model.getLineContent(5), 'Foo^Bar%Baz'); + + editor.setSelection(new Selection(6, 1, 6, 12)); + titlecaseAction.run(null!, editor); + assert.equal(model.getLineContent(6), 'Foo$Bar!Baz'); } ); diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index bb253e2260..d2a364c37c 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -9,7 +9,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { Selection } from 'vs/editor/common/core/selection'; import { VariableResolver, Variable, Text } from 'vs/editor/contrib/snippet/snippetParser'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { getLeadingWhitespace, commonPrefixLength, isFalsyOrWhitespace, pad } from 'vs/base/common/strings'; +import { getLeadingWhitespace, commonPrefixLength, isFalsyOrWhitespace, pad, endsWith } from 'vs/base/common/strings'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; @@ -269,7 +269,10 @@ export class WorkspaceBasedVariableResolver implements VariableResolver { return basename(workspaceIdentifier.path); } - const filename = basename(workspaceIdentifier.configPath.path); - return filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); + let filename = basename(workspaceIdentifier.configPath.path); + if (endsWith(filename, WORKSPACE_EXTENSION)) { + filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); + } + return filename; } } \ No newline at end of file diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index ac4863b2fc..9ff8d2ef55 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -8,7 +8,6 @@ * into a typed and checked ILexer definition. */ -import * as objects from 'vs/base/common/objects'; import * as monarchCommon from 'vs/editor/standalone/common/monarch/monarchCommon'; import { IMonarchLanguage, IMonarchLanguageBracket } from 'vs/editor/standalone/common/monarch/monarchTypes'; @@ -52,6 +51,33 @@ function string(prop: any, defValue: string): string { return defValue; } + +function arrayToHash(array: string[]): { [name: string]: true } { + const result: any = {}; + for (const e of array) { + result[e] = true; + } + return result; +} + + +function createKeywordMatcher(arr: string[], caseInsensitive: boolean = false): (str: string) => boolean { + if (caseInsensitive) { + arr = arr.map(function (x) { return x.toLowerCase(); }); + } + const hash = arrayToHash(arr); + if (caseInsensitive) { + return function (word) { + return hash[word.toLowerCase()] !== undefined && hash.hasOwnProperty(word.toLowerCase()); + }; + } else { + return function (word) { + return hash[word] !== undefined && hash.hasOwnProperty(word); + }; + } +} + + // Lexer helpers /** @@ -142,7 +168,7 @@ function createGuard(lexer: monarchCommon.ILexerMin, ruleName: string, tkey: str // special case a regexp that matches just words if ((op === '~' || op === '!~') && /^(\w|\|)*$/.test(pat)) { - let inWords = objects.createKeywordMatcher(pat.split('|'), lexer.ignoreCase); + let inWords = createKeywordMatcher(pat.split('|'), lexer.ignoreCase); tester = function (s) { return (op === '~' ? inWords(s) : !inWords(s)); }; } else if (op === '@' || op === '!@') { @@ -153,7 +179,7 @@ function createGuard(lexer: monarchCommon.ILexerMin, ruleName: string, tkey: str if (!(isArrayOf(function (elem) { return (typeof (elem) === 'string'); }, words))) { throw monarchCommon.createError(lexer, 'the @ match target \'' + pat + '\' must be an array of strings, in rule: ' + ruleName); } - let inWords = objects.createKeywordMatcher(words, lexer.ignoreCase); + let inWords = createKeywordMatcher(words, lexer.ignoreCase); tester = function (s) { return (op === '@' ? inWords(s) : !inWords(s)); }; } else if (op === '~' || op === '!~') { diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 08734855f2..ef24605e40 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -657,6 +657,26 @@ suite('Editor Model - TextModel', () => { assert.deepEqual(m.validatePosition(new Position(NaN, 3)), new Position(1, 3)); }); + test('issue #71480: validatePosition handle floats', () => { + let m = TextModel.createFromString('line one\nline two'); + + assert.deepEqual(m.validatePosition(new Position(0.2, 1)), new Position(1, 1), 'a'); + assert.deepEqual(m.validatePosition(new Position(1.2, 1)), new Position(1, 1), 'b'); + assert.deepEqual(m.validatePosition(new Position(1.5, 2)), new Position(1, 2), 'c'); + assert.deepEqual(m.validatePosition(new Position(1.8, 3)), new Position(1, 3), 'd'); + assert.deepEqual(m.validatePosition(new Position(1, 0.3)), new Position(1, 1), 'e'); + assert.deepEqual(m.validatePosition(new Position(2, 0.8)), new Position(2, 1), 'f'); + assert.deepEqual(m.validatePosition(new Position(1, 1.2)), new Position(1, 1), 'g'); + assert.deepEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); + }); + + test('issue #71480: validateRange handle floats', () => { + let m = TextModel.createFromString('line one\nline two'); + + assert.deepEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); + assert.deepEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); + }); + test('validateRange around high-low surrogate pairs 1', () => { let m = TextModel.createFromString('a📚b'); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a7e941775b..0c66688d94 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -38,6 +38,7 @@ declare namespace monaco { } export class CancellationTokenSource { + constructor(parent?: CancellationToken); readonly token: CancellationToken; cancel(): void; dispose(): void; diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 99c1f5600c..ef9588a464 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IMenu, IMenuActionOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService, IContextKeyChangeEvent } from 'vs/platform/contextkey/common/contextkey'; export class MenuService implements IMenuService { @@ -52,7 +52,7 @@ class Menu implements IMenu { // when context keys change we need to check if the menu also // has changed - Event.debounce( + Event.debounce( this._contextKeyService.onDidChangeContext, (last, event) => last || event.affectsSome(this._contextKeys), 50 diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index 01ea0eb0f5..75bee0b950 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as crypto from 'crypto'; import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; -import { writeFileAndFlushSync } from 'vs/base/node/extfs'; +import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs'; import * as arrays from 'vs/base/common/arrays'; import { IBackupMainService, IBackupWorkspacesFormat, IEmptyWindowBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -19,7 +19,6 @@ import { URI } from 'vs/base/common/uri'; import { isEqual as areResourcesEquals, getComparisonKey, hasToIgnoreCase } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; import { Schemas } from 'vs/base/common/network'; -import { writeFile, readFile, readdir, exists, del, rename } from 'vs/base/node/pfs'; export class BackupMainService implements IBackupMainService { @@ -343,7 +342,7 @@ export class BackupMainService implements IBackupMainService { private async deleteStaleBackup(backupPath: string): Promise { try { if (await exists(backupPath)) { - await del(backupPath); + await rimraf(backupPath, RimRafMode.MOVE); } } catch (ex) { this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`); @@ -415,7 +414,7 @@ export class BackupMainService implements IBackupMainService { private saveSync(): void { try { - writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(this.serializeBackups())); + writeFileSync(this.workspacesJsonPath, JSON.stringify(this.serializeBackups())); } catch (ex) { this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); } @@ -435,7 +434,7 @@ export class BackupMainService implements IBackupMainService { folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()), emptyWorkspaceInfos: this.emptyWorkspaces, emptyWorkspaces: this.emptyWorkspaces.map(info => info.backupFolder) - } as IBackupWorkspacesFormat; + }; } private getRandomEmptyWindowId(): string { diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 63b75b1f8b..705ca74517 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -121,7 +121,7 @@ suite('BackupMainService', () => { setup(() => { // Delete any existing backups completely and then re-create it. - return pfs.del(backupHome, os.tmpdir()).then(() => { + return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE).then(() => { return pfs.mkdirp(backupHome); }).then(() => { configService = new TestConfigurationService(); @@ -132,7 +132,7 @@ suite('BackupMainService', () => { }); teardown(() => { - return pfs.del(backupHome, os.tmpdir()); + return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); }); test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () { diff --git a/src/vs/platform/configuration/node/configuration.ts b/src/vs/platform/configuration/node/configuration.ts index 9e91bb3152..3336528611 100644 --- a/src/vs/platform/configuration/node/configuration.ts +++ b/src/vs/platform/configuration/node/configuration.ts @@ -3,15 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ConfigurationModelParser, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { ConfigWatcher } from 'vs/base/node/config'; import { Event, Emitter } from 'vs/base/common/event'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { URI } from 'vs/base/common/uri'; -import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files'; -import * as resources from 'vs/base/common/resources'; export class NodeBasedUserConfiguration extends Disposable { @@ -54,53 +50,7 @@ export class NodeBasedUserConfiguration extends Disposable { return this.initialize().then(() => new Promise(c => this.userConfigModelWatcher.reload(userConfigModelParser => c(userConfigModelParser.configurationModel)))); } -} - -export class FileServiceBasedUserConfiguration extends Disposable { - - private readonly reloadConfigurationScheduler: RunOnceScheduler; - protected readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); - readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; - - constructor( - private readonly configurationResource: URI, - private readonly fileService: IFileService - ) { - super(); - - this._register(fileService.onFileChanges(e => this.handleFileEvents(e))); - this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); - this.fileService.watch(this.configurationResource); - this._register(toDisposable(() => this.fileService.unwatch(this.configurationResource))); - } - - initialize(): Promise { - return this.reload(); - } - - reload(): Promise { - return this.fileService.resolveContent(this.configurationResource) - .then(content => content.value, () => { - // File not found - return ''; - }).then(content => { - const parser = new ConfigurationModelParser(this.configurationResource.toString()); - parser.parse(content); - return parser.configurationModel; - }); - } - - private handleFileEvents(event: FileChangesEvent): void { - const events = event.changes; - - let affectedByChanges = false; - // Find changes that affect workspace file - for (let i = 0, len = events.length; i < len && !affectedByChanges; i++) { - affectedByChanges = resources.isEqual(this.configurationResource, events[i].resource); - } - - if (affectedByChanges) { - this.reloadConfigurationScheduler.schedule(); - } + getConfigurationModel(): ConfigurationModel { + return this.userConfigModelWatcher.getConfig().configurationModel; } } \ No newline at end of file diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index ca5d50cb20..dd2434fb3b 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { keys } from 'vs/base/common/map'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -95,7 +95,7 @@ class ConfigAwareContextValuesContainer extends Context { constructor( id: number, private readonly _configurationService: IConfigurationService, - emitter: Emitter + emitter: Emitter ) { super(id, null); @@ -104,7 +104,7 @@ class ConfigAwareContextValuesContainer extends Context { // new setting, reset everything const allKeys = keys(this._values); this._values.clear(); - emitter.fire(allKeys); + emitter.fire(new ArrayContextKeyChangeEvent(allKeys)); } else { const changedKeys: string[] = []; for (const configKey of event.affectedKeys) { @@ -114,7 +114,7 @@ class ConfigAwareContextValuesContainer extends Context { changedKeys.push(contextKey); } } - emitter.fire(changedKeys); + emitter.fire(new ArrayContextKeyChangeEvent(changedKeys)); } }); } @@ -165,44 +165,45 @@ class ConfigAwareContextValuesContainer extends Context { class ContextKey implements IContextKey { - private _parent: AbstractContextKeyService; + private _service: AbstractContextKeyService; private _key: string; private _defaultValue: T | undefined; - constructor(parent: AbstractContextKeyService, key: string, defaultValue: T | undefined) { - this._parent = parent; + constructor(service: AbstractContextKeyService, key: string, defaultValue: T | undefined) { + this._service = service; this._key = key; this._defaultValue = defaultValue; this.reset(); } public set(value: T): void { - this._parent.setContext(this._key, value); + this._service.setContext(this._key, value); } public reset(): void { if (typeof this._defaultValue === 'undefined') { - this._parent.removeContext(this._key); + this._service.removeContext(this._key); } else { - this._parent.setContext(this._key, this._defaultValue); + this._service.setContext(this._key, this._defaultValue); } } public get(): T | undefined { - return this._parent.getContextKeyValue(this._key); + return this._service.getContextKeyValue(this._key); } } class SimpleContextKeyChangeEvent implements IContextKeyChangeEvent { - constructor(private readonly _key: string) { } + constructor(readonly key: string) { } affectsSome(keys: IReadableSet): boolean { - return keys.has(this._key); + return keys.has(this.key); } } + class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent { - constructor(private readonly _keys: string[]) { } + constructor(readonly keys: string[]) { } affectsSome(keys: IReadableSet): boolean { - for (const key of this._keys) { + for (const key of this.keys) { if (keys.has(key)) { return true; } @@ -211,18 +212,28 @@ class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent { } } +class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent { + constructor(readonly events: IContextKeyChangeEvent[]) { } + affectsSome(keys: IReadableSet): boolean { + for (const e of this.events) { + if (e.affectsSome(keys)) { + return true; + } + } + return false; + } +} + export abstract class AbstractContextKeyService implements IContextKeyService { public _serviceBrand: any; protected _isDisposed: boolean; - protected _onDidChangeContext: Event; - protected _onDidChangeContextKey: Emitter; + protected _onDidChangeContext = new PauseableEmitter({ merge: input => new CompositeContextKeyChangeEvent(input) }); protected _myContextId: number; constructor(myContextId: number) { this._isDisposed = false; this._myContextId = myContextId; - this._onDidChangeContextKey = new Emitter(); } abstract dispose(): void; @@ -235,21 +246,23 @@ export abstract class AbstractContextKeyService implements IContextKeyService { } public get onDidChangeContext(): Event { - if (!this._onDidChangeContext) { - this._onDidChangeContext = Event.map(this._onDidChangeContextKey.event, ((changedKeyOrKeys): IContextKeyChangeEvent => { - return typeof changedKeyOrKeys === 'string' - ? new SimpleContextKeyChangeEvent(changedKeyOrKeys) - : new ArrayContextKeyChangeEvent(changedKeyOrKeys); - })); + return this._onDidChangeContext.event; + } + + bufferChangeEvents(callback: Function): void { + this._onDidChangeContext.pause(); + try { + callback(); + } finally { + this._onDidChangeContext.resume(); } - return this._onDidChangeContext; } public createScoped(domNode: IContextKeyServiceTarget): IContextKeyService { if (this._isDisposed) { throw new Error(`AbstractContextKeyService has been disposed`); } - return new ScopedContextKeyService(this, this._onDidChangeContextKey, domNode); + return new ScopedContextKeyService(this, domNode); } public contextMatchesRules(rules: ContextKeyExpr | undefined): boolean { @@ -280,7 +293,7 @@ export abstract class AbstractContextKeyService implements IContextKeyService { return; } if (myContext.setValue(key, value)) { - this._onDidChangeContextKey.fire(key); + this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key)); } } @@ -289,7 +302,7 @@ export abstract class AbstractContextKeyService implements IContextKeyService { return; } if (this.getContextValuesContainer(this._myContextId).removeValue(key)) { - this._onDidChangeContextKey.fire(key); + this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key)); } } @@ -308,19 +321,17 @@ export abstract class AbstractContextKeyService implements IContextKeyService { export class ContextKeyService extends AbstractContextKeyService implements IContextKeyService { private _lastContextId: number; - private _contexts: { - [contextId: string]: Context; - }; + private readonly _contexts = new Map(); private _toDispose: IDisposable[] = []; constructor(@IConfigurationService configurationService: IConfigurationService) { super(0); this._lastContextId = 0; - this._contexts = Object.create(null); - const myContext = new ConfigAwareContextValuesContainer(this._myContextId, configurationService, this._onDidChangeContextKey); - this._contexts[String(this._myContextId)] = myContext; + + const myContext = new ConfigAwareContextValuesContainer(this._myContextId, configurationService, this._onDidChangeContext); + this._contexts.set(this._myContextId, myContext); this._toDispose.push(myContext); // Uncomment this to see the contexts continuously logged @@ -344,7 +355,7 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon if (this._isDisposed) { return NullContext.INSTANCE; } - return this._contexts[String(contextId)]; + return this._contexts.get(contextId) || NullContext.INSTANCE; } public createChildContext(parentContextId: number = this._myContextId): number { @@ -352,15 +363,14 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon throw new Error(`ContextKeyService has been disposed`); } let id = (++this._lastContextId); - this._contexts[String(id)] = new Context(id, this.getContextValuesContainer(parentContextId)); + this._contexts.set(id, new Context(id, this.getContextValuesContainer(parentContextId))); return id; } public disposeContext(contextId: number): void { - if (this._isDisposed) { - return; + if (!this._isDisposed) { + this._contexts.delete(contextId); } - delete this._contexts[String(contextId)]; } } @@ -369,10 +379,9 @@ class ScopedContextKeyService extends AbstractContextKeyService { private _parent: AbstractContextKeyService; private _domNode: IContextKeyServiceTarget | undefined; - constructor(parent: AbstractContextKeyService, emitter: Emitter, domNode?: IContextKeyServiceTarget) { + constructor(parent: AbstractContextKeyService, domNode?: IContextKeyServiceTarget) { super(parent.createChildContext()); this._parent = parent; - this._onDidChangeContextKey = emitter; if (domNode) { this._domNode = domNode; @@ -390,7 +399,7 @@ class ScopedContextKeyService extends AbstractContextKeyService { } public get onDidChangeContext(): Event { - return this._parent.onDidChangeContext; + return Event.any(this._parent.onDidChangeContext, this._onDidChangeContext.event); } public getContextValuesContainer(contextId: number): Context { diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 5d742b0d6f..8b17487155 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -762,6 +762,9 @@ export interface IContextKeyService { dispose(): void; onDidChangeContext: Event; + bufferChangeEvents(callback: Function): void; + + createKey(key: string, defaultValue: T | undefined): IContextKey; contextMatchesRules(rules: ContextKeyExpr | undefined): boolean; getContextKeyValue(key: string): T | undefined; diff --git a/src/vs/platform/diagnostics/common/diagnosticsService.ts b/src/vs/platform/diagnostics/common/diagnosticsService.ts new file mode 100644 index 0000000000..61a98de5fa --- /dev/null +++ b/src/vs/platform/diagnostics/common/diagnosticsService.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { UriComponents } from 'vs/base/common/uri'; +import { ProcessItem } from 'vs/base/common/processes'; + +export interface IMachineInfo { + os: string; + cpus?: string; + memory: string; + vmHint: string; +} + +export interface IDiagnosticInfo { + machineInfo: IMachineInfo; + workspaceMetadata?: { [key: string]: WorkspaceStats }; + processes?: ProcessItem; +} +export interface SystemInfo extends IMachineInfo { + processArgs: string; + gpuStatus: any; + screenReader: string; + load?: string; +} + +export interface IDiagnosticInfoOptions { + includeProcesses?: boolean; + folders?: UriComponents[]; + includeExtensions?: boolean; +} + +export interface WorkspaceStatItem { + name: string; + count: number; +} + +export interface WorkspaceStats { + fileTypes: WorkspaceStatItem[]; + configFiles: WorkspaceStatItem[]; + fileCount: number; + maxFilesReached: boolean; +} \ 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 ade627654e..647bab721c 100644 --- a/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts @@ -3,19 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMainProcessInfo } from 'vs/platform/launch/electron-main/launchService'; -import { ProcessItem, listProcesses } from 'vs/base/node/ps'; +import { IMainProcessInfo, ILaunchService } from 'vs/platform/launch/electron-main/launchService'; +import { listProcesses } from 'vs/base/node/ps'; import product from 'vs/platform/product/node/product'; import pkg from 'vs/platform/product/node/package'; -import * as os from 'os'; +import * as osLib from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; import { repeat, pad } from 'vs/base/common/strings'; import { isWindows } from 'vs/base/common/platform'; import { app } from 'electron'; -import { basename, join } from 'vs/base/common/path'; +import { basename } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { readdir, stat } from 'fs'; +import { WorkspaceStats, SystemInfo } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { collectWorkspaceStats, getMachineInfo } from 'vs/platform/diagnostics/node/diagnosticsService'; +import { ProcessItem } from 'vs/base/common/processes'; export const ID = 'diagnosticsService'; export const IDiagnosticsService = createDecorator(ID); @@ -23,10 +25,9 @@ export const IDiagnosticsService = createDecorator(ID); export interface IDiagnosticsService { _serviceBrand: any; - formatEnvironment(info: IMainProcessInfo): string; - getPerformanceInfo(info: IMainProcessInfo): Promise; - getSystemInfo(info: IMainProcessInfo): SystemInfo; - getDiagnostics(info: IMainProcessInfo): Promise; + getPerformanceInfo(launchService: ILaunchService): Promise; + getSystemInfo(launchService: ILaunchService): Promise; + getDiagnostics(launchService: ILaunchService): Promise; } export interface VersionInfo { @@ -34,16 +35,6 @@ export interface VersionInfo { os: string; } -export interface SystemInfo { - CPUs?: string; - 'Memory (System)': string; - 'Load (avg)'?: string; - VM: string; - 'Screen Reader': string; - 'Process Argv': string; - 'GPU Status': Electron.GPUFeatureStatus; -} - export interface ProcessInfo { cpu: number; memory: number; @@ -66,14 +57,14 @@ export class DiagnosticsService implements IDiagnosticsService { const output: string[] = []; output.push(`Version: ${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`); - output.push(`OS Version: ${os.type()} ${os.arch()} ${os.release()}`); - const cpus = os.cpus(); + output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`); + const cpus = osLib.cpus(); if (cpus && cpus.length > 0) { output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`); } - output.push(`Memory (System): ${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`); + output.push(`Memory (System): ${(osLib.totalmem() / GB).toFixed(2)}GB (${(osLib.freemem() / GB).toFixed(2)}GB free)`); if (!isWindows) { - output.push(`Load (avg): ${os.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS + output.push(`Load (avg): ${osLib.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS } output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`); output.push(`Screen Reader: ${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`); @@ -83,82 +74,42 @@ export class DiagnosticsService implements IDiagnosticsService { return output.join('\n'); } - getPerformanceInfo(info: IMainProcessInfo): Promise { - return listProcesses(info.mainPID).then(rootProcess => { - const workspaceInfoMessages: string[] = []; - - // Workspace Stats - const workspaceStatPromises: Promise[] = []; - if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) { - info.windows.forEach(window => { - if (window.folderURIs.length === 0) { - return; - } - - workspaceInfoMessages.push(`| Window (${window.title})`); - - window.folderURIs.forEach(uriComponents => { - const folderUri = URI.revive(uriComponents); - if (folderUri.scheme === 'file') { - const folder = folderUri.fsPath; - workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { - - let countMessage = `${stats.fileCount} files`; - if (stats.maxFilesReached) { - countMessage = `more than ${countMessage}`; - } - workspaceInfoMessages.push(`| Folder (${basename(folder)}): ${countMessage}`); - workspaceInfoMessages.push(this.formatWorkspaceStats(stats)); - })); - } else { - workspaceInfoMessages.push(`| Folder (${folderUri.toString()}): RPerformance stats not available.`); - } - }); - }); - } - - return Promise.all(workspaceStatPromises).then(() => { - return { - processInfo: this.formatProcessList(info, rootProcess), - workspaceInfo: workspaceInfoMessages.join('\n') - }; - }).catch(error => { - return { - processInfo: this.formatProcessList(info, rootProcess), - workspaceInfo: `Unable to calculate workspace stats: ${error}` - }; - }); + async getPerformanceInfo(launchService: ILaunchService): Promise { + const info = await launchService.getMainProcessInfo(); + return Promise.all([listProcesses(info.mainPID), this.formatWorkspaceMetadata(info)]).then(result => { + const [rootProcess, workspaceInfo] = result; + return { + processInfo: this.formatProcessList(info, rootProcess), + workspaceInfo + }; }); } - getSystemInfo(info: IMainProcessInfo): SystemInfo { - const MB = 1024 * 1024; - const GB = 1024 * MB; - + async getSystemInfo(launchService: ILaunchService): Promise { + const info = await launchService.getMainProcessInfo(); + const { memory, vmHint, os, cpus } = getMachineInfo(); const systemInfo: SystemInfo = { - 'Memory (System)': `${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`, - VM: `${Math.round((virtualMachineHint.value() * 100))}%`, - 'Screen Reader': `${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`, - 'Process Argv': `${info.mainArguments.join(' ')}`, - 'GPU Status': app.getGPUFeatureStatus() + os, + memory, + cpus, + vmHint, + processArgs: `${info.mainArguments.join(' ')}`, + gpuStatus: app.getGPUFeatureStatus(), + screenReader: `${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}` }; - const cpus = os.cpus(); - if (cpus && cpus.length > 0) { - systemInfo.CPUs = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`; - } if (!isWindows) { - systemInfo['Load (avg)'] = `${os.loadavg().map(l => Math.round(l)).join(', ')}`; + systemInfo.load = `${osLib.loadavg().map(l => Math.round(l)).join(', ')}`; } - - return systemInfo; + return Promise.resolve(systemInfo); } - getDiagnostics(info: IMainProcessInfo): Promise { + async getDiagnostics(launchService: ILaunchService): Promise { const output: string[] = []; - return listProcesses(info.mainPID).then(rootProcess => { + const info = await launchService.getMainProcessInfo(); + return listProcesses(info.mainPID).then(async rootProcess => { // Environment Info output.push(''); @@ -169,45 +120,16 @@ export class DiagnosticsService implements IDiagnosticsService { output.push(this.formatProcessList(info, rootProcess)); // Workspace Stats - const workspaceStatPromises: Promise[] = []; if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) { output.push(''); output.push('Workspace Stats: '); - info.windows.forEach(window => { - if (window.folderURIs.length === 0) { - return; - } - - output.push(`| Window (${window.title})`); - - window.folderURIs.forEach(uriComponents => { - const folderUri = URI.revive(uriComponents); - if (folderUri.scheme === 'file') { - const folder = folderUri.fsPath; - workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => { - let countMessage = `${stats.fileCount} files`; - if (stats.maxFilesReached) { - countMessage = `more than ${countMessage}`; - } - output.push(`| Folder (${basename(folder)}): ${countMessage}`); - output.push(this.formatWorkspaceStats(stats)); - - }).catch(error => { - output.push(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`); - })); - } else { - output.push(`| Folder (${folderUri.toString()}): Workspace stats not available.`); - } - }); - }); + output.push(await this.formatWorkspaceMetadata(info)); } - return Promise.all(workspaceStatPromises).then(() => { - output.push(''); - output.push(''); + output.push(''); + output.push(''); - return output.join('\n'); - }); + return output.join('\n'); }); } @@ -268,6 +190,43 @@ export class DiagnosticsService implements IDiagnosticsService { return Object.keys(gpuFeatures).map(feature => `${feature}: ${repeat(' ', longestFeatureName - feature.length)} ${gpuFeatures[feature]}`).join('\n '); } + private formatWorkspaceMetadata(info: IMainProcessInfo): Promise { + const output: string[] = []; + const workspaceStatPromises: Promise[] = []; + + info.windows.forEach(window => { + if (window.folderURIs.length === 0) { + return; + } + + output.push(`| Window (${window.title})`); + + window.folderURIs.forEach(uriComponents => { + const folderUri = URI.revive(uriComponents); + if (folderUri.scheme === 'file') { + const folder = folderUri.fsPath; + workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(stats => { + let countMessage = `${stats.fileCount} files`; + if (stats.maxFilesReached) { + countMessage = `more than ${countMessage}`; + } + output.push(`| Folder (${basename(folder)}): ${countMessage}`); + output.push(this.formatWorkspaceStats(stats)); + + }).catch(error => { + output.push(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`); + })); + } else { + output.push(`| Folder (${folderUri.toString()}): Workspace stats not available.`); + } + }); + }); + + return Promise.all(workspaceStatPromises) + .then(_ => output.join('\n')) + .catch(e => `Unable to collect workspace stats: ${e}`); + } + private formatProcessList(info: IMainProcessInfo, rootProcess: ProcessItem): string { const mapPidToWindowTitle = new Map(); info.windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title)); @@ -299,7 +258,7 @@ export class DiagnosticsService implements IDiagnosticsService { name = `${name} (${mapPidToWindowTitle.get(item.pid)})`; } } - const memory = process.platform === 'win32' ? item.mem : (os.totalmem() * (item.mem / 100)); + const memory = process.platform === 'win32' ? item.mem : (osLib.totalmem() * (item.mem / 100)); output.push(`${pad(Number(item.load.toFixed(0)), 5, ' ')}\t${pad(Number((memory / MB).toFixed(0)), 6, ' ')}\t${pad(Number((item.pid).toFixed(0)), 6, ' ')}\t${name}`); // Recurse into children if any @@ -309,25 +268,6 @@ export class DiagnosticsService implements IDiagnosticsService { } } -interface WorkspaceStatItem { - name: string; - count: number; -} - -interface WorkspaceStats { - fileTypes: WorkspaceStatItem[]; - configFiles: WorkspaceStatItem[]; - fileCount: number; - maxFilesReached: boolean; - // launchConfigFiles: WorkspaceStatItem[]; -} - -function asSortedItems(map: Map): WorkspaceStatItem[] { - const a: WorkspaceStatItem[] = []; - map.forEach((value, index) => a.push({ name: index, count: value })); - return a.sort((a, b) => b.count - a.count); -} - // function collectLaunchConfigs(folder: string): Promise { // const launchConfigs = new Map(); @@ -368,137 +308,3 @@ function asSortedItems(map: Map): WorkspaceStatItem[] { // }); // }); // } - -function collectWorkspaceStats(folder: string, filter: string[]): Promise { - const configFilePatterns = [ - { 'tag': 'grunt.js', 'pattern': /^gruntfile\.js$/i }, - { 'tag': 'gulp.js', 'pattern': /^gulpfile\.js$/i }, - { 'tag': 'tsconfig.json', 'pattern': /^tsconfig\.json$/i }, - { 'tag': 'package.json', 'pattern': /^package\.json$/i }, - { 'tag': 'jsconfig.json', 'pattern': /^jsconfig\.json$/i }, - { 'tag': 'tslint.json', 'pattern': /^tslint\.json$/i }, - { 'tag': 'eslint.json', 'pattern': /^eslint\.json$/i }, - { 'tag': 'tasks.json', 'pattern': /^tasks\.json$/i }, - { 'tag': 'launch.json', 'pattern': /^launch\.json$/i }, - { 'tag': 'settings.json', 'pattern': /^settings\.json$/i }, - { 'tag': 'webpack.config.js', 'pattern': /^webpack\.config\.js$/i }, - { 'tag': 'project.json', 'pattern': /^project\.json$/i }, - { 'tag': 'makefile', 'pattern': /^makefile$/i }, - { 'tag': 'sln', 'pattern': /^.+\.sln$/i }, - { 'tag': 'csproj', 'pattern': /^.+\.csproj$/i }, - { 'tag': 'cmake', 'pattern': /^.+\.cmake$/i } - ]; - - const fileTypes = new Map(); - const configFiles = new Map(); - - const MAX_FILES = 20000; - - function walk(dir: string, filter: string[], token: { count: any; maxReached: any; }, done: (allFiles: string[]) => void): void { - let results: string[] = []; - readdir(dir, async (err, files) => { - // Ignore folders that can't be read - if (err) { - return done(results); - } - - let pending = files.length; - if (pending === 0) { - return done(results); - } - - for (const file of files) { - if (token.maxReached) { - return done(results); - } - - stat(join(dir, file), (err, stats) => { - // Ignore files that can't be read - if (err) { - if (--pending === 0) { - return done(results); - } - } else { - if (stats.isDirectory()) { - if (filter.indexOf(file) === -1) { - walk(join(dir, file), filter, token, (res: string[]) => { - results = results.concat(res); - - if (--pending === 0) { - return done(results); - } - }); - } else { - if (--pending === 0) { - done(results); - } - } - } else { - if (token.count >= MAX_FILES) { - token.maxReached = true; - } - - token.count++; - results.push(file); - - if (--pending === 0) { - done(results); - } - } - } - }); - } - }); - } - - const addFileType = (fileType: string) => { - if (fileTypes.has(fileType)) { - fileTypes.set(fileType, fileTypes.get(fileType)! + 1); - } - else { - fileTypes.set(fileType, 1); - } - }; - - const addConfigFiles = (fileName: string) => { - for (const each of configFilePatterns) { - if (each.pattern.test(fileName)) { - if (configFiles.has(each.tag)) { - configFiles.set(each.tag, configFiles.get(each.tag)! + 1); - } else { - configFiles.set(each.tag, 1); - } - } - } - }; - - const acceptFile = (name: string) => { - if (name.lastIndexOf('.') >= 0) { - const suffix: string | undefined = name.split('.').pop(); - if (suffix) { - addFileType(suffix); - } - } - addConfigFiles(name); - }; - - const token: { count: number, maxReached: boolean } = { count: 0, maxReached: false }; - - return new Promise((resolve, reject) => { - walk(folder, filter, token, async (files) => { - files.forEach(acceptFile); - - // TODO@rachel commented out due to severe performance issues - // see https://github.com/Microsoft/vscode/issues/70563 - // const launchConfigs = await collectLaunchConfigs(folder); - - resolve({ - configFiles: asSortedItems(configFiles), - fileTypes: asSortedItems(fileTypes), - fileCount: token.count, - maxFilesReached: token.maxReached, - // launchConfigFiles: launchConfigs - }); - }); - }); -} \ No newline at end of file diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts new file mode 100644 index 0000000000..ae056538ab --- /dev/null +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as os from 'os'; +import { virtualMachineHint } from 'vs/base/node/id'; +import { IMachineInfo, WorkspaceStats, WorkspaceStatItem } from 'vs/platform/diagnostics/common/diagnosticsService'; +import { readdir, stat } from 'fs'; +import { join } from 'vs/base/common/path'; + +export function getMachineInfo(): IMachineInfo { + const MB = 1024 * 1024; + const GB = 1024 * MB; + + const machineInfo: IMachineInfo = { + os: `${os.type()} ${os.arch()} ${os.release()}`, + memory: `${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`, + vmHint: `${Math.round((virtualMachineHint.value() * 100))}%`, + }; + + const cpus = os.cpus(); + if (cpus && cpus.length > 0) { + machineInfo.cpus = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`; + } + + return machineInfo; +} + +export function collectWorkspaceStats(folder: string, filter: string[]): Promise { + const configFilePatterns = [ + { 'tag': 'grunt.js', 'pattern': /^gruntfile\.js$/i }, + { 'tag': 'gulp.js', 'pattern': /^gulpfile\.js$/i }, + { 'tag': 'tsconfig.json', 'pattern': /^tsconfig\.json$/i }, + { 'tag': 'package.json', 'pattern': /^package\.json$/i }, + { 'tag': 'jsconfig.json', 'pattern': /^jsconfig\.json$/i }, + { 'tag': 'tslint.json', 'pattern': /^tslint\.json$/i }, + { 'tag': 'eslint.json', 'pattern': /^eslint\.json$/i }, + { 'tag': 'tasks.json', 'pattern': /^tasks\.json$/i }, + { 'tag': 'launch.json', 'pattern': /^launch\.json$/i }, + { 'tag': 'settings.json', 'pattern': /^settings\.json$/i }, + { 'tag': 'webpack.config.js', 'pattern': /^webpack\.config\.js$/i }, + { 'tag': 'project.json', 'pattern': /^project\.json$/i }, + { 'tag': 'makefile', 'pattern': /^makefile$/i }, + { 'tag': 'sln', 'pattern': /^.+\.sln$/i }, + { 'tag': 'csproj', 'pattern': /^.+\.csproj$/i }, + { 'tag': 'cmake', 'pattern': /^.+\.cmake$/i } + ]; + + const fileTypes = new Map(); + const configFiles = new Map(); + + const MAX_FILES = 20000; + + function walk(dir: string, filter: string[], token: { count: number, maxReached: boolean }, done: (allFiles: string[]) => void): void { + let results: string[] = []; + readdir(dir, async (err, files) => { + // Ignore folders that can't be read + if (err) { + return done(results); + } + + let pending = files.length; + if (pending === 0) { + return done(results); + } + + for (const file of files) { + if (token.maxReached) { + return done(results); + } + + stat(join(dir, file), (err, stats) => { + // Ignore files that can't be read + if (err) { + if (--pending === 0) { + return done(results); + } + } else { + if (stats.isDirectory()) { + if (filter.indexOf(file) === -1) { + walk(join(dir, file), filter, token, (res: string[]) => { + results = results.concat(res); + + if (--pending === 0) { + return done(results); + } + }); + } else { + if (--pending === 0) { + done(results); + } + } + } else { + if (token.count >= MAX_FILES) { + token.maxReached = true; + } + + token.count++; + results.push(file); + + if (--pending === 0) { + done(results); + } + } + } + }); + } + }); + } + + const addFileType = (fileType: string) => { + if (fileTypes.has(fileType)) { + fileTypes.set(fileType, fileTypes.get(fileType)! + 1); + } + else { + fileTypes.set(fileType, 1); + } + }; + + const addConfigFiles = (fileName: string) => { + for (const each of configFilePatterns) { + if (each.pattern.test(fileName)) { + if (configFiles.has(each.tag)) { + configFiles.set(each.tag, configFiles.get(each.tag)! + 1); + } else { + configFiles.set(each.tag, 1); + } + } + } + }; + + const acceptFile = (name: string) => { + if (name.lastIndexOf('.') >= 0) { + const suffix: string | undefined = name.split('.').pop(); + if (suffix) { + addFileType(suffix); + } + } + addConfigFiles(name); + }; + + const token: { count: number, maxReached: boolean } = { count: 0, maxReached: false }; + + return new Promise((resolve, reject) => { + walk(folder, filter, token, async (files) => { + files.forEach(acceptFile); + + // TODO@rachel commented out due to severe performance issues + // see https://github.com/Microsoft/vscode/issues/70563 + // const launchConfigs = await collectLaunchConfigs(folder); + + resolve({ + configFiles: asSortedItems(configFiles), + fileTypes: asSortedItems(fileTypes), + fileCount: token.count, + maxFilesReached: token.maxReached, + // launchConfigFiles: launchConfigs + }); + }); + }); +} + +function asSortedItems(map: Map): WorkspaceStatItem[] { + const a: WorkspaceStatItem[] = []; + map.forEach((value, index) => a.push({ name: index, count: value })); + return a.sort((a, b) => b.count - a.count); +} \ No newline at end of file diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index d210a1dcee..590c0cbdbe 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -184,22 +184,22 @@ export interface IFileDialogService { /** * Shows a file-folder selection dialog and opens the selected entry. */ - pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise; + pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise; /** * Shows a file selection dialog and opens the selected entry. */ - pickFileAndOpen(options: IPickAndOpenOptions): Promise; + pickFileAndOpen(options: IPickAndOpenOptions): Promise; /** * Shows a folder selection dialog and opens the selected entry. */ - pickFolderAndOpen(options: IPickAndOpenOptions): Promise; + pickFolderAndOpen(options: IPickAndOpenOptions): Promise; /** * Shows a workspace selection dialog and opens the selected entry. */ - pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise; + pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise; /** * Shows a save file dialog and returns the chosen file URI. diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 6d40a237f6..2da8237266 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -7,7 +7,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { URI } from 'vs/base/common/uri'; export interface ParsedArgs { - [arg: string]: any; _: string[]; 'folder-uri'?: string | string[]; 'file-uri'?: string | string[]; @@ -19,6 +18,7 @@ export interface ParsedArgs { waitMarkerFilePath?: string; diff?: boolean; add?: boolean; + gitCredential?: string; goto?: boolean; 'new-window'?: boolean; 'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch. diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index c8b906ee91..a067596688 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -7,7 +7,7 @@ import * as minimist from 'minimist'; import * as os from 'os'; import { localize } from 'vs/nls'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; -import { join } from 'path'; +import { join } from 'vs/base/common/path'; import { writeFileSync } from 'fs'; /** @@ -102,8 +102,9 @@ export const options: Option[] = [ { id: 'user', type: 'string', alias: 'U' }, { id: 'command', type: 'string', alias: 'c' }, { id: 'aad', type: 'boolean' }, - { id: 'integrated', type: 'boolean', alias: 'E' } + { id: 'integrated', type: 'boolean', alias: 'E' }, // {{SQL CARBON EDIT}} - End + { id: '_', type: 'string' } ]; export function parseArgs(args: string[], isOptionSupported = (_: Option) => true): ParsedArgs { diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index 14935f41be..518cdb2959 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -18,8 +18,7 @@ import pkg from 'vs/platform/product/node/package'; import product from 'vs/platform/product/node/product'; import { isEngineValid } from 'vs/platform/extensions/node/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { readFile } from 'vs/base/node/pfs'; -import { writeFileAndFlushSync } from 'vs/base/node/extfs'; +import { writeFileSync, readFile } from 'vs/base/node/pfs'; import { generateUuid, isUUID } from 'vs/base/common/uuid'; import { values } from 'vs/base/common/map'; // {{SQL CARBON EDIT}} @@ -1004,7 +1003,7 @@ export function resolveMarketplaceHeaders(environmentService: IEnvironmentServic if (!uuid) { uuid = generateUuid(); try { - writeFileAndFlushSync(marketplaceMachineIdFile, uuid); + writeFileSync(marketplaceMachineIdFile, uuid); } catch (error) { //noop } diff --git a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts index cee86987a2..7804936c43 100644 --- a/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts +++ b/src/vs/platform/extensionManagement/node/extensionsManifestCache.ts @@ -36,6 +36,6 @@ export class ExtensionsManifestCache extends Disposable { } invalidate(): void { - pfs.del(this.extensionsManifestCache).then(() => { }, () => { }); + pfs.rimraf(this.extensionsManifestCache, pfs.RimRafMode.MOVE).then(() => { }, () => { }); } } diff --git a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts index fbbb2e2bb4..7c895546c9 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts @@ -5,12 +5,11 @@ import * as assert from 'assert'; import * as os from 'os'; -import * as extfs from 'vs/base/node/extfs'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs } from 'vs/platform/environment/node/argv'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { join } from 'vs/base/common/path'; -import { mkdirp } from 'vs/base/node/pfs'; +import { mkdirp, RimRafMode, rimraf } from 'vs/base/node/pfs'; // {{SQL CARBON EDIT}} import { resolveMarketplaceHeaders, ExtensionGalleryService } from 'vs/platform/extensionManagement/node/extensionGalleryService'; import { isUUID } from 'vs/base/common/uuid'; @@ -22,15 +21,15 @@ suite('Extension Gallery Service', () => { setup(done => { // Delete any existing backups completely and then re-create it. - extfs.del(marketplaceHome, os.tmpdir(), () => { + rimraf(marketplaceHome, RimRafMode.MOVE).then(() => { mkdirp(marketplaceHome).then(() => { done(); }, error => done(error)); - }); + }, error => done(error)); }); teardown(done => { - extfs.del(marketplaceHome, os.tmpdir(), done); + rimraf(marketplaceHome, RimRafMode.MOVE).then(done, done); }); test('marketplace machine id', () => { @@ -147,4 +146,4 @@ suite('Extension Gallery Service', () => { extension.publisher.publisherName = matchingText; assert(ExtensionGalleryService.isMatchingExtension(extension, searchText), 'publisher publisherName field should be used for matching'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/platform/extensions/common/extensionHost.ts b/src/vs/platform/extensions/common/extensionHost.ts deleted file mode 100644 index cd33bbb1b1..0000000000 --- a/src/vs/platform/extensions/common/extensionHost.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Broadcast communication constants - -export const EXTENSION_LOG_BROADCAST_CHANNEL = 'vscode:extensionLog'; -export const EXTENSION_ATTACH_BROADCAST_CHANNEL = 'vscode:extensionAttach'; -export const EXTENSION_TERMINATE_BROADCAST_CHANNEL = 'vscode:extensionTerminate'; -export const EXTENSION_RELOAD_BROADCAST_CHANNEL = 'vscode:extensionReload'; -export const EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL = 'vscode:extensionCloseExtensionHost'; \ No newline at end of file diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 44733191d6..5ee365a046 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -7,7 +7,7 @@ import { sep } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import * as glob from 'vs/base/common/glob'; import { isLinux } from 'vs/base/common/platform'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { startsWithIgnoreCase } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -26,7 +26,8 @@ export interface IResourceEncoding { } export interface IFileService { - _serviceBrand: any; + + _serviceBrand: ServiceIdentifier; //#region File System Provider @@ -59,7 +60,7 @@ export interface IFileService { /** * Checks if the provider for the provided resource has the provided file system capability. */ - hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise; + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean; //#endregion @@ -163,14 +164,11 @@ export interface IFileService { del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; /** - * Allows to start a watcher that reports file change events on the provided resource. + * Allows to start a watcher that reports file/folder change events on the provided resource. + * + * Note: watching a folder does not report events recursively for child folders yet. */ - watch(resource: URI): void; - - /** - * Allows to stop a watcher on the provided resource or absolute fs path. - */ - unwatch(resource: URI): void; + watch(resource: URI): IDisposable; /** * Frees up any resources occupied by this service. @@ -231,6 +229,8 @@ export interface IFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities; onDidChangeCapabilities: Event; + onDidErrorOccur?: Event; // TODO@ben remove once file watchers are solid + onDidChangeFile: Event; watch(resource: URI, opts: IWatchOptions): IDisposable; @@ -801,7 +801,7 @@ export class FileOperationError extends Error { super(message); } - static isFileOperationError(obj: any): obj is FileOperationError { + static isFileOperationError(obj: unknown): obj is FileOperationError { return obj instanceof Error && !isUndefinedOrNull((obj as FileOperationError).fileOperationResult); } } @@ -851,8 +851,8 @@ export interface IFilesConfiguration { autoSave: string; autoSaveDelay: number; eol: string; + enableTrash: boolean; hotExit: string; - useExperimentalFileWatcher: boolean; }; } @@ -1119,23 +1119,20 @@ export function etag(mtime: number | undefined, size: number | undefined): strin // TODO@ben remove traces of legacy file service export const ILegacyFileService = createDecorator('legacyFileService'); -export interface ILegacyFileService { +export interface ILegacyFileService extends IDisposable { _serviceBrand: any; encoding: IResourceEncodings; - onFileChanges: Event; onAfterOperation: Event; + registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable; + resolveContent(resource: URI, options?: IResolveContentOptions): Promise; resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise; - updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise; + updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise; - createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; - - watch(resource: URI): void; - - unwatch(resource: URI): void; + createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; } \ No newline at end of file diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts index f267e159d1..9db9cab627 100644 --- a/src/vs/platform/instantiation/common/instantiation.ts +++ b/src/vs/platform/instantiation/common/instantiation.ts @@ -95,7 +95,7 @@ export interface IInstantiationService { /** * */ - invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R; + invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R; /** * Creates a child of this service which inherts all current services diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 5e009dd97a..7d71317230 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { illegalState } from 'vs/base/common/errors'; -import { create } from 'vs/base/common/types'; import { Graph } from 'vs/platform/instantiation/common/graph'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ServiceIdentifier, IInstantiationService, ServicesAccessor, _util, optional } from 'vs/platform/instantiation/common/instantiation'; @@ -40,7 +39,7 @@ export class InstantiationService implements IInstantiationService { return new InstantiationService(services, this._strict, this); } - invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R { + invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R { let _trace = Trace.traceInvocation(fn); let _done = false; try { @@ -108,7 +107,7 @@ export class InstantiationService implements IInstantiationService { } // now create the instance - return create.apply(null, [ctor].concat(args, serviceArgs)); + return new ctor(...[...args, ...serviceArgs]); } private _setServiceInstance(id: ServiceIdentifier, instance: T): void { diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index b7175ef599..f22d2af05d 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -9,7 +9,7 @@ import { parseArgs } from 'vs/platform/environment/node/argv'; import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue'; import { BrowserWindow, ipcMain, screen, Event, dialog } from 'electron'; import { ILaunchService } from 'vs/platform/launch/electron-main/launchService'; -import { PerformanceInfo, SystemInfo, IDiagnosticsService } from 'vs/platform/diagnostics/electron-main/diagnosticsService'; +import { PerformanceInfo, IDiagnosticsService } from 'vs/platform/diagnostics/electron-main/diagnosticsService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; @@ -39,7 +39,7 @@ export class IssueService implements IIssueService { private registerListeners(): void { ipcMain.on('vscode:issueSystemInfoRequest', (event: Event) => { - this.getSystemInformation().then(msg => { + this.diagnosticsService.getSystemInfo(this.launchService).then(msg => { event.sender.send('vscode:issueSystemInfoResponse', msg); }); }); @@ -215,9 +215,7 @@ export class IssueService implements IIssueService { } public getSystemStatus(): Promise { - return this.launchService.getMainProcessInfo().then(info => { - return this.diagnosticsService.getDiagnostics(info); - }); + return this.diagnosticsService.getDiagnostics(this.launchService); } private getWindowPosition(parentWindow: BrowserWindow, defaultWidth: number, defaultHeight: number): IWindowState { @@ -288,26 +286,16 @@ export class IssueService implements IIssueService { return state; } - private getSystemInformation(): Promise { - return new Promise((resolve, reject) => { - this.launchService.getMainProcessInfo().then(info => { - resolve(this.diagnosticsService.getSystemInfo(info)); - }); - }); - } - private getPerformanceInfo(): Promise { return new Promise((resolve, reject) => { - this.launchService.getMainProcessInfo().then(info => { - this.diagnosticsService.getPerformanceInfo(info) - .then(diagnosticInfo => { - resolve(diagnosticInfo); - }) - .catch(err => { - this.logService.warn('issueService#getPerformanceInfo ', err.message); - reject(err); - }); - }); + this.diagnosticsService.getPerformanceInfo(this.launchService) + .then(diagnosticInfo => { + resolve(diagnosticInfo); + }) + .catch(err => { + this.logService.warn('issueService#getPerformanceInfo ', err.message); + reject(err); + }); }); } diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 11fc59759f..b9f0405a7d 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -106,6 +106,7 @@ suite('AbstractKeybindingService', () => { _serviceBrand: undefined, dispose: undefined!, onDidChangeContext: undefined!, + bufferChangeEvents() { }, createKey: undefined!, contextMatchesRules: undefined!, getContextKeyValue: undefined!, diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index c413d594b5..ddda227875 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -53,6 +53,7 @@ export class MockContextKeyService implements IContextKeyService { public get onDidChangeContext(): Event { return Event.None; } + public bufferChangeEvents() { } public getContextKeyValue(key: string) { const value = this._keys.get(key); if (value) { diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index 25067865dc..d65ac31202 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -11,6 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import { localize } from 'vs/nls'; import { isEqualOrParent, basename } from 'vs/base/common/resources'; +import { endsWith } from 'vs/base/common/strings'; export interface ILabelService { _serviceBrand: any; @@ -54,9 +55,11 @@ export function getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, w return localize('untitledWorkspace', "Untitled (Workspace)"); } - const filename = basename(workspace.configPath); - const workspaceName = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); - return localize('workspaceName', "{0} (Workspace)", workspaceName); + let filename = basename(workspace.configPath); + if (endsWith(filename, WORKSPACE_EXTENSION)) { + filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); + } + return localize('workspaceName', "{0} (Workspace)", filename); } diff --git a/src/vs/platform/launch/electron-main/launchService.ts b/src/vs/platform/launch/electron-main/launchService.ts index 7d3f2a0aa1..c8b07e78d3 100644 --- a/src/vs/platform/launch/electron-main/launchService.ts +++ b/src/vs/platform/launch/electron-main/launchService.ts @@ -311,6 +311,6 @@ export class LaunchService implements ILaunchService { pid: win.webContents.getOSProcessId(), title: win.getTitle(), folderURIs - } as IWindowInfo; + }; } } diff --git a/src/vs/platform/lifecycle/common/lifecycleService.ts b/src/vs/platform/lifecycle/common/lifecycleService.ts index e056eab5cf..cb9cb88447 100644 --- a/src/vs/platform/lifecycle/common/lifecycleService.ts +++ b/src/vs/platform/lifecycle/common/lifecycleService.ts @@ -58,7 +58,7 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi } } - when(phase: LifecyclePhase): Promise { + when(phase: LifecyclePhase): Promise { if (phase <= this._phase) { return Promise.resolve(); } @@ -69,6 +69,6 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi this.phaseWhen.set(phase, barrier); } - return barrier.wait(); + return barrier.wait().then(undefined); } } diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 5fca502778..a03920d569 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { app, shell, Menu, MenuItem, BrowserWindow } from 'electron'; -import { OpenContext, IRunActionInWindowRequest, getTitleBarStyle, IRunKeybindingInWindowRequest } from 'vs/platform/windows/common/windows'; +import { OpenContext, IRunActionInWindowRequest, getTitleBarStyle, IRunKeybindingInWindowRequest, IURIToOpen } from 'vs/platform/windows/common/windows'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; @@ -473,7 +473,9 @@ export class Menubar { private createOpenRecentMenuItem(uri: URI, label: string, commandId: string): Electron.MenuItem { const revivedUri = URI.revive(uri); - const typeHint = commandId === 'openRecentFile' || commandId === 'openRecentWorkspace' ? 'file' : 'folder'; + const uriToOpen: IURIToOpen = + (commandId === 'openRecentFile') ? { fileUri: revivedUri } : + (commandId === 'openRecentWorkspace') ? { workspaceUri: revivedUri } : { folderUri: revivedUri }; return new MenuItem(this.likeAction(commandId, { label, @@ -482,9 +484,8 @@ export class Menubar { const success = this.windowsMainService.open({ context: OpenContext.MENU, cli: this.environmentService.args, - urisToOpen: [{ uri: revivedUri, typeHint }], - forceNewWindow: openInNewWindow, - forceOpenWorkspaceAsFile: commandId === 'openRecentFile' + urisToOpen: [uriToOpen], + forceNewWindow: openInNewWindow }).length > 0; if (!success) { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index f8effbf01f..15f4bf44ee 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -52,7 +52,7 @@ export interface IProgressService2 { _serviceBrand: any; - withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: () => void): Promise; + withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: () => void): Promise; } export interface IProgressRunner { diff --git a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index bddaea9c66..58a1ef576d 100644 --- a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts @@ -20,6 +20,10 @@ export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverS return Promise.resolve({ authority, host: authority, port: 80 }); } + clearResolvedAuthority(authority: string): void { + throw new Error(`Not implemented`); + } + setResolvedAuthority(resolvedAuthority: ResolvedAuthority) { throw new Error(`Not implemented`); } diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index d298b1708a..140a325ed4 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -21,6 +21,7 @@ export interface IRemoteAuthorityResolverService { resolveAuthority(authority: string): Promise; + clearResolvedAuthority(authority: string): void; setResolvedAuthority(resolvedAuthority: ResolvedAuthority): void; setResolvedAuthorityError(authority: string, err: any): void; } diff --git a/src/vs/platform/remote/electron-browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/electron-browser/remoteAuthorityResolverService.ts index 594ba0e927..d450256ecb 100644 --- a/src/vs/platform/remote/electron-browser/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/electron-browser/remoteAuthorityResolverService.ts @@ -5,6 +5,7 @@ import { ResolvedAuthority, IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ipcRenderer as ipc } from 'electron'; +import * as errors from 'vs/base/common/errors'; class PendingResolveAuthorityRequest { constructor( @@ -38,6 +39,11 @@ export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverS return this._resolveAuthorityRequests[authority].promise; } + clearResolvedAuthority(authority: string): void { + this._resolveAuthorityRequests[authority].reject(errors.canceled()); + delete this._resolveAuthorityRequests[authority]; + } + setResolvedAuthority(resolvedAuthority: ResolvedAuthority) { if (this._resolveAuthorityRequests[resolvedAuthority.authority]) { let request = this._resolveAuthorityRequests[resolvedAuthority.authority]; diff --git a/src/vs/platform/state/common/state.ts b/src/vs/platform/state/common/state.ts index 0b3f7b7f94..f09be2e7c9 100644 --- a/src/vs/platform/state/common/state.ts +++ b/src/vs/platform/state/common/state.ts @@ -12,6 +12,6 @@ export interface IStateService { getItem(key: string, defaultValue: T): T; getItem(key: string, defaultValue?: T): T | undefined; - setItem(key: string, data: any): void; + setItem(key: string, data?: object | string | number | boolean | undefined | null): void; removeItem(key: string): void; } \ No newline at end of file diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index 4a6e7f3974..af82bf3107 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -6,11 +6,10 @@ import * as path from 'vs/base/common/path'; import * as fs from 'fs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { writeFileAndFlushSync } from 'vs/base/node/extfs'; +import { writeFileSync, readFile } from 'vs/base/node/pfs'; import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { IStateService } from 'vs/platform/state/common/state'; import { ILogService } from 'vs/platform/log/common/log'; -import { readFile } from 'vs/base/node/pfs'; export class FileStorage { @@ -69,7 +68,7 @@ export class FileStorage { return res; } - setItem(key: string, data: any): void { + setItem(key: string, data?: object | string | number | boolean | undefined | null): void { // Remove an item when it is undefined or null if (isUndefinedOrNull(data)) { @@ -103,7 +102,7 @@ export class FileStorage { } try { - writeFileAndFlushSync(this.dbPath, serializedDatabase); // permission issue can happen here + writeFileSync(this.dbPath, serializedDatabase); // permission issue can happen here this.lastFlushedSerializedDatabase = serializedDatabase; } catch (error) { this.onError(error); @@ -136,7 +135,7 @@ export class StateService implements IStateService { return this.fileStorage.getItem(key, defaultValue); } - setItem(key: string, data: any): void { + setItem(key: string, data?: object | string | number | boolean | undefined | null): void { this.fileStorage.setItem(key, data); } diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index 14f5827175..a0abe8dda7 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -6,21 +6,21 @@ import * as assert from 'assert'; import * as os from 'os'; import * as path from 'vs/base/common/path'; -import * as extfs from 'vs/base/node/extfs'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileStorage } from 'vs/platform/state/node/stateService'; +import { mkdirp, rimraf, RimRafMode, writeFileSync } from 'vs/base/node/pfs'; suite('StateService', () => { const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'stateservice'); const storageFile = path.join(parentDir, 'storage.json'); teardown(done => { - extfs.del(parentDir, os.tmpdir(), done); + rimraf(parentDir, RimRafMode.MOVE).then(done, done); }); test('Basics', () => { - return extfs.mkdirp(parentDir).then(() => { - extfs.writeFileAndFlushSync(storageFile, ''); + return mkdirp(parentDir).then(() => { + writeFileSync(storageFile, ''); let service = new FileStorage(storageFile, () => null); diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 989aa37aab..babea85607 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -72,11 +72,12 @@ export interface IStorageService { /** * Store a value under the given key to storage. The value will be converted to a string. + * Storing either undefined or null will remove the entry under the key. * * The scope argument allows to define the scope of the storage * operation to either the current workspace only or all workspaces. */ - store(key: string, value: string | boolean | number, scope: StorageScope): void; + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void; /** * Delete an element stored under the provided key from storage. @@ -153,7 +154,7 @@ export class InMemoryStorageService extends Disposable implements IStorageServic return parseInt(value, 10); } - store(key: string, value: string | boolean | number, scope: StorageScope): Promise { + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise { // We remove the key for undefined/null values if (isUndefinedOrNull(value)) { diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 159973ea0e..18a91707e8 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -101,7 +101,7 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC const items = new Map(); events.forEach(event => items.set(event.key, this.storageMainService.get(event.key))); - return { items: mapToSerializable(items) } as ISerializableItemsChangeEvent; + return { items: mapToSerializable(items) }; } listen(_: unknown, event: string): Event { @@ -120,7 +120,7 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC case 'updateItems': { return this.whenReady.then(() => { - const items = arg as ISerializableUpdateRequest; + const items: ISerializableUpdateRequest = arg; if (items.insert) { for (const [key, value] of items.insert) { this.storageMainService.store(key, value); @@ -199,7 +199,7 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS close(): Promise { // when we are about to close, we start to ignore main-side changes since we close anyway - this.onDidChangeItemsOnMainListener = dispose(this.onDidChangeItemsOnMainListener); + dispose(this.onDidChangeItemsOnMainListener); return Promise.resolve(); // global storage is closed on the main side } @@ -207,6 +207,6 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS dispose(): void { super.dispose(); - this.onDidChangeItemsOnMainListener = dispose(this.onDidChangeItemsOnMainListener); + dispose(this.onDidChangeItemsOnMainListener); } } \ No newline at end of file diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index fede5a8d81..f14ecfb7c2 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -57,7 +57,7 @@ export interface IStorageMainService { * Store a string value under the given key to storage. The value will * be converted to a string. */ - store(key: string, value: any): void; + store(key: string, value: string | boolean | number | undefined | null): void; /** * Delete an element stored under the provided key from storage. @@ -109,7 +109,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic return { logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined, logError: error => this.logService.error(error) - } as ISQLiteStorageDatabaseLoggingOptions; + }; } initialize(): Promise { @@ -160,7 +160,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic return this.storage.getNumber(key, fallbackValue); } - store(key: string, value: any): Promise { + store(key: string, value: string | boolean | number | undefined | null): Promise { return this.storage.set(key, value); } diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 4e53040019..f4cdc631db 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -105,8 +105,8 @@ export class StorageService extends Disposable implements IStorageService { }; // Dispose old (if any) - this.workspaceStorage = dispose(this.workspaceStorage); - this.workspaceStorageListener = dispose(this.workspaceStorageListener); + dispose(this.workspaceStorage); + dispose(this.workspaceStorageListener); // Create new this.workspaceStoragePath = workspaceStoragePath; @@ -176,7 +176,7 @@ export class StorageService extends Disposable implements IStorageService { return this.getStorage(scope).getNumber(key, fallbackValue); } - store(key: string, value: string | boolean | number, scope: StorageScope): void { + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void { this.getStorage(scope).set(key, value); } diff --git a/src/vs/platform/storage/test/node/storageService.test.ts b/src/vs/platform/storage/test/node/storageService.test.ts index c84e73d2d5..6d4c04c92f 100644 --- a/src/vs/platform/storage/test/node/storageService.test.ts +++ b/src/vs/platform/storage/test/node/storageService.test.ts @@ -9,7 +9,7 @@ import { StorageService } from 'vs/platform/storage/node/storageService'; import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, del } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { NullLogService } from 'vs/platform/log/common/log'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs } from 'vs/platform/environment/node/argv'; @@ -115,6 +115,6 @@ suite('StorageService', () => { equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); await storage.close(); - await del(storageDir, tmpdir()); + await rimraf(storageDir, RimRafMode.MOVE); }); }); \ No newline at end of file diff --git a/src/vs/platform/telemetry/node/commonProperties.ts b/src/vs/platform/telemetry/node/commonProperties.ts index 6600924428..e7aa7b8af2 100644 --- a/src/vs/platform/telemetry/node/commonProperties.ts +++ b/src/vs/platform/telemetry/node/commonProperties.ts @@ -59,6 +59,11 @@ export function resolveCommonProperties(commit: string | undefined, version: str } }); + if (process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION) { + // __GDPR__COMMON__ "common.snap" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + result['common.snap'] = 'true'; + } + return readFile(installSourcePath, 'utf8').then(contents => { // __GDPR__COMMON__ "common.source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } diff --git a/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts index 85abe4f142..3767c234ac 100644 --- a/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/commonProperties.test.ts @@ -9,8 +9,7 @@ import * as fs from 'fs'; import { resolveWorkbenchCommonProperties } from 'vs/platform/telemetry/node/workbenchCommonProperties'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { IStorageService, StorageScope, InMemoryStorageService } from 'vs/platform/storage/common/storage'; -import { del } from 'vs/base/node/extfs'; -import { mkdirp } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; suite('Telemetry - common properties', function () { @@ -26,7 +25,7 @@ suite('Telemetry - common properties', function () { }); teardown(done => { - del(parentDir, os.tmpdir(), done); + rimraf(parentDir, RimRafMode.MOVE).then(done, done); }); // {{SQL CARBON EDIT}} diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index e1a7b18318..47948337ed 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -16,6 +16,7 @@ import { LogLevel } from 'vs/platform/log/common/log'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; export const IWindowsService = createDecorator('windowsService'); @@ -23,7 +24,7 @@ export interface INativeOpenDialogOptions { windowId?: number; forceNewWindow?: boolean; - dialogOptions?: OpenDialogOptions; + defaultPath?: string; telemetryEventName?: string; telemetryExtraData?: ITelemetryData; @@ -179,7 +180,6 @@ export interface IMessageBoxResult { export interface IOpenSettings { forceNewWindow?: boolean; forceReuseWindow?: boolean; - forceOpenWorkspaceAsFile?: boolean; diffMode?: boolean; addMode?: boolean; noRecentEntry?: boolean; @@ -187,14 +187,36 @@ export interface IOpenSettings { args?: ParsedArgs; } -export type URIType = 'file' | 'folder'; +export type IURIToOpen = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; -export interface IURIToOpen { - uri: URI; - typeHint?: URIType; +export interface IWorkspaceToOpen { + workspaceUri: URI; label?: string; } +export interface IFolderToOpen { + folderUri: URI; + label?: string; +} + +export interface IFileToOpen { + fileUri: URI; + label?: string; +} + +export function isWorkspaceToOpen(uriToOpen: IURIToOpen): uriToOpen is IWorkspaceToOpen { + return !!uriToOpen['workspaceUri']; +} + +export function isFolderToOpen(uriToOpen: IURIToOpen): uriToOpen is IFolderToOpen { + return !!uriToOpen['folderUri']; +} + +export function isFileToOpen(uriToOpen: IURIToOpen): uriToOpen is IFileToOpen { + return !!uriToOpen['fileUri']; +} + + export interface IWindowService { _serviceBrand: any; @@ -428,31 +450,31 @@ export interface IRunKeybindingInWindowRequest { export class ActiveWindowManager implements IDisposable { private disposables: IDisposable[] = []; - private firstActiveWindowIdPromise: Promise | null; - private _activeWindowId: number | undefined; + private firstActiveWindowIdPromise: CancelablePromise | undefined; + private activeWindowId: number | undefined; constructor(@IWindowsService windowsService: IWindowsService) { const onActiveWindowChange = Event.latch(Event.any(windowsService.onWindowOpen, windowsService.onWindowFocus)); onActiveWindowChange(this.setActiveWindow, this, this.disposables); - this.firstActiveWindowIdPromise = windowsService.getActiveWindowId() - .then(id => (typeof this._activeWindowId === 'undefined') && this.setActiveWindow(id)); + this.firstActiveWindowIdPromise = createCancelablePromise(_ => windowsService.getActiveWindowId()); + this.firstActiveWindowIdPromise + .then(id => this.activeWindowId = id) + .finally(this.firstActiveWindowIdPromise = undefined); } private setActiveWindow(windowId: number | undefined) { if (this.firstActiveWindowIdPromise) { - this.firstActiveWindowIdPromise = null; + this.firstActiveWindowIdPromise.cancel(); + this.firstActiveWindowIdPromise = undefined; } - this._activeWindowId = windowId; + this.activeWindowId = windowId; } - getActiveClientId(): Promise { - if (this.firstActiveWindowIdPromise) { - return this.firstActiveWindowIdPromise; - } - - return Promise.resolve(`window:${this._activeWindowId}`); + async getActiveClientId(): Promise { + const id = this.firstActiveWindowIdPromise ? (await this.firstActiveWindowIdPromise) : this.activeWindowId; + return `window:${id}`; } dispose() { diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index 118656cde5..5bd18c6f0f 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { IWindowService, IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, IDevToolsOptions, IOpenSettings, IURIToOpen } from 'vs/platform/windows/common/windows'; +import { IWindowService, IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, 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 { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { ILabelService } from 'vs/platform/label/common/label'; export class WindowService extends Disposable implements IWindowService { @@ -100,7 +99,7 @@ export class WindowService extends Disposable implements IWindowService { openWindow(uris: IURIToOpen[], options: IOpenSettings = {}): Promise { if (!!this.configuration.remoteAuthority) { - uris.forEach(u => u.label = u.label || this.getRecentLabel(u, !!(options && options.forceOpenWorkspaceAsFile))); + uris.forEach(u => u.label = u.label || this.getRecentLabel(u)); } return this.windowsService.openWindow(this.windowId, uris, options); } @@ -173,13 +172,13 @@ export class WindowService extends Disposable implements IWindowService { return this.windowsService.resolveProxy(this.windowId, url); } - private getRecentLabel(u: IURIToOpen, forceOpenWorkspaceAsFile: boolean): string { - if (u.typeHint === 'folder') { - return this.labelService.getWorkspaceLabel(u.uri, { verbose: true }); - } else if (!forceOpenWorkspaceAsFile && hasWorkspaceFileExtension(u.uri.path)) { - return this.labelService.getWorkspaceLabel({ id: '', configPath: u.uri }, { verbose: true }); + private getRecentLabel(u: IURIToOpen): string { + if (isFolderToOpen(u)) { + return this.labelService.getWorkspaceLabel(u.folderUri, { verbose: true }); + } else if (isWorkspaceToOpen(u)) { + return this.labelService.getWorkspaceLabel({ id: '', configPath: u.workspaceUri }, { verbose: true }); } else { - return this.labelService.getUriLabel(u.uri); + return this.labelService.getUriLabel(u.fileUri); } } } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 9c1e61ac0d..1f0754bda9 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -98,10 +98,10 @@ export interface IWindowsMainService { closeWorkspace(win: ICodeWindow): void; open(openConfig: IOpenConfiguration): ICodeWindow[]; openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string, openConfig: IOpenConfiguration): void; - pickFileFolderAndOpen(options: INativeOpenDialogOptions): void; - pickFolderAndOpen(options: INativeOpenDialogOptions): void; - pickFileAndOpen(options: INativeOpenDialogOptions): void; - pickWorkspaceAndOpen(options: INativeOpenDialogOptions): void; + pickFileFolderAndOpen(options: INativeOpenDialogOptions): Promise; + pickFolderAndOpen(options: INativeOpenDialogOptions): Promise; + pickFileAndOpen(options: INativeOpenDialogOptions): Promise; + pickWorkspaceAndOpen(options: INativeOpenDialogOptions): Promise; showMessageBox(options: Electron.MessageBoxOptions, win?: ICodeWindow): Promise; showSaveDialog(options: Electron.SaveDialogOptions, win?: ICodeWindow): Promise; showOpenDialog(options: Electron.OpenDialogOptions, win?: ICodeWindow): Promise; @@ -133,7 +133,6 @@ export interface IOpenConfiguration { readonly forceEmpty?: boolean; readonly diffMode?: boolean; addMode?: boolean; - readonly forceOpenWorkspaceAsFile?: boolean; readonly initialStartup?: boolean; readonly noRecentEntry?: boolean; } diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index bf6047e619..1d532f2a1e 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -290,7 +290,6 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable cli: options.args ? { ...this.environmentService.args, ...options.args } : this.environmentService.args, forceNewWindow: options.forceNewWindow, forceReuseWindow: options.forceReuseWindow, - forceOpenWorkspaceAsFile: options.forceOpenWorkspaceAsFile, diffMode: options.diffMode, addMode: options.addMode, noRecentEntry: options.noRecentEntry, @@ -380,6 +379,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable version = `${version} (${product.target} setup)`; } + const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION; // {{SQL CARBON EDIT}} const detail = nls.localize('aboutDetail', "Version: {0}\nCommit: {1}\nDate: {2}\nVS Code {8}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nOS: {7}", @@ -390,7 +390,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable process.versions['chrome'], process.versions['node'], process.versions['v8'], - `${os.type()} ${os.arch()} ${os.release()}`, + `${os.type()} ${os.arch()} ${os.release()}${isSnap ? ' snap' : ''}`, product.vscodeVersion ); @@ -422,7 +422,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable // Catch file URLs if (uri.authority === Schemas.file && !!uri.path) { - this.openFileForURI({ uri: URI.file(uri.fsPath) }); // using fsPath on a non-file URI... + this.openFileForURI({ fileUri: URI.file(uri.fsPath) }); // using fsPath on a non-file URI... return true; } diff --git a/src/vs/platform/windows/node/windowsIpc.ts b/src/vs/platform/windows/node/windowsIpc.ts index 8d2b312d5d..f90f3590d2 100644 --- a/src/vs/platform/windows/node/windowsIpc.ts +++ b/src/vs/platform/windows/node/windowsIpc.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWindowsService, IURIToOpen, IOpenSettings } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IURIToOpen, IOpenSettings, isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; import { reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; import { IRecent, isRecentFile, isRecentFolder } from 'vs/platform/history/common/history'; @@ -89,7 +89,15 @@ export class WindowsChannel implements IServerChannel { case 'openWindow': { const urisToOpen: IURIToOpen[] = arg[1]; const options: IOpenSettings = arg[2]; - urisToOpen.forEach(r => { r.uri = URI.revive(r.uri); return r; }); + urisToOpen.forEach(r => { + if (isWorkspaceToOpen(r)) { + r.workspaceUri = URI.revive(r.workspaceUri); + } else if (isFolderToOpen(r)) { + r.folderUri = URI.revive(r.folderUri); + } else { + r.fileUri = URI.revive(r.fileUri); + } + }); options.waitMarkerFileURI = options.waitMarkerFileURI && URI.revive(options.waitMarkerFileURI); return this.service.openWindow(arg[0], urisToOpen, options); } diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index edaf0186c0..f356346f40 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -6,10 +6,9 @@ import { IWorkspacesMainService, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { join, dirname } from 'vs/base/common/path'; -import { mkdirp, writeFile } from 'vs/base/node/pfs'; -import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs'; +import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; +import { readFileSync, existsSync, mkdirSync } from 'fs'; import { isLinux } from 'vs/base/common/platform'; -import { delSync, readdirSync, writeFileAndFlushSync } from 'vs/base/node/extfs'; import { Event, Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; import { createHash } from 'crypto'; @@ -126,7 +125,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain mkdirSync(configPathDir); } - writeFileAndFlushSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); + writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); return workspace; } @@ -176,8 +175,9 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { const configPath = originalFSPath(workspace.configPath); try { + // Delete Workspace - delSync(dirname(configPath)); + rimrafSync(dirname(configPath)); // Mark Workspace Storage to be deleted const workspaceStoragePath = join(this.environmentService.workspaceStorageHome, workspace.id); diff --git a/src/vs/platform/workspaces/node/workspacesIpc.ts b/src/vs/platform/workspaces/node/workspacesIpc.ts index 579d8c2106..1212a505a3 100644 --- a/src/vs/platform/workspaces/node/workspacesIpc.ts +++ b/src/vs/platform/workspaces/node/workspacesIpc.ts @@ -27,7 +27,7 @@ export class WorkspacesChannel implements IServerChannel { return { uri: URI.revive(rawFolder.uri), // convert raw URI back to real URI name: rawFolder.name - } as IWorkspaceFolderCreationData; + }; }); } diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index 5386bf9515..9f6facb91e 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -55,13 +55,13 @@ suite('WorkspacesMainService', () => { service = new TestWorkspacesMainService(environmentService, logService); // Delete any existing backups completely and then re-create it. - return pfs.del(untitledWorkspacesHomePath, os.tmpdir()).then(() => { + return pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE).then(() => { return pfs.mkdirp(untitledWorkspacesHomePath); }); }); teardown(() => { - return pfs.del(untitledWorkspacesHomePath, os.tmpdir()); + return pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); }); function assertPathEquals(p1: string, p2: string): void { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ab2cc73669..03cf6111ec 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5935,7 +5935,7 @@ declare module 'vscode' { * serializer must restore the webview's `.html` and hook up all webview events. * @param state Persisted state from the webview content. * - * @return Thenble indicating that the webview has been fully restored. + * @return Thenable indicating that the webview has been fully restored. */ deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable; } @@ -6640,7 +6640,7 @@ declare module 'vscode' { * be able to handle uris which are directed to the extension itself. A uri must respect * the following rules: * - * - The uri-scheme must be the product name; + * - The uri-scheme must be `vscode.env.uriScheme`; * - The uri-authority must be the extension id (eg. `my.extension`); * - The uri-path, -query and -fragment parts are arbitrary. * diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 66bebec815..f06455a725 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -222,7 +222,6 @@ declare module 'vscode' { * See the vscode setting `"search.useGlobalIgnoreFiles"`. */ useGlobalIgnoreFiles: boolean; - } /** @@ -391,7 +390,6 @@ declare module 'vscode' { * Provide the set of files that match a certain file path pattern. * @param query The parameters for this query. * @param options A set of options to consider while searching files. - * @param progress A progress callback that must be invoked for all results. * @param token A cancellation token. */ provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult; @@ -625,6 +623,11 @@ declare module 'vscode' { * An [event](#Event) that fires when the log level has changed. */ export const onDidChangeLogLevel: Event; + + /** + * The custom uri scheme the editor registers to in the operating system, like 'vscode', 'vscode-insiders'. + */ + export const uriScheme: string; } //#endregion @@ -1402,7 +1405,7 @@ declare module 'vscode' { * running on. * * If a webview accesses localhost content, we recomend that you specify port mappings even if - * the `from` and `to` ports are the same. + * the `port` and `resolvedPort` ports are the same. */ readonly portMapping?: ReadonlyArray; } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 36ec655075..262c3c67b8 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -109,40 +109,48 @@ export class MainThreadCommentThread implements modes.CommentThread2 { private _onDidChangeLabel = new Emitter(); get onDidChangeLabel(): Event { return this._onDidChangeLabel.event; } + private _comments: modes.Comment[] | undefined; - public get comments(): modes.Comment[] { + public get comments(): modes.Comment[] | undefined { return this._comments; } - public set comments(newComments: modes.Comment[]) { + public set comments(newComments: modes.Comment[] | undefined) { this._comments = newComments; this._onDidChangeComments.fire(this._comments); } - private _onDidChangeComments = new Emitter(); - get onDidChangeComments(): Event { return this._onDidChangeComments.event; } + private _onDidChangeComments = new Emitter(); + get onDidChangeComments(): Event { return this._onDidChangeComments.event; } - set acceptInputCommand(newCommand: modes.Command) { + private _acceptInputCommand: modes.Command | undefined; + set acceptInputCommand(newCommand: modes.Command | undefined) { this._acceptInputCommand = newCommand; this._onDidChangeAcceptInputCommand.fire(this._acceptInputCommand); } - get acceptInputCommand(): modes.Command { + get acceptInputCommand(): modes.Command | undefined { return this._acceptInputCommand!; } - private _onDidChangeAcceptInputCommand = new Emitter(); - get onDidChangeAcceptInputCommand(): Event { return this._onDidChangeAcceptInputCommand.event; } + private _onDidChangeAcceptInputCommand = new Emitter(); + get onDidChangeAcceptInputCommand(): Event { return this._onDidChangeAcceptInputCommand.event; } - set additionalCommands(newCommands: modes.Command[]) { + private _additionalCommands: modes.Command[] | undefined; + set additionalCommands(newCommands: modes.Command[] | undefined) { this._additionalCommands = newCommands; this._onDidChangeAdditionalCommands.fire(this._additionalCommands); } - get additionalCommands(): modes.Command[] { + get additionalCommands(): modes.Command[] | undefined { return this._additionalCommands; } + private _onDidChangeAdditionalCommands = new Emitter(); + get onDidChangeAdditionalCommands(): Event { return this._onDidChangeAdditionalCommands.event; } + + private _deleteCommand: modes.Command | undefined; + set deleteCommand(newCommand: modes.Command | undefined) { this._deleteCommand = newCommand; } @@ -151,9 +159,6 @@ export class MainThreadCommentThread implements modes.CommentThread2 { return this._deleteCommand; } - private _onDidChangeAdditionalCommands = new Emitter(); - get onDidChangeAdditionalCommands(): Event { return this._onDidChangeAdditionalCommands.event; } - set range(range: IRange) { this._range = range; this._onDidChangeRange.fire(this._range); @@ -166,16 +171,17 @@ export class MainThreadCommentThread implements modes.CommentThread2 { private _onDidChangeRange = new Emitter(); public onDidChangeRange = this._onDidChangeRange.event; + private _collapsibleState: modes.CommentThreadCollapsibleState | undefined; get collapsibleState() { return this._collapsibleState; } - set collapsibleState(newState: modes.CommentThreadCollapsibleState) { + set collapsibleState(newState: modes.CommentThreadCollapsibleState | undefined) { this._collapsibleState = newState; this._onDidChangeCollasibleState.fire(this._collapsibleState); } - private _onDidChangeCollasibleState = new Emitter(); + private _onDidChangeCollasibleState = new Emitter(); public onDidChangeCollasibleState = this._onDidChangeCollasibleState.event; constructor( @@ -184,14 +190,24 @@ export class MainThreadCommentThread implements modes.CommentThread2 { public extensionId: string, public threadId: string, public resource: string, - private _range: IRange, - private _comments: modes.Comment[], - private _acceptInputCommand: modes.Command | undefined, - private _additionalCommands: modes.Command[], - private _deleteCommand: modes.Command | undefined, - private _collapsibleState: modes.CommentThreadCollapsibleState - ) { + private _range: IRange + ) { } + batchUpdate( + range: IRange, + label: string, + comments: modes.Comment[], + acceptInputCommand: modes.Command | undefined, + additionalCommands: modes.Command[], + deleteCommand: modes.Command | undefined, + collapsibleState: modes.CommentThreadCollapsibleState) { + this._range = range; + this._label = label; + this._comments = comments; + this._acceptInputCommand = acceptInputCommand; + this._additionalCommands = additionalCommands; + this._deleteCommand = deleteCommand; + this._collapsibleState = collapsibleState; } dispose() { } @@ -254,23 +270,14 @@ export class MainThreadCommentController { threadId: string, resource: UriComponents, range: IRange, - comments: modes.Comment[], - acceptInputCommand: modes.Command | undefined, - additionalCommands: modes.Command[], - deleteCommand: modes.Command | undefined, - collapseState: modes.CommentThreadCollapsibleState): modes.CommentThread2 { + ): modes.CommentThread2 { let thread = new MainThreadCommentThread( commentThreadHandle, this, '', threadId, URI.revive(resource).toString(), - range, - comments, - acceptInputCommand, - additionalCommands, - deleteCommand, - collapseState + range ); this._threads.set(commentThreadHandle, thread); @@ -284,6 +291,27 @@ export class MainThreadCommentController { return thread; } + updateCommentThread(commentThreadHandle: number, + threadId: string, + resource: UriComponents, + range: IRange, + label: string, + comments: modes.Comment[], + acceptInputCommand: modes.Command | undefined, + additionalCommands: modes.Command[], + deleteCommand: modes.Command | undefined, + collapsibleState: modes.CommentThreadCollapsibleState): void { + let thread = this.getKnownThread(commentThreadHandle); + thread.batchUpdate(range, label, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); + + this._commentService.updateComments(this._uniqueId, { + added: [], + removed: [], + changed: [thread], + draftMode: modes.DraftMode.NotSupported + }); + } + deleteCommentThread(commentThreadHandle: number) { let thread = this.getKnownThread(commentThreadHandle); this._threads.delete(commentThreadHandle); @@ -298,48 +326,6 @@ export class MainThreadCommentController { thread.dispose(); } - updateComments(commentThreadHandle: number, comments: modes.Comment[]) { - let thread = this.getKnownThread(commentThreadHandle); - thread.comments = comments; - - this._commentService.updateComments(this._uniqueId, { - added: [], - removed: [], - changed: [thread], - draftMode: modes.DraftMode.NotSupported - }); - } - - updateAcceptInputCommand(commentThreadHandle: number, acceptInputCommand: modes.Command) { - let thread = this.getKnownThread(commentThreadHandle); - thread.acceptInputCommand = acceptInputCommand; - } - - updateAdditionalCommands(commentThreadHandle: number, additionalCommands: modes.Command[]) { - let thread = this.getKnownThread(commentThreadHandle); - thread.additionalCommands = additionalCommands; - } - - updateDeleteCommand(commentThreadHandle: number, deleteCommand: modes.Command) { - const thread = this.getKnownThread(commentThreadHandle); - thread.deleteCommand = deleteCommand; - } - - updateCollapsibleState(commentThreadHandle: number, collapseState: modes.CommentThreadCollapsibleState) { - let thread = this.getKnownThread(commentThreadHandle); - thread.collapsibleState = collapseState; - } - - updateCommentThreadRange(commentThreadHandle: number, range: IRange) { - let thread = this.getKnownThread(commentThreadHandle); - thread.range = range; - } - - updateCommentThreadLabel(commentThreadHandle: number, label: string) { - let thread = this.getKnownThread(commentThreadHandle); - thread.label = label; - } - updateInput(input: string) { let thread = this.activeCommentThread; @@ -358,7 +344,6 @@ export class MainThreadCommentController { return thread; } - async getDocumentComments(resource: URI, token: CancellationToken) { let ret: modes.CommentThread2[] = []; for (let thread of keys(this._threads)) { @@ -499,19 +484,35 @@ export class MainThreadComments extends Disposable implements MainThreadComments commentThreadHandle: number, threadId: string, resource: UriComponents, - range: IRange, - comments: modes.Comment[], - acceptInputCommand: modes.Command | undefined, - additionalCommands: modes.Command[], - deleteCommand: modes.Command, - collapseState: modes.CommentThreadCollapsibleState): modes.CommentThread2 | undefined { + range: IRange + ): modes.CommentThread2 | undefined { let provider = this._commentControllers.get(handle); if (!provider) { return undefined; } - return provider.createCommentThread(commentThreadHandle, threadId, resource, range, comments, acceptInputCommand, additionalCommands, deleteCommand, collapseState); + return provider.createCommentThread(commentThreadHandle, threadId, resource, range); + } + + $updateCommentThread(handle: number, + commentThreadHandle: number, + threadId: string, + resource: UriComponents, + range: IRange, + label: string, + comments: modes.Comment[], + acceptInputCommand: modes.Command | undefined, + additionalCommands: modes.Command[], + deleteCommand: modes.Command, + collapsibleState: modes.CommentThreadCollapsibleState): void { + let provider = this._commentControllers.get(handle); + + if (!provider) { + return undefined; + } + + return provider.updateCommentThread(commentThreadHandle, threadId, resource, range, label, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); } $deleteCommentThread(handle: number, commentThreadHandle: number) { @@ -524,16 +525,6 @@ export class MainThreadComments extends Disposable implements MainThreadComments return provider.deleteCommentThread(commentThreadHandle); } - $updateComments(handle: number, commentThreadHandle: number, comments: modes.Comment[]) { - let provider = this._commentControllers.get(handle); - - if (!provider) { - return; - } - - provider.updateComments(commentThreadHandle, comments); - } - $setInputValue(handle: number, input: string) { let provider = this._commentControllers.get(handle); @@ -544,66 +535,6 @@ export class MainThreadComments extends Disposable implements MainThreadComments provider.updateInput(input); } - $updateCommentThreadAcceptInputCommand(handle: number, commentThreadHandle: number, acceptInputCommand: modes.Command) { - let provider = this._commentControllers.get(handle); - - if (!provider) { - return; - } - - provider.updateAcceptInputCommand(commentThreadHandle, acceptInputCommand); - } - - $updateCommentThreadAdditionalCommands(handle: number, commentThreadHandle: number, additionalCommands: modes.Command[]) { - let provider = this._commentControllers.get(handle); - - if (!provider) { - return; - } - - provider.updateAdditionalCommands(commentThreadHandle, additionalCommands); - } - - $updateCommentThreadDeleteCommand(handle: number, commentThreadHandle: number, acceptInputCommand: modes.Command) { - let provider = this._commentControllers.get(handle); - - if (!provider) { - return; - } - - provider.updateDeleteCommand(commentThreadHandle, acceptInputCommand); - } - - $updateCommentThreadCollapsibleState(handle: number, commentThreadHandle: number, collapseState: modes.CommentThreadCollapsibleState): void { - let provider = this._commentControllers.get(handle); - - if (!provider) { - return; - } - - provider.updateCollapsibleState(commentThreadHandle, collapseState); - } - - $updateCommentThreadRange(handle: number, commentThreadHandle: number, range: any): void { - let provider = this._commentControllers.get(handle); - - if (!provider) { - return; - } - - provider.updateCommentThreadRange(commentThreadHandle, range); - } - - $updateCommentThreadLabel(handle: number, commentThreadHandle: number, label: string): void { - let provider = this._commentControllers.get(handle); - - if (!provider) { - return; - } - - provider.updateCommentThreadLabel(commentThreadHandle, label); - } - $registerDocumentCommentProvider(handle: number, features: CommentProviderFeatures): void { this._documentProviders.set(handle, undefined); const handler = new MainThreadDocumentCommentProvider(this._proxy, handle, features); diff --git a/src/vs/workbench/api/browser/mainThreadConsole.ts b/src/vs/workbench/api/browser/mainThreadConsole.ts index 881eff63e9..5e6e86b3b6 100644 --- a/src/vs/workbench/api/browser/mainThreadConsole.ts +++ b/src/vs/workbench/api/browser/mainThreadConsole.ts @@ -9,8 +9,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IRemoteConsoleLog, log, parse } from 'vs/base/common/console'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { IWindowsService } from 'vs/platform/windows/common/windows'; -import { IBroadcastService } from 'vs/workbench/services/broadcast/common/broadcast'; -import { EXTENSION_LOG_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; +import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug'; @extHostNamedCustomer(MainContext.MainThreadConsole) export class MainThreadConsole implements MainThreadConsoleShape { @@ -22,7 +21,7 @@ export class MainThreadConsole implements MainThreadConsoleShape { extHostContext: IExtHostContext, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IWindowsService private readonly _windowsService: IWindowsService, - @IBroadcastService private readonly _broadcastService: IBroadcastService, + @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, ) { const devOpts = parseExtensionDevOptions(this._environmentService); this._isExtensionDevHost = devOpts.isExtensionDevHost; @@ -45,14 +44,8 @@ export class MainThreadConsole implements MainThreadConsoleShape { } // Broadcast to other windows if we are in development mode - else if (!this._environmentService.isBuilt || this._isExtensionDevHost) { - this._broadcastService.broadcast({ - channel: EXTENSION_LOG_BROADCAST_CHANNEL, - payload: { - logEntry: entry, - debugId: this._environmentService.debugExtensionHost.debugId - } - }); + else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) { + this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry); } } } diff --git a/src/vs/workbench/api/browser/mainThreadKeytar.ts b/src/vs/workbench/api/browser/mainThreadKeytar.ts new file mode 100644 index 0000000000..4955145966 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadKeytar.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { MainContext, MainThreadKeytarShape, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; + +interface IKeytarModule { + getPassword(service: string, account: string): Promise; + setPassword(service: string, account: string, password: string): Promise; + deletePassword(service: string, account: string): Promise; + findPassword(service: string): Promise; +} + +@extHostNamedCustomer(MainContext.MainThreadKeytar) +export class MainThreadKeytar implements MainThreadKeytarShape { + + private _keytar: IKeytarModule | null; + + constructor( + extHostContext: IExtHostContext + ) { + try { + this._keytar = require.__$__nodeRequire('keytar'); + } catch (e) { + this._keytar = null; + } + } + + dispose(): void { + // + } + + async $getPassword(service: string, account: string): Promise { + if (this._keytar) { + return this._keytar.getPassword(service, account); + } + return null; + } + + async $setPassword(service: string, account: string, password: string): Promise { + if (this._keytar) { + return this._keytar.setPassword(service, account, password); + } + } + + async $deletePassword(service: string, account: string): Promise { + if (this._keytar) { + return this._keytar.deletePassword(service, account); + } + return false; + } + + async $findPassword(service: string): Promise { + if (this._keytar) { + return this._keytar.findPassword(service); + } + return null; + } +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index c6a089bff9..3f91f28e3d 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -10,8 +10,8 @@ import * as modes from 'vs/editor/common/modes'; import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; -import { Range as EditorRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, CodeInsetDto, LinkDto, CallHierarchyDto } from '../common/extHost.protocol'; +import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, CodeInsetDto, LinkDto, CallHierarchyDto, SuggestDataDto } from '../common/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -22,6 +22,7 @@ import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { IHeapService } from 'vs/workbench/services/heap/common/heap'; +import { mixin } from 'vs/base/common/objects'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -150,8 +151,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return dto; }); }, - resolveCodeLens: (model: ITextModel, codeLens: modes.ICodeLensSymbol, token: CancellationToken): Promise => { - return this._proxy.$resolveCodeLens(handle, model.uri, codeLens, token).then(obj => { + resolveCodeLens: (_model: ITextModel, codeLens: modes.ICodeLensSymbol, token: CancellationToken): Promise => { + return this._proxy.$resolveCodeLens(handle, codeLens, token).then(obj => { if (obj) { this._heapService.trackObject(obj); this._heapService.trackObject(obj.command); @@ -359,29 +360,56 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- suggest + private static _inflateSuggestDto(defaultRange: IRange, data: SuggestDataDto): modes.CompletionItem { + return { + label: data.a, + kind: data.b, + detail: data.c, + documentation: data.d, + sortText: data.e, + filterText: data.f, + preselect: data.g, + insertText: data.h || data.a, + insertTextRules: data.i, + range: data.j || defaultRange, + commitCharacters: data.k, + additionalTextEdits: data.l, + command: data.m, + // not-standard + _id: data.x, + _pid: data.y + }; + } + $registerSuggestSupport(handle: number, selector: ISerializedDocumentFilter[], triggerCharacters: string[], supportsResolveDetails: boolean): void { - this._registrations[handle] = modes.CompletionProviderRegistry.register(selector, { + const provider: modes.CompletionItemProvider = { triggerCharacters, provideCompletionItems: (model: ITextModel, position: EditorPosition, context: modes.CompletionContext, token: CancellationToken): Promise => { return this._proxy.$provideCompletionItems(handle, model.uri, position, context, token).then(result => { if (!result) { - return result; + // {{SQL CARBON EDIT}} @todo anthonydresser required because of strict null checks + return undefined; } return { - suggestions: result.suggestions, - incomplete: result.incomplete, - dispose: () => { - if (typeof result._id === 'number') { - this._proxy.$releaseCompletionItems(handle, result._id); - } - } + suggestions: result.b.map(d => MainThreadLanguageFeatures._inflateSuggestDto(result.a, d)), + incomplete: result.c, + dispose: () => this._proxy.$releaseCompletionItems(handle, result.x) }; }); - }, - resolveCompletionItem: supportsResolveDetails - ? (model, position, suggestion, token) => this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion, token) - : undefined - }); + } + }; + if (supportsResolveDetails) { + provider.resolveCompletionItem = (model, position, suggestion, token) => { + return this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion._id, suggestion._pid, token).then(result => { + if (!result) { + return suggestion; + } + let newSuggestion = MainThreadLanguageFeatures._inflateSuggestDto(suggestion.range, result); + return mixin(suggestion, newSuggestion, true); + }); + }; + } + this._registrations[handle] = modes.CompletionProviderRegistry.register(selector, provider); } // --- parameter hints diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 64c6a9cbc6..3fed376eb1 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -220,7 +220,7 @@ CommandsRegistry.registerCommand('_workbench.enterWorkspace', async function (ac const runningExtensions = await extensionService.getExtensions(); // If requested extension to disable is running, then reload window with given workspace if (disableExtensions && runningExtensions.some(runningExtension => disableExtensions.some(id => ExtensionIdentifier.equals(runningExtension.identifier, id)))) { - return windowService.openWindow([{ uri: workspace, typeHint: 'file' }], { args: { _: [], 'disable-extension': disableExtensions } }); + return windowService.openWindow([{ workspaceUri: workspace }], { args: { _: [], 'disable-extension': disableExtensions } }); } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ce15477c6d..6a26d7be0f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -126,16 +126,10 @@ export interface MainThreadCommentsShape extends IDisposable { $registerCommentController(handle: number, id: string, label: string): void; $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; - $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], deleteCommand: modes.Command | undefined, collapseState: modes.CommentThreadCollapsibleState): modes.CommentThread2 | undefined; + $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange): modes.CommentThread2 | undefined; + $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, label: string, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], deleteCommand: modes.Command | undefined, collapseState: modes.CommentThreadCollapsibleState): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; - $updateComments(handle: number, commentThreadHandle: number, comments: modes.Comment[]): void; $setInputValue(handle: number, input: string): void; - $updateCommentThreadAcceptInputCommand(handle: number, commentThreadHandle: number, acceptInputCommand: modes.Command): void; - $updateCommentThreadAdditionalCommands(handle: number, commentThreadHandle: number, additionalCommands: modes.Command[]): void; - $updateCommentThreadDeleteCommand(handle: number, commentThreadHandle: number, deleteCommand: modes.Command): void; - $updateCommentThreadCollapsibleState(handle: number, commentThreadHandle: number, collapseState: modes.CommentThreadCollapsibleState): void; - $updateCommentThreadRange(handle: number, commentThreadHandle: number, range: IRange): void; - $updateCommentThreadLabel(handle: number, commentThreadHandle: number, label: string): void; $registerDocumentCommentProvider(handle: number, features: CommentProviderFeatures): void; $unregisterDocumentCommentProvider(handle: number): void; $registerWorkspaceCommentProvider(handle: number, extensionId: ExtensionIdentifier): void; @@ -266,6 +260,13 @@ export interface MainThreadConsoleShape extends IDisposable { }): void; } +export interface MainThreadKeytarShape extends IDisposable { + $getPassword(service: string, account: string): Promise; + $setPassword(service: string, account: string, password: string): Promise; + $deletePassword(service: string, account: string): Promise; + $findPassword(service: string): Promise; +} + export interface ISerializedRegExp { pattern: string; flags?: string; @@ -862,14 +863,30 @@ export class IdObject { } } -export interface SuggestionDto extends modes.CompletionItem { - _id: number; - _parentId: number; +export interface SuggestDataDto { + a/* label */: string; + b/* kind */: modes.CompletionItemKind; + c/* detail */?: string; + d/* documentation */?: string | IMarkdownString; + e/* sortText */?: string; + f/* filterText */?: string; + g/* preselect */?: boolean; + h/* insertText */?: string; + i/* insertTextRules */?: modes.CompletionItemInsertTextRule; + j/* range */?: IRange; + k/* commitCharacters */?: string[]; + l/* additionalTextEdits */?: ISingleEditOperation[]; + m/* command */?: modes.Command; + // not-standard + x: number; + y: number; } -export interface SuggestResultDto extends IdObject { - suggestions: SuggestionDto[]; - incomplete?: boolean; +export interface SuggestResultDto { + x: number; + a: IRange; + b: SuggestDataDto[]; + c?: boolean; } export interface LocationDto { @@ -970,7 +987,7 @@ export interface CallHierarchyDto { export interface ExtHostLanguageFeaturesShape { $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise; $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise; - $resolveCodeLens(handle: number, resource: UriComponents, symbol: CodeLensDto, token: CancellationToken): Promise; + $resolveCodeLens(handle: number, symbol: CodeLensDto, token: CancellationToken): Promise; $provideCodeInsets(handle: number, resource: UriComponents, token: CancellationToken): Promise; $resolveCodeInset(handle: number, resource: UriComponents, symbol: CodeInsetDto, token: CancellationToken): Promise; $provideDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; @@ -990,7 +1007,7 @@ export interface ExtHostLanguageFeaturesShape { $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string, token: CancellationToken): Promise; $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise; - $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.CompletionItem, token: CancellationToken): Promise; + $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, id: number, pid: number, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise; $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Promise; @@ -1199,6 +1216,7 @@ export const MainContext = { MainThreadTextEditors: createMainId('MainThreadTextEditors'), MainThreadErrors: createMainId('MainThreadErrors'), MainThreadTreeViews: createMainId('MainThreadTreeViews'), + MainThreadKeytar: createMainId('MainThreadKeytar'), MainThreadLanguageFeatures: createMainId('MainThreadLanguageFeatures'), MainThreadLanguages: createMainId('MainThreadLanguages'), MainThreadMessageService: createMainId('MainThreadMessageService'), diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 7791d9ae48..a1cb49b8d9 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -34,7 +34,6 @@ namespace schema { case 'explorer/context': return MenuId.ExplorerContext; case 'editor/title/context': return MenuId.EditorTitleContext; case 'debug/callstack/context': return MenuId.DebugCallStackContext; - case 'debug/toolbar': return MenuId.DebugToolBar; case 'debug/toolBar': return MenuId.DebugToolBar; case 'menuBar/file': return MenuId.MenubarFileMenu; case 'scm/title': return MenuId.SCMTitle; diff --git a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts index fbca87430c..44aa26ab31 100644 --- a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts @@ -33,6 +33,7 @@ import '../browser/mainThreadExtensionService'; import '../browser/mainThreadFileSystem'; import '../browser/mainThreadFileSystemEventService'; import '../browser/mainThreadHeapService'; +import '../browser/mainThreadKeytar'; import '../browser/mainThreadLanguageFeatures'; import '../browser/mainThreadLanguages'; import '../browser/mainThreadLogService'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index bd55cc833f..04ccf2cdeb 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -7,30 +7,38 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as map from 'vs/base/common/map'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IWebviewOptions } from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import product from 'vs/platform/product/node/product'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewInsetHandle, WebviewPanelHandle, WebviewPanelShowOptions } from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { CodeInsetController } from 'vs/workbench/contrib/codeinset/electron-browser/codeInset.contribution'; -import { WebviewEditor } from 'vs/workbench/contrib/webview/electron-browser/webviewEditor'; -import { WebviewEditorInput } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInput'; -import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorService'; +import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; +import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; +import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewEditorService'; import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { extHostNamedCustomer } from '../common/extHostCustomers'; -import { IWebviewOptions } from 'vs/editor/common/modes'; @extHostNamedCustomer(MainContext.MainThreadWebviews) export class MainThreadWebviews extends Disposable implements MainThreadWebviewsShape { - private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto']; + private static readonly standardSupportedLinkSchemes = new Set([ + 'http', + 'https', + 'mailto', + product.urlProtocol, + 'vscode', + 'vscode-insiders' + ]); private static revivalPool = 0; @@ -104,7 +112,6 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews }; this._webviews.set(handle, webview); - this._activeWebview = handle; /* __GDPR__ "webviews:createWebviewPanel" : { @@ -200,9 +207,9 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews return; } - const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)); + const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0); if (targetGroup) { - this._webviewService.revealWebview(webview, targetGroup || this._editorGroupService.getGroup(webview.group || ACTIVE_GROUP), !!showOptions.preserveFocus); + this._webviewService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus); } } @@ -366,12 +373,18 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews } const webview = this.getWebview(handle); - const enableCommandUris = webview.options.enableCommandUris; - if (MainThreadWebviews.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || enableCommandUris && link.scheme === 'command') { + if (this.isSupportedLink(webview, link)) { this._openerService.open(link); } } + private isSupportedLink(webview: WebviewEditorInput, link: URI): boolean { + if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) { + return true; + } + return !!webview.options.enableCommandUris && link.scheme === 'command'; + } + private getWebview(handle: WebviewPanelHandle): WebviewEditorInput { const webview = this._webviews.get(handle); if (!webview) { diff --git a/src/vs/workbench/api/node/apiCommands.ts b/src/vs/workbench/api/node/apiCommands.ts index e579a7e7d6..2ee3ac852d 100644 --- a/src/vs/workbench/api/node/apiCommands.ts +++ b/src/vs/workbench/api/node/apiCommands.ts @@ -11,9 +11,9 @@ import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { EditorGroupLayout } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IWindowsService, IOpenSettings } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IOpenSettings, IURIToOpen } from 'vs/platform/windows/common/windows'; import { IDownloadService } from 'vs/platform/download/common/download'; -import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { IRecent } from 'vs/platform/history/common/history'; // ----------------------------------------------------------------- @@ -51,7 +51,8 @@ export class OpenFolderAPICommand { } const options: IOpenSettings = { forceNewWindow: arg.forceNewWindow, noRecentEntry: arg.noRecentEntry }; uri = URI.revive(uri); - return executor.executeCommand('_files.windowOpen', [{ uri }], options); + const uriToOpen: IURIToOpen = hasWorkspaceFileExtension(uri.path) ? { workspaceUri: uri } : { folderUri: uri }; + return executor.executeCommand('_files.windowOpen', [uriToOpen], options); } } CommandsRegistry.registerCommand({ diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index a2bd8e3e89..9afb52eb93 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -19,7 +19,7 @@ import { score } from 'vs/editor/common/modes/languageSelector'; import * as files from 'vs/platform/files/common/files'; import pkg from 'vs/platform/product/node/package'; import product from 'vs/platform/product/node/product'; -import { ExtHostContext, IInitData, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +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'; @@ -237,6 +237,7 @@ export function createApiFactory( get language() { return platform.language!; }, get appName() { return product.nameLong; }, get appRoot() { return initData.environment.appRoot!.fsPath; }, + get uriScheme() { return product.urlProtocol; }, get logLevel() { checkProposedApiEnabled(extension); return typeConverters.LogLevel.to(extHostLogService.getLevel()); @@ -883,41 +884,105 @@ class Extension implements vscode.Extension { } } -export function initializeExtensionApi(extensionService: ExtHostExtensionService, apiFactory: IExtensionApiFactory, extensionRegistry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): Promise { - return extensionService.getExtensionPathIndex().then(trie => defineAPI(apiFactory, trie, extensionRegistry, configProvider)); +interface INodeModuleFactory { + readonly nodeModuleName: string; + load(request: string, parent: { filename: string; }): any; } -function defineAPI(factory: IExtensionApiFactory, extensionPaths: TernarySearchTree, extensionRegistry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): void { +export class NodeModuleRequireInterceptor { + public static INSTANCE = new NodeModuleRequireInterceptor(); - // each extension is meant to get its own api implementation - const extApiImpl = new Map(); - let defaultApiImpl: typeof vscode; + private readonly _factories: Map; - const node_module = require.__$__nodeRequire('module'); - const original = node_module._load; - node_module._load = function load(request: string, parent: any, isMain: any) { - if (request !== 'vscode') { - return original.apply(this, arguments); - } + constructor() { + this._factories = new Map(); + this._installInterceptor(this._factories); + } + + private _installInterceptor(factories: Map): void { + const node_module = require.__$__nodeRequire('module'); + const original = node_module._load; + node_module._load = function load(request: string, parent: { filename: string; }, isMain: any) { + if (!factories.has(request)) { + return original.apply(this, arguments); + } + return factories.get(request)!.load(request, parent); + }; + } + + public register(interceptor: INodeModuleFactory): void { + this._factories.set(interceptor.nodeModuleName, interceptor); + } +} + +export class VSCodeNodeModuleFactory implements INodeModuleFactory { + public readonly nodeModuleName = 'vscode'; + + private readonly _extApiImpl = new Map(); + private _defaultApiImpl: typeof vscode; + + constructor( + private readonly _apiFactory: IExtensionApiFactory, + private readonly _extensionPaths: TernarySearchTree, + private readonly _extensionRegistry: ExtensionDescriptionRegistry, + private readonly _configProvider: ExtHostConfigProvider + ) { + } + + public load(request: string, parent: { filename: string; }): any { // get extension id from filename and api for extension - const ext = extensionPaths.findSubstr(URI.file(parent.filename).fsPath); + const ext = this._extensionPaths.findSubstr(URI.file(parent.filename).fsPath); if (ext) { - let apiImpl = extApiImpl.get(ExtensionIdentifier.toKey(ext.identifier)); + let apiImpl = this._extApiImpl.get(ExtensionIdentifier.toKey(ext.identifier)); if (!apiImpl) { - apiImpl = factory(ext, extensionRegistry, configProvider); - extApiImpl.set(ExtensionIdentifier.toKey(ext.identifier), apiImpl); + apiImpl = this._apiFactory(ext, this._extensionRegistry, this._configProvider); + this._extApiImpl.set(ExtensionIdentifier.toKey(ext.identifier), apiImpl); } return apiImpl; } // fall back to a default implementation - if (!defaultApiImpl) { + if (!this._defaultApiImpl) { let extensionPathsPretty = ''; - extensionPaths.forEach((value, index) => extensionPathsPretty += `\t${index} -> ${value.identifier.value}\n`); + this._extensionPaths.forEach((value, index) => extensionPathsPretty += `\t${index} -> ${value.identifier.value}\n`); console.warn(`Could not identify extension for 'vscode' require call from ${parent.filename}. These are the extension path mappings: \n${extensionPathsPretty}`); - defaultApiImpl = factory(nullExtensionDescription, extensionRegistry, configProvider); + this._defaultApiImpl = this._apiFactory(nullExtensionDescription, this._extensionRegistry, this._configProvider); } - return defaultApiImpl; - }; + return this._defaultApiImpl; + } +} + +interface IKeytarModule { + getPassword(service: string, account: string): Promise; + setPassword(service: string, account: string, password: string): Promise; + deletePassword(service: string, account: string): Promise; + findPassword(service: string): Promise; +} + +export class KeytarNodeModuleFactory implements INodeModuleFactory { + public readonly nodeModuleName = 'keytar'; + + private _impl: IKeytarModule; + + constructor(mainThreadKeytar: MainThreadKeytarShape) { + this._impl = { + getPassword: (service: string, account: string): Promise => { + return mainThreadKeytar.$getPassword(service, account); + }, + setPassword: (service: string, account: string, password: string): Promise => { + return mainThreadKeytar.$setPassword(service, account, password); + }, + deletePassword: (service: string, account: string): Promise => { + return mainThreadKeytar.$deletePassword(service, account); + }, + findPassword: (service: string): Promise => { + return mainThreadKeytar.$findPassword(service); + } + }; + } + + public load(request: string, parent: { filename: string; }): any { + return this._impl; + } } diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index 226399e169..ec00bbd207 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -7,7 +7,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 { IURIToOpen, URIType, IOpenSettings } from 'vs/platform/windows/common/windows'; +import { IURIToOpen, IOpenSettings } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; @@ -55,17 +55,6 @@ export class CLIServer { return this._ipcHandlePath; } - private collectURIToOpen(strs: string[] | undefined, typeHint: URIType, result: IURIToOpen[]): void { - if (Array.isArray(strs)) { - for (const s of strs) { - try { - result.push({ uri: URI.parse(s), typeHint }); - } catch (e) { - // ignore - } - } - } - } private onRequest(req: http.IncomingMessage, res: http.ServerResponse): void { const chunks: string[] = []; @@ -95,13 +84,32 @@ export class CLIServer { private open(data: OpenCommandPipeArgs, res: http.ServerResponse) { let { fileURIs, folderURIs, forceNewWindow, diffMode, addMode, forceReuseWindow, waitMarkerFilePath } = data; - if (folderURIs && folderURIs.length || fileURIs && fileURIs.length) { - const urisToOpen: IURIToOpen[] = []; - this.collectURIToOpen(folderURIs, 'folder', urisToOpen); - this.collectURIToOpen(fileURIs, 'file', urisToOpen); - if (!forceReuseWindow && urisToOpen.some(o => o.typeHint === 'folder' || (o.typeHint === 'file' && hasWorkspaceFileExtension(o.uri.path)))) { - forceNewWindow = true; + const urisToOpen: IURIToOpen[] = []; + if (Array.isArray(folderURIs)) { + for (const s of folderURIs) { + try { + urisToOpen.push({ folderUri: URI.parse(s) }); + forceNewWindow = true; + } catch (e) { + // ignore + } } + } + if (Array.isArray(fileURIs)) { + for (const s of fileURIs) { + try { + if (hasWorkspaceFileExtension(s)) { + urisToOpen.push({ workspaceUri: URI.parse(s) }); + forceNewWindow = true; + } else { + urisToOpen.push({ fileUri: URI.parse(s) }); + } + } catch (e) { + // ignore + } + } + } + if (urisToOpen.length) { const waitMarkerFileURI = waitMarkerFilePath ? URI.file(waitMarkerFilePath) : undefined; const windowOpenArgs: IOpenSettings = { forceNewWindow, diffMode, addMode, forceReuseWindow, waitMarkerFileURI }; this._commands.executeCommand('_files.windowOpen', urisToOpen, windowOpenArgs); diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index 36aee64975..9ffa42fbeb 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -16,6 +16,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; +import { debounce } from 'vs/base/common/decorators'; interface HandlerData { @@ -360,10 +361,13 @@ export class ExtHostCommentThread implements vscode.CommentThread { return this._resource; } + private _onDidUpdateCommentThread = new Emitter(); + readonly onDidUpdateCommentThread = this._onDidUpdateCommentThread.event; + set range(range: vscode.Range) { if (range.isEqual(this._range)) { this._range = range; - this._proxy.$updateCommentThreadRange(this._commentController.handle, this.handle, extHostTypeConverter.Range.from(this._range)); + this._onDidUpdateCommentThread.fire(); } } @@ -379,7 +383,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { set label(label: string) { this._label = label; - this._proxy.$updateCommentThreadLabel(this._commentController.handle, this.handle, this._label); + this._onDidUpdateCommentThread.fire(); } get comments(): vscode.Comment[] { @@ -387,8 +391,8 @@ export class ExtHostCommentThread implements vscode.CommentThread { } set comments(newComments: vscode.Comment[]) { - this._proxy.$updateComments(this._commentController.handle, this.handle, newComments.map(cmt => { return convertToModeComment(this._commentController, cmt, this._commandsConverter); })); this._comments = newComments; + this._onDidUpdateCommentThread.fire(); } private _acceptInputCommand: vscode.Command; @@ -398,9 +402,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { set acceptInputCommand(acceptInputCommand: vscode.Command) { this._acceptInputCommand = acceptInputCommand; - - const internal = this._commandsConverter.toInternal(acceptInputCommand); - this._proxy.$updateCommentThreadAcceptInputCommand(this._commentController.handle, this.handle, internal); + this._onDidUpdateCommentThread.fire(); } private _additionalCommands: vscode.Command[] = []; @@ -410,9 +412,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { set additionalCommands(additionalCommands: vscode.Command[]) { this._additionalCommands = additionalCommands; - - const internals = additionalCommands.map(x => this._commandsConverter.toInternal(x)); - this._proxy.$updateCommentThreadAdditionalCommands(this._commentController.handle, this.handle, internals); + this._onDidUpdateCommentThread.fire(); } private _deleteCommand?: vscode.Command; @@ -422,9 +422,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { set deleteCommand(deleteCommand: vscode.Command) { this._deleteCommand = deleteCommand; - - const internal = this._commandsConverter.toInternal(deleteCommand); - this._proxy.$updateCommentThreadDeleteCommand(this._commentController.handle, this.handle, internal); + this._onDidUpdateCommentThread.fire(); } private _collapseState?: vscode.CommentThreadCollapsibleState; @@ -435,9 +433,11 @@ export class ExtHostCommentThread implements vscode.CommentThread { set collapsibleState(newState: vscode.CommentThreadCollapsibleState) { this._collapseState = newState; - this._proxy.$updateCommentThreadCollapsibleState(this._commentController.handle, this.handle, convertToCollapsibleState(newState)); + this._onDidUpdateCommentThread.fire(); } + private _localDisposables: types.Disposable[]; + constructor( private _proxy: MainThreadCommentsShape, private readonly _commandsConverter: CommandsConverter, @@ -452,12 +452,41 @@ export class ExtHostCommentThread implements vscode.CommentThread { this.handle, this._threadId, this._resource, - extHostTypeConverter.Range.from(this._range), - this._comments.map(comment => { return convertToModeComment(this._commentController, comment, this._commandsConverter); }), - this._acceptInputCommand ? this._commandsConverter.toInternal(this._acceptInputCommand) : undefined, - this._additionalCommands ? this._additionalCommands.map(x => this._commandsConverter.toInternal(x)) : [], - this._deleteCommand ? this._commandsConverter.toInternal(this._deleteCommand) : undefined, - this._collapseState! + extHostTypeConverter.Range.from(this._range) + ); + + this._localDisposables = []; + + this._localDisposables.push(this.onDidUpdateCommentThread(() => { + this.eventuallyUpdateCommentThread(); + })); + + // set up comments after ctor to batch update events. + this.comments = _comments; + } + + @debounce(100) + eventuallyUpdateCommentThread(): void { + const commentThreadRange = extHostTypeConverter.Range.from(this._range); + const label = this.label; + const comments = this._comments.map(cmt => { return convertToModeComment(this._commentController, cmt, this._commandsConverter); }); + const acceptInputCommand = this._acceptInputCommand ? this._commandsConverter.toInternal(this._acceptInputCommand) : undefined; + const additionalCommands = this._additionalCommands ? this._additionalCommands.map(x => this._commandsConverter.toInternal(x)) : []; + const deleteCommand = this._deleteCommand ? this._commandsConverter.toInternal(this._deleteCommand) : undefined; + const collapsibleState = convertToCollapsibleState(this._collapseState); + + this._proxy.$updateCommentThread( + this._commentController.handle, + this.handle, + this._threadId, + this._resource, + commentThreadRange, + label, + comments, + acceptInputCommand, + additionalCommands, + deleteCommand, + collapsibleState ); } @@ -472,6 +501,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { } dispose() { + this._localDisposables.forEach(disposable => disposable.dispose()); this._proxy.$deleteCommentThread( this._commentController.handle, this.handle @@ -607,7 +637,7 @@ function convertFromCommentThread(commentThread: modes.CommentThread): vscode.Co threadId: commentThread.threadId!, resource: URI.parse(commentThread.resource!), range: extHostTypeConverter.Range.to(commentThread.range), - comments: commentThread.comments.map(convertFromComment), + comments: commentThread.comments ? commentThread.comments.map(convertFromComment) : [], collapsibleState: commentThread.collapsibleState }; } diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 1f1c906ed5..a65e2d2652 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -15,6 +15,8 @@ import { URI } from 'vs/base/common/uri'; 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 { 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'; @@ -32,6 +34,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio 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'; class ExtensionMemento implements IExtensionMemento { @@ -243,7 +246,13 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { private async _initialize(): Promise { try { const configProvider = await this._extHostConfiguration.getConfigProvider(); + + // {{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))); + // Do this when extension service exists, but extensions are not being activated yet. await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._extHostLogService, this._mainThreadTelemetryProxy); this._almostReadyToRunExtensions.open(); @@ -318,7 +327,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { if (!ext.main) { return undefined; } - return pfs.realpath(ext.extensionLocation.fsPath).then(value => tree.set(URI.file(value).fsPath, ext)); + return realpath(ext.extensionLocation.fsPath).then(value => tree.set(URI.file(value).fsPath, ext)); }); this._extensionPathIndex = Promise.all(extensions).then(() => tree); } @@ -735,13 +744,13 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { if (!extensionDescription) { return; } - const realpath = await pfs.realpath(extensionDescription.extensionLocation.fsPath); - trie.delete(URI.file(realpath).fsPath); + const realpathValue = await realpath(extensionDescription.extensionLocation.fsPath); + trie.delete(URI.file(realpathValue).fsPath); })); await Promise.all(toAdd.map(async (extensionDescription) => { - const realpath = await pfs.realpath(extensionDescription.extensionLocation.fsPath); - trie.set(URI.file(realpath).fsPath, extensionDescription); + const realpathValue = await realpath(extensionDescription.extensionLocation.fsPath); + trie.set(URI.file(realpathValue).fsPath, extensionDescription); })); this._registry.deltaExtensions(toAdd, toRemove); diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 5c968cc560..b227d757b9 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -15,7 +15,7 @@ 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 { asPromise } from 'vs/base/common/async'; -import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, SuggestionDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, MainThreadWebviewsShape, CodeInsetDto } 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 } from '../common/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'; @@ -129,7 +129,7 @@ class CodeLensAdapter { }); } - resolveCodeLens(resource: URI, symbol: CodeLensDto, token: CancellationToken): Promise { + resolveCodeLens(symbol: CodeLensDto, token: CancellationToken): Promise { const lens = this._heapService.get(ObjectIdentifier.of(symbol)); if (!lens) { @@ -637,15 +637,18 @@ class SuggestAdapter { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); - return asPromise( - () => this._provider.provideCompletionItems(doc, pos, token, typeConvert.CompletionContext.to(context)) - ).then(value => { + return asPromise(() => this._provider.provideCompletionItems(doc, pos, token, typeConvert.CompletionContext.to(context))).then(value => { const _id = this._idPool++; + // the default text edit range + const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)) + .with({ end: pos }); + const result: SuggestResultDto = { - _id, - suggestions: [], + x: _id, + b: [], + a: typeConvert.Range.from(wordRangeBeforePos), }; let list: CompletionList; @@ -658,54 +661,45 @@ class SuggestAdapter { } else { list = value; - result.incomplete = list.isIncomplete; + result.c = list.isIncomplete; } - // the default text edit range - const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)) - .with({ end: pos }); - for (let i = 0; i < list.items.length; i++) { - const suggestion = this._convertCompletionItem(list.items[i], pos, wordRangeBeforePos, i, _id); + const suggestion = this._convertCompletionItem2(list.items[i], pos, i, _id); // check for bad completion item // for the converter did warn if (suggestion) { - result.suggestions.push(suggestion); + result.b.push(suggestion); } } - this._cache.set(_id, list.items); + + if (SuggestAdapter.supportsResolving(this._provider)) { + this._cache.set(_id, list.items); + } return result; }); } - resolveCompletionItem(resource: URI, position: IPosition, suggestion: modes.CompletionItem, token: CancellationToken): Promise { + resolveCompletionItem(_resource: URI, position: IPosition, id: number, pid: number, token: CancellationToken): Promise { if (typeof this._provider.resolveCompletionItem !== 'function') { - return Promise.resolve(suggestion); + return Promise.resolve(undefined); } - const { _parentId, _id } = (suggestion); - const item = this._cache.has(_parentId) ? this._cache.get(_parentId)![_id] : undefined; + const item = this._cache.has(pid) ? this._cache.get(pid)![id] : undefined; if (!item) { - return Promise.resolve(suggestion); + return Promise.resolve(undefined); } return asPromise(() => this._provider.resolveCompletionItem!(item, token)).then(resolvedItem => { if (!resolvedItem) { - return suggestion; + return undefined; } - const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); - const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos)).with({ end: pos }); - const newSuggestion = this._convertCompletionItem(resolvedItem, pos, wordRangeBeforePos, _id, _parentId); - if (newSuggestion) { - mixin(suggestion, newSuggestion, true); - } - - return suggestion; + return this._convertCompletionItem2(resolvedItem, pos, id, pid); }); } @@ -713,60 +707,52 @@ class SuggestAdapter { this._cache.delete(id); } - private _convertCompletionItem(item: vscode.CompletionItem, position: vscode.Position, defaultRange: vscode.Range, _id: number, _parentId: number): SuggestionDto | undefined { + private _convertCompletionItem2(item: vscode.CompletionItem, position: vscode.Position, id: number, pid: number): SuggestDataDto | undefined { if (typeof item.label !== 'string' || item.label.length === 0) { console.warn('INVALID text edit -> must have at least a label'); return undefined; } - const result: SuggestionDto = { + const result: SuggestDataDto = { // - _id, - _parentId, + x: id, + y: pid, // - label: item.label, - kind: typeConvert.CompletionItemKind.from(item.kind), - detail: item.detail, - documentation: typeof item.documentation === 'undefined' ? undefined : typeConvert.MarkdownString.fromStrict(item.documentation), - filterText: item.filterText, - sortText: item.sortText, - preselect: item.preselect, - // - range: undefined!, // populated below - insertText: undefined!, // populated below - insertTextRules: item.keepWhitespace ? modes.CompletionItemInsertTextRule.KeepWhitespace : 0, - additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(typeConvert.TextEdit.from), - command: this._commands.toInternal(item.command), - commitCharacters: item.commitCharacters + a: item.label, + b: typeConvert.CompletionItemKind.from(item.kind), + c: item.detail, + d: typeof item.documentation === 'undefined' ? undefined : typeConvert.MarkdownString.fromStrict(item.documentation), + e: item.sortText, + f: item.filterText, + g: item.preselect, + i: item.keepWhitespace ? modes.CompletionItemInsertTextRule.KeepWhitespace : 0, + k: item.commitCharacters, + l: item.additionalTextEdits && item.additionalTextEdits.map(typeConvert.TextEdit.from), + m: this._commands.toInternal(item.command), }; // 'insertText'-logic if (item.textEdit) { - result.insertText = item.textEdit.newText; + result.h = item.textEdit.newText; } else if (typeof item.insertText === 'string') { - result.insertText = item.insertText; + result.h = item.insertText; } else if (item.insertText instanceof SnippetString) { - result.insertText = item.insertText.value; - result.insertTextRules! |= modes.CompletionItemInsertTextRule.InsertAsSnippet; - - } else { - result.insertText = item.label; + result.h = item.insertText.value; + result.i! |= modes.CompletionItemInsertTextRule.InsertAsSnippet; } // 'overwrite[Before|After]'-logic - let range: vscode.Range; + let range: vscode.Range | undefined; if (item.textEdit) { range = item.textEdit.range; } else if (item.range) { range = item.range; - } else { - range = defaultRange; } - result.range = typeConvert.Range.from(range); + result.j = typeConvert.Range.from(range); - if (!range.isSingleLine || range.start.line !== position.line) { + if (range && (!range.isSingleLine || range.start.line !== position.line)) { console.warn('INVALID text edit -> must be single line and on the same line'); return undefined; } @@ -1181,8 +1167,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource), token), []); } - $resolveCodeLens(handle: number, resource: UriComponents, symbol: modes.ICodeLensSymbol, token: CancellationToken): Promise { - return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(URI.revive(resource), symbol, token), undefined); + $resolveCodeLens(handle: number, symbol: modes.ICodeLensSymbol, token: CancellationToken): Promise { + return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(symbol, token), undefined); } // --- code insets @@ -1387,8 +1373,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined); } - $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.CompletionItem, token: CancellationToken): Promise { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(URI.revive(resource), position, suggestion, token), suggestion); + $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, id: number, pid: number, token: CancellationToken): Promise { + return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(URI.revive(resource), position, id, pid, token), undefined); } $releaseCompletionItems(handle: number, id: number): void { diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 3d62d62d1b..ada8b93e4d 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -163,7 +163,7 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { 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(() => outputDirPath); + this._outputDir = dirExists(outputDirPath).then(exists => exists ? exists : mkdirp(outputDirPath).then(() => true)).then(() => outputDirPath); this._proxy = mainContext.getProxy(MainContext.MainThreadOutputService); } diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 76cd1d4c5d..223d432056 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -6,7 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import * as extfs from 'vs/base/node/extfs'; +import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; import { IFileQuery, IFolderQuery, IRawFileQuery, IRawQuery, IRawTextQuery, ISearchCompleteStats, ITextQuery, isSerializedFileMatch, ISerializedSearchProgressItem } from 'vs/workbench/services/search/common/search'; import { FileSearchManager } from 'vs/workbench/services/search/node/fileSearchManager'; @@ -35,7 +35,7 @@ export class ExtHostSearch implements ExtHostSearchShape { private _fileSearchManager: FileSearchManager; - constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer | null, private _logService: ILogService, private _extfs = extfs) { + constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer | null, private _logService: ILogService, private _pfs = pfs) { this._proxy = mainContext.getProxy(MainContext.MainThreadSearch); this._fileSearchManager = new FileSearchManager(); } @@ -146,7 +146,7 @@ export class ExtHostSearch implements ExtHostSearchShape { } const query = reviveQuery(rawQuery); - const engine = new TextSearchManager(query, provider, this._extfs); + const engine = new TextSearchManager(query, provider, this._pfs); return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token); } } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index b502bd07a6..68df6f1151 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as crypto from 'crypto'; import { coalesce, equals } from 'vs/base/common/arrays'; import { illegalArgument } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; @@ -1545,6 +1544,14 @@ export class TaskGroup implements vscode.TaskGroup { } } +function computeTaskExecutionId(values: string[]): string { + let id: string = ''; + for (let i = 0; i < values.length; i++) { + id += values[i].replace(/,/g, ',,') + ','; + } + return id; +} + @es5ClassCompat export class ProcessExecution implements vscode.ProcessExecution { @@ -1604,17 +1611,17 @@ export class ProcessExecution implements vscode.ProcessExecution { } public computeId(): string { - const hash = crypto.createHash('md5'); - hash.update('process'); + const props: string[] = []; + props.push('process'); if (this._process !== undefined) { - hash.update(this._process); + props.push(this._process); } if (this._args && this._args.length > 0) { for (let arg of this._args) { - hash.update(arg); + props.push(arg); } } - return hash.digest('hex'); + return computeTaskExecutionId(props); } } @@ -1687,20 +1694,20 @@ export class ShellExecution implements vscode.ShellExecution { } public computeId(): string { - const hash = crypto.createHash('md5'); - hash.update('shell'); + const props: string[] = []; + props.push('shell'); if (this._commandLine !== undefined) { - hash.update(this._commandLine); + props.push(this._commandLine); } if (this._command !== undefined) { - hash.update(typeof this._command === 'string' ? this._command : this._command.value); + props.push(typeof this._command === 'string' ? this._command : this._command.value); } if (this._args && this._args.length > 0) { for (let arg of this._args) { - hash.update(typeof arg === 'string' ? arg : arg.value); + props.push(typeof arg === 'string' ? arg : arg.value); } } - return hash.digest('hex'); + return computeTaskExecutionId(props); } } @@ -1723,10 +1730,7 @@ export class CustomExecution implements vscode.CustomExecution { } public computeId(): string { - const hash = crypto.createHash('md5'); - hash.update('customExecution'); - hash.update(generateUuid()); - return hash.digest('hex'); + return 'customExecution' + generateUuid(); } public set callback(value: (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable) { diff --git a/src/vs/workbench/browser/actions.ts b/src/vs/workbench/browser/actions.ts index c8600c33b4..31bbb9bb53 100644 --- a/src/vs/workbench/browser/actions.ts +++ b/src/vs/workbench/browser/actions.ts @@ -17,14 +17,14 @@ export class ActionBarContributor { /** * Returns true if this contributor has actions for the given context. */ - hasActions(context: any): boolean { + hasActions(context: unknown): boolean { return false; } /** * Returns an array of primary actions in the given context. */ - getActions(context: any): IAction[] { + getActions(context: unknown): IAction[] { return []; } } @@ -46,14 +46,14 @@ export const Scope = { export class ContributableActionProvider implements IActionProvider { private readonly registry: IActionBarRegistry = Registry.as(Extensions.Actionbar); - private toContext(tree: ITree, element: any): any { + private toContext(tree: ITree, element: unknown): unknown { return { viewer: tree, element: element }; } - hasActions(tree: ITree, element: any): boolean { + hasActions(tree: ITree, element: unknown): boolean { const context = this.toContext(tree, element); const contributors = this.registry.getActionBarContributors(Scope.VIEWER); @@ -66,7 +66,7 @@ export class ContributableActionProvider implements IActionProvider { return false; } - getActions(tree: ITree, element: any): IAction[] { + getActions(tree: ITree, element: unknown): IAction[] { const actions: IAction[] = []; const context = this.toContext(tree, element); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index bdc2cc7011..af67436be7 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -233,12 +233,10 @@ export class ToggleEditorVisibilityAction extends Action { return Promise.resolve(); } - } registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleEditorVisibilityAction, ToggleEditorVisibilityAction.ID, ToggleEditorVisibilityAction.LABEL), 'View: Toggle Editor Area Visibility', viewCategory, ContextKeyExpr.equals('config.workbench.useExperimentalGridLayout', true)); - export class ToggleSidebarVisibilityAction extends Action { static readonly ID = 'workbench.action.toggleSidebarVisibility'; diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 3dd8206274..fb365946a1 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -80,7 +80,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: (accessor, arg2) => focusDown(accessor, arg2) }); -function expandMultiSelection(focused: List | PagedList | ITree | ObjectTree | DataTree | AsyncDataTree, previousFocus: any): void { +function expandMultiSelection(focused: List | PagedList | ITree | ObjectTree | DataTree | AsyncDataTree, previousFocus: unknown): void { // List if (focused instanceof List || focused instanceof PagedList) { @@ -625,7 +625,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const selection = tree.getSelection(); // Which element should be considered to start selecting all? - let start: any | undefined = undefined; + let start: unknown | undefined = undefined; if (focus.length > 0 && (selection.length === 0 || selection.indexOf(focus[0]) === -1)) { start = focus[0]; @@ -636,7 +636,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } // What is the scope of select all? - let scope: any | undefined = undefined; + let scope: unknown | undefined = undefined; if (!start) { scope = undefined; @@ -651,8 +651,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } } - const newSelection: any[] = []; - const visit = (node: ITreeNode) => { + const newSelection: unknown[] = []; + const visit = (node: ITreeNode) => { for (const child of node.children) { if (child.visible) { newSelection.push(child.element); diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 2f14866c85..8e204d0d3b 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -300,7 +300,7 @@ export class DuplicateWorkspaceInNewWindowAction extends Action { return this.workspacesService.createUntitledWorkspace(folders, remoteAuthority).then(newWorkspace => { return this.workspaceEditingService.copyWorkspaceSettings(newWorkspace).then(() => { - return this.windowService.openWindow([{ uri: newWorkspace.configPath, typeHint: 'file' }], { forceNewWindow: true }); + return this.windowService.openWindow([{ workspaceUri: newWorkspace.configPath }], { forceNewWindow: true }); }); }); } diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index 99bfb69c65..c06620ae81 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -89,13 +89,13 @@ CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (acc return undefined; } - const folderPicks = folders.map(folder => { + const folderPicks: IQuickPickItem[] = folders.map(folder => { return { label: folder.name, description: labelService.getUriLabel(resources.dirname(folder.uri), { relative: true }), folder, iconClasses: getIconClasses(modelService, modeService, folder.uri, FileKind.ROOT_FOLDER) - } as IQuickPickItem; + }; }); const options: IPickOptions = (args ? args[0] : undefined) || Object.create(null); diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 58a1a0e9d1..9c392ad5be 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -176,7 +176,7 @@ export abstract class Composite extends Component implements IComposite { /** * Provide a context to be passed to the toolbar. */ - getActionsContext(): any { + getActionsContext(): unknown { return null; } @@ -210,10 +210,10 @@ export abstract class Composite extends Component implements IComposite { } /** - * Returns the underlying composite control or null if it is not accessible. + * Returns the underlying composite control or `undefined` if it is not accessible. */ - getControl(): ICompositeControl | null { - return null; + getControl(): ICompositeControl | undefined { + return undefined; } } @@ -257,7 +257,7 @@ export abstract class CompositeRegistry extends Disposable protected deregisterComposite(id: string): void { const descriptor = this.compositeById(id); - if (descriptor === null) { + if (!descriptor) { return; } diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 3cc426f2a8..e80fbe7919 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -7,7 +7,7 @@ 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 { IWindowConfiguration, IWindowService } from 'vs/platform/windows/common/windows'; +import { IWindowService, 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 { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom'; @@ -16,7 +16,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { SidebarVisibleContext, SideBarVisibleContext } from 'vs/workbench/common/viewlet'; +import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -38,8 +38,6 @@ export class WorkbenchContextKeysHandler extends Disposable { private inZenModeContext: IContextKey; private sideBarVisibleContext: IContextKey; - //TODO@Isidor remove in May - private sidebarVisibleContext: IContextKey; constructor( @IContextKeyService private contextKeyService: IContextKeyService, @@ -93,7 +91,7 @@ export class WorkbenchContextKeysHandler extends Disposable { IsRemoteContext.bindTo(this.contextKeyService).set(!!this.windowService.getConfiguration().remoteAuthority); // macOS Native Tabs - const windowConfig = this.configurationService.getValue(); + const windowConfig = this.configurationService.getValue(); HasMacNativeTabsContext.bindTo(this.contextKeyService).set(windowConfig && windowConfig.window && windowConfig.window.nativeTabs); // Development @@ -131,7 +129,6 @@ export class WorkbenchContextKeysHandler extends Disposable { // Sidebar this.sideBarVisibleContext = SideBarVisibleContext.bindTo(this.contextKeyService); - this.sidebarVisibleContext = SidebarVisibleContext.bindTo(this.contextKeyService); } private updateEditorContextKeys(): void { @@ -208,6 +205,5 @@ export class WorkbenchContextKeysHandler extends Disposable { private updateSideBarContextKeys(): void { this.sideBarVisibleContext.set(this.layoutService.isVisible(Parts.SIDEBAR_PART)); - this.sidebarVisibleContext.set(this.layoutService.isVisible(Parts.SIDEBAR_PART)); } } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 5fa0f3592c..d586c10196 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; +import { hasWorkspaceFileExtension, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { normalize } from 'vs/base/common/path'; import { basename } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; @@ -79,7 +79,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array { resources.push({ resource: URI.parse(draggedEditor.resource), backupResource: draggedEditor.backupResource ? URI.parse(draggedEditor.backupResource) : undefined, viewState: draggedEditor.viewState, isExternal: false }); }); @@ -105,7 +105,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array r.resource.fsPath === file.path) /* prevent duplicates */) { try { resources.push({ resource: URI.file(file.path), isExternal: true }); @@ -120,7 +120,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array { if (!resources.some(r => r.resource.fsPath === codeFile) /* prevent duplicates */) { resources.push({ resource: URI.file(codeFile), isExternal: true }); @@ -254,16 +254,14 @@ export class ResourcesDropHandler { } private handleWorkspaceFileDrop(fileOnDiskResources: URI[]): Promise { - const workspaceResources: { workspaces: IURIToOpen[], folders: IURIToOpen[] } = { - workspaces: [], - folders: [] - }; + const urisToOpen: IURIToOpen[] = []; + const folderURIs: IWorkspaceFolderCreationData[] = []; return Promise.all(fileOnDiskResources.map(fileOnDiskResource => { // Check for Workspace if (hasWorkspaceFileExtension(fileOnDiskResource.fsPath)) { - workspaceResources.workspaces.push({ uri: fileOnDiskResource, typeHint: 'file' }); + urisToOpen.push({ workspaceUri: fileOnDiskResource }); return undefined; } @@ -271,14 +269,14 @@ export class ResourcesDropHandler { // Check for Folder return this.fileService.resolve(fileOnDiskResource).then(stat => { if (stat.isDirectory) { - workspaceResources.folders.push({ uri: stat.resource, typeHint: 'folder' }); + urisToOpen.push({ folderUri: stat.resource }); + folderURIs.push({ uri: stat.resource }); } }, error => undefined); })).then(_ => { - const { workspaces, folders } = workspaceResources; // Return early if no external resource is a folder or workspace - if (workspaces.length === 0 && folders.length === 0) { + if (urisToOpen.length === 0) { return false; } @@ -286,12 +284,12 @@ export class ResourcesDropHandler { this.windowService.focusWindow(); // Open in separate windows if we drop workspaces or just one folder - if (workspaces.length > 0 || folders.length === 1) { - return this.windowService.openWindow([...workspaces, ...folders], { forceReuseWindow: true }).then(_ => true); + if (urisToOpen.length > folderURIs.length || folderURIs.length === 1) { + return this.windowService.openWindow(urisToOpen, { forceReuseWindow: true }).then(_ => true); } // folders.length > 1: Multiple folders: Create new workspace with folders and open - return this.workspaceEditingService.createAndEnterWorkspace(folders).then(_ => true); + return this.workspaceEditingService.createAndEnterWorkspace(folderURIs).then(_ => true); }); } } diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 7036d2bb67..ef125258f3 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -16,7 +16,7 @@ export interface IEditorDescriptor { getId(): string; getName(): string; - describes(obj: any): boolean; + describes(obj: unknown): boolean; } export interface IEditorRegistry { @@ -76,7 +76,7 @@ export class EditorDescriptor implements IEditorDescriptor { return this.name; } - describes(obj: any): boolean { + describes(obj: unknown): boolean { return obj instanceof BaseEditor && (obj).getId() === this.id; } } @@ -88,7 +88,7 @@ class EditorRegistry implements IEditorRegistry { registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor): void; registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor[]): void; - registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: any): void { + registerEditor(descriptor: EditorDescriptor, editorInputDescriptor: SyncDescriptor | SyncDescriptor[]): void { // Support both non-array and array parameter let inputDescriptors: SyncDescriptor[] = []; diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 50745fd303..2893ef03bc 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI as uri } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { IconLabel, IIconLabelValueOptions, IIconLabelCreationOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -27,7 +27,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { withNullAsUndefined } from 'vs/base/common/types'; export interface IResourceLabelProps { - resource?: uri; + resource?: URI; name?: string; description?: string; } @@ -61,7 +61,7 @@ export interface IResourceLabel extends IDisposable { /** * Convinient method to render a file label based on a resource. */ - setFile(resource: uri, options?: IFileLabelOptions): void; + setFile(resource: URI, options?: IFileLabelOptions): void; /** * Convinient method to apply a label by passing an editor along. @@ -151,7 +151,7 @@ export class ResourceLabels extends Disposable { setLabel: (label?: string, description?: string, options?: IIconLabelValueOptions) => widget.setLabel(label, description, options), setResource: (label: IResourceLabelProps, options?: IResourceLabelOptions) => widget.setResource(label, options), setEditor: (editor: IEditorInput, options?: IResourceLabelOptions) => widget.setEditor(editor, options), - setFile: (resource: uri, options?: IFileLabelOptions) => widget.setFile(resource, options), + setFile: (resource: URI, options?: IFileLabelOptions) => widget.setFile(resource, options), clear: () => widget.clear(), dispose: () => this.disposeWidget(widget) }; @@ -338,7 +338,7 @@ class ResourceLabelWidget extends IconLabel { }, options); } - setFile(resource: uri, options?: IFileLabelOptions): void { + setFile(resource: URI, options?: IFileLabelOptions): void { const hideLabel = options && options.hideLabel; let name: string | undefined; if (!hideLabel) { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index a5cfa4777f..25cdc8edad 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -457,9 +457,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi let input: IResourceInput | IUntitledResourceInput; if (isNew) { - input = { filePath: resource.fsPath, options: { pinned: true } } as IUntitledResourceInput; + input = { filePath: resource.fsPath, options: { pinned: true } }; } else { - input = { resource, options: { pinned: true }, forceFile: true } as IResourceInput; + input = { resource, options: { pinned: true }, forceFile: true }; } if (!isNew && typeof p.lineNumber === 'number') { diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts index 97e24e8a6a..3c87c60a4f 100644 --- a/src/vs/workbench/browser/nodeless.simpleservices.ts +++ b/src/vs/workbench/browser/nodeless.simpleservices.ts @@ -669,6 +669,8 @@ export class SimpleRemoteAuthorityResolverService implements IRemoteAuthorityRes return Promise.resolve(undefined); } + clearResolvedAuthority(authority: string): void { } + setResolvedAuthority(resolvedAuthority: ResolvedAuthority): void { } setResolvedAuthorityError(authority: string, err: any): void { } @@ -694,6 +696,7 @@ export class SimpleRemoteFileService implements IFileService { readonly onAfterOperation = Event.None; readonly onDidChangeFileSystemProviderRegistrations = Event.None; readonly onWillActivateFileSystemProvider = Event.None; + readonly onError = Event.None; resolve(resource: URI, options?: IResolveFileOptions): Promise { // @ts-ignore @@ -798,13 +801,11 @@ export class SimpleRemoteFileService implements IFileService { canHandleResource(resource: URI): boolean { return resource.scheme === 'file'; } - hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise { return Promise.resolve(false); } + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean { return false; } del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean }): Promise { return Promise.resolve(); } - watch(_resource: URI): void { } - - unwatch(_resource: URI): void { } + watch(_resource: URI): IDisposable { return Disposable.None; } getWriteEncoding(_resource: URI): IResourceEncoding { return { encoding: 'utf8', hasBOM: false }; } diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index 601bbb4f35..f4706368b2 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -45,7 +45,7 @@ export class PanelRegistry extends CompositeRegistry { * Returns an array of registered panels known to the platform. */ getPanels(): PanelDescriptor[] { - return this.getComposites() as PanelDescriptor[]; + return this.getComposites(); } /** diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 7e9c88c371..5e6a95a025 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -14,7 +14,7 @@ import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { prepareActions } from 'vs/workbench/browser/actions'; -import { Action, IAction } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import { Part, IPartOptions } from 'vs/workbench/browser/part'; import { Composite, CompositeRegistry } from 'vs/workbench/browser/composite'; import { IComposite } from 'vs/workbench/common/composite'; @@ -399,7 +399,7 @@ export abstract class CompositePart extends Part { // Toolbar this.toolBar = this._register(new ToolBar(titleActionsContainer, this.contextMenuService, { - actionItemProvider: action => this.actionItemProvider(action as Action), + actionItemProvider: action => this.actionItemProvider(action), orientation: ActionsOrientation.HORIZONTAL, getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment() @@ -432,7 +432,7 @@ export abstract class CompositePart extends Part { this.titleLabel.updateStyles(); } - protected actionItemProvider(action: Action): IActionItem | undefined { + protected actionItemProvider(action: IAction): IActionItem | undefined { // Check Active Composite if (this.activeComposite) { @@ -442,7 +442,7 @@ export abstract class CompositePart extends Part { return undefined; } - protected actionsContextProvider(): any { + protected actionsContextProvider(): unknown { // Check Active Composite if (this.activeComposite) { diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index b41f5c3601..582e82cd22 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -248,7 +248,7 @@ export class EditorMemento implements IEditorMemento { } } - private doGetResource(resourceOrEditor: URI | EditorInput): URI | null { + private doGetResource(resourceOrEditor: URI | EditorInput): URI | undefined { if (resourceOrEditor instanceof EditorInput) { return resourceOrEditor.getResource(); } diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 553eff7941..d566ffac1f 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -40,7 +40,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { private metadata: string | undefined; private binaryContainer: HTMLElement; private scrollbar: DomScrollableElement; - private resourceViewerContext: ResourceViewerContext; + private resourceViewerContext: ResourceViewerContext | undefined; constructor( id: string, @@ -127,7 +127,8 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { // Clear Resource Viewer clearNode(this.binaryContainer); - this.resourceViewerContext = dispose(this.resourceViewerContext); + dispose(this.resourceViewerContext); + this.resourceViewerContext = undefined; super.clearInput(); } @@ -149,7 +150,8 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { dispose(): void { this.binaryContainer.remove(); - this.resourceViewerContext = dispose(this.resourceViewerContext); + dispose(this.resourceViewerContext); + this.resourceViewerContext = undefined; super.dispose(); } diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index b7d328935e..09ea926a4f 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -30,7 +30,7 @@ export class ExecuteCommandAction extends Action { label: string, private commandId: string, private commandService: ICommandService, - private commandArgs?: any + private commandArgs?: unknown ) { super(id, label); } @@ -429,14 +429,16 @@ export class OpenToSideFromQuickOpenAction extends Action { const entry = toEditorQuickOpenEntry(context); if (entry) { const input = entry.getInput(); - if (input instanceof EditorInput) { - return this.editorService.openEditor(input, entry.getOptions() || undefined, SIDE_GROUP); + if (input) { + if (input instanceof EditorInput) { + return this.editorService.openEditor(input, entry.getOptions() || undefined, SIDE_GROUP); + } + + const resourceInput = input as IResourceInput; + resourceInput.options = mixin(resourceInput.options, entry.getOptions()); + + return this.editorService.openEditor(resourceInput, SIDE_GROUP); } - - const resourceInput = input as IResourceInput; - resourceInput.options = mixin(resourceInput.options, entry.getOptions()); - - return this.editorService.openEditor(resourceInput, SIDE_GROUP); } return Promise.resolve(false); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index a7167bb7bb..27c1d6044a 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -762,13 +762,13 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon return !!editorContext ? [editorContext] : []; } -function isEditorGroup(thing: any): thing is IEditorGroup { +function isEditorGroup(thing: unknown): thing is IEditorGroup { const group = thing as IEditorGroup; return group && typeof group.id === 'number' && Array.isArray(group.editors); } -function isEditorIdentifier(thing: any): thing is IEditorIdentifier { +function isEditorIdentifier(thing: unknown): thing is IEditorIdentifier { const identifier = thing as IEditorIdentifier; return identifier && typeof identifier.groupId === 'number'; diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index 2c51b30d49..4cd4d03a7b 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -68,7 +68,7 @@ export class EditorControl extends Disposable { const control = this.doShowEditorControl(descriptor); // Set input - return this.doSetInput(control, editor, withUndefinedAsNull(options)).then((editorChanged => (({ control, editorChanged } as IOpenEditorResult)))); + return this.doSetInput(control, editor, withUndefinedAsNull(options)).then((editorChanged => (({ control, editorChanged })))); } private doShowEditorControl(descriptor: IEditorDescriptor): BaseEditor { diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 5e76301fa2..87a52a54ea 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -257,12 +257,16 @@ class DropOverlay extends Themable { // Check for URI transfer else { const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true /* open workspace instead of file if dropped */ }); - dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => targetGroup!.focus()); + dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => { + if (targetGroup) { + targetGroup.focus(); + } + }); } } private isCopyOperation(e: DragEvent, draggedEditor?: IEditorIdentifier): boolean { - if (draggedEditor && !(draggedEditor.editor as EditorInput).supportsSplitEditor()) { + if (draggedEditor && draggedEditor.editor instanceof EditorInput && !draggedEditor.editor.supportsSplitEditor()) { return false; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index baef4f031e..68e29d985e 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -49,7 +49,7 @@ import { GlobalNewUntitledFileAction } from 'vs/workbench/contrib/files/browser/ import { isErrorWithActions, IErrorWithActions } from 'vs/base/common/errorsWithActions'; import { IVisibleEditor } from 'vs/workbench/services/editor/common/editorService'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { IHashService } from 'vs/workbench/services/hash/common/hashService'; +import { hash } from 'vs/base/common/hash'; import { guessMimeTypes } from 'vs/base/common/mime'; import { extname } from 'vs/base/common/path'; @@ -132,7 +132,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @IKeybindingService private readonly keybindingService: IKeybindingService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IHashService private readonly hashService: IHashService, // {{SQL CARBON EDIT}} @ICommandService private commandService: ICommandService ) { @@ -224,7 +223,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { let activeEditorListener: IDisposable; const observeActiveEditor = () => { - activeEditorListener = dispose(activeEditorListener); + dispose(activeEditorListener); const activeEditor = this._group.activeEditor; if (activeEditor) { @@ -467,16 +466,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private onDidEditorOpen(editor: EditorInput): void { // Telemetry - this.toEditorTelemetryDescriptor(editor).then(descriptor => { - /* __GDPR__ - "editorOpened" : { - "${include}": [ - "${EditorTelemetryDescriptor}" - ] - } - */ - this.telemetryService.publicLog('editorOpened', descriptor); - }); + /* __GDPR__ + "editorOpened" : { + "${include}": [ + "${EditorTelemetryDescriptor}" + ] + } + */ + this.telemetryService.publicLog('editorOpened', this.toEditorTelemetryDescriptor(editor)); // Update container this.updateContainer(); @@ -508,16 +505,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { }); // Telemetry - this.toEditorTelemetryDescriptor(event.editor).then(descriptor => { - /* __GDPR__ + /* __GDPR__ "editorClosed" : { "${include}": [ "${EditorTelemetryDescriptor}" ] } */ - this.telemetryService.publicLog('editorClosed', descriptor); - }); + this.telemetryService.publicLog('editorClosed', this.toEditorTelemetryDescriptor(event.editor)); // Update container this.updateContainer(); @@ -527,24 +522,22 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_CLOSE, editor, editorIndex: event.index }); } - private toEditorTelemetryDescriptor(editor: EditorInput): Thenable { + private toEditorTelemetryDescriptor(editor: EditorInput): object { const descriptor = editor.getTelemetryDescriptor(); const resource = editor.getResource(); if (resource && resource.fsPath) { - return this.hashService.createSHA1(resource.fsPath).then(hashedPath => { - descriptor['resource'] = { mimeType: guessMimeTypes(resource.fsPath).join(', '), scheme: resource.scheme, ext: extname(resource.fsPath), path: hashedPath }; + descriptor['resource'] = { mimeType: guessMimeTypes(resource.fsPath).join(', '), scheme: resource.scheme, ext: extname(resource.fsPath), path: hash(resource.fsPath) }; - /* __GDPR__FRAGMENT__ - "EditorTelemetryDescriptor" : { - "resource": { "${inline}": [ "${URIDescriptor}" ] } - } - */ - return descriptor; - }); + /* __GDPR__FRAGMENT__ + "EditorTelemetryDescriptor" : { + "resource": { "${inline}": [ "${URIDescriptor}" ] } + } + */ + return descriptor; } - return Promise.resolve(descriptor); + return descriptor; } private onDidEditorDispose(editor: EditorInput): void { @@ -1229,10 +1222,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Filter: direction (left / right) - else if (hasDirection) { + else if (hasDirection && filter.except) { editorsToClose = (filter.direction === CloseDirection.LEFT) ? - editorsToClose.slice(0, this._group.indexOf(filter.except as EditorInput)) : - editorsToClose.slice(this._group.indexOf(filter.except as EditorInput) + 1); + editorsToClose.slice(0, this._group.indexOf(filter.except)) : + editorsToClose.slice(this._group.indexOf(filter.except) + 1); } // Filter: except diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 54f3090ac9..c30c9b54de 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -844,7 +844,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro } private doCreateGridControlWithPreviousState(): boolean { - const uiState = this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY] as IEditorPartUIState; + const uiState: IEditorPartUIState = this.workspaceMemento[EditorPart.EDITOR_PART_UI_STATE_STORAGE_KEY]; if (uiState && uiState.serializedGrid) { try { diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 15a84779e0..2a402d0d85 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -6,10 +6,10 @@ import 'vs/css!./media/editorstatus'; import * as nls from 'vs/nls'; import { $, append, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; -import * as strings from 'vs/base/common/strings'; +import { format } from 'vs/base/common/strings'; import { extname, basename } from 'vs/base/common/resources'; -import * as types from 'vs/base/common/types'; -import { URI as uri } from 'vs/base/common/uri'; +import { areFunctions, withNullAsUndefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { Action } from 'vs/base/common/actions'; import { Language } from 'vs/base/common/platform'; @@ -87,7 +87,7 @@ function toEditorWithEncodingSupport(input: IEditorInput): IEncodingSupport | nu // File or Resource Editor let encodingSupport = input as IFileEditorInput; - if (types.areFunctions(encodingSupport.setEncoding, encodingSupport.getEncoding)) { + if (areFunctions(encodingSupport.setEncoding, encodingSupport.getEncoding)) { return encodingSupport; } @@ -457,18 +457,18 @@ export class EditorStatus implements IStatusbarItem { if (info.selections.length === 1) { if (info.charactersSelected) { - return strings.format(nlsSingleSelectionRange, info.selections[0].positionLineNumber, info.selections[0].positionColumn, info.charactersSelected); + return format(nlsSingleSelectionRange, info.selections[0].positionLineNumber, info.selections[0].positionColumn, info.charactersSelected); } - return strings.format(nlsSingleSelection, info.selections[0].positionLineNumber, info.selections[0].positionColumn); + return format(nlsSingleSelection, info.selections[0].positionLineNumber, info.selections[0].positionColumn); } if (info.charactersSelected) { - return strings.format(nlsMultiSelectionRange, info.selections.length, info.charactersSelected); + return format(nlsMultiSelectionRange, info.selections.length, info.charactersSelected); } if (info.selections.length > 0) { - return strings.format(nlsMultiSelection, info.selections.length); + return format(nlsMultiSelection, info.selections.length); } return undefined; @@ -536,7 +536,7 @@ export class EditorStatus implements IStatusbarItem { private updateStatusBar(): void { const activeControl = this.editorService.activeControl; - const activeCodeEditor = activeControl ? types.withNullAsUndefined(getCodeEditor(activeControl.getControl())) : undefined; + const activeCodeEditor = activeControl ? withNullAsUndefined(getCodeEditor(activeControl.getControl())) : undefined; // Update all states this.onScreenReaderModeChange(activeCodeEditor); @@ -769,7 +769,7 @@ export class EditorStatus implements IStatusbarItem { this.updateState(info); } - private onResourceEncodingChange(resource: uri): void { + private onResourceEncodingChange(resource: URI): void { const activeControl = this.editorService.activeControl; if (activeControl) { const activeResource = toResource(activeControl.input, { supportSideBySide: true }); @@ -876,14 +876,14 @@ export class ChangeModeAction extends Action { } // construct a fake resource to be able to show nice icons if any - let fakeResource: uri | undefined; + let fakeResource: URI | undefined; const extensions = this.modeService.getExtensions(lang); if (extensions && extensions.length) { - fakeResource = uri.file(extensions[0]); + fakeResource = URI.file(extensions[0]); } else { const filenames = this.modeService.getFilenames(lang); if (filenames && filenames.length) { - fakeResource = uri.file(filenames[0]); + fakeResource = URI.file(filenames[0]); } } @@ -997,7 +997,7 @@ export class ChangeModeAction extends Action { }); } - private configureFileAssociation(resource: uri): void { + private configureFileAssociation(resource: URI): void { const extension = extname(resource); const base = basename(resource); const currentAssociation = this.modeService.getModeIdByFilepathOrFirstLine(base); @@ -1208,7 +1208,7 @@ export class ChangeEncodingAction extends Action { .then((guessedEncoding: string) => { const isReopenWithEncoding = (action === reopenWithEncodingPick); - const configuredEncoding = this.textResourceConfigurationService.getValue(types.withNullAsUndefined(resource), 'files.encoding'); + const configuredEncoding = this.textResourceConfigurationService.getValue(withNullAsUndefined(resource), 'files.encoding'); let directMatchIndex: number | undefined; let aliasMatchIndex: number | undefined; diff --git a/src/vs/workbench/browser/parts/editor/editorWidgets.ts b/src/vs/workbench/browser/parts/editor/editorWidgets.ts index 9c6e26ddd7..f5f583e2c0 100644 --- a/src/vs/workbench/browser/parts/editor/editorWidgets.ts +++ b/src/vs/workbench/browser/parts/editor/editorWidgets.ts @@ -101,7 +101,7 @@ export class OpenWorkspaceButtonContribution extends Disposable implements IEdit private static readonly ID = 'editor.contrib.openWorkspaceButton'; - private openWorkspaceButton: FloatingClickWidget; + private openWorkspaceButton: FloatingClickWidget | undefined; constructor( private editor: ICodeEditor, @@ -163,7 +163,7 @@ export class OpenWorkspaceButtonContribution extends Disposable implements IEdit this._register(this.openWorkspaceButton.onClick(() => { const model = this.editor.getModel(); if (model) { - this.windowService.openWindow([{ uri: model.uri, typeHint: 'file' }]); + this.windowService.openWindow([{ fileUri: model.uri }]); } })); @@ -172,7 +172,8 @@ export class OpenWorkspaceButtonContribution extends Disposable implements IEdit } private disposeOpenWorkspaceWidgetRenderer(): void { - this.openWorkspaceButton = dispose(this.openWorkspaceButton); + dispose(this.openWorkspaceButton); + this.openWorkspaceButton = undefined; } dispose(): void { diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index f89c835b0e..0e18690a19 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -141,12 +141,12 @@ export class SideBySideEditor extends BaseEditor { this.splitview.layout(dimension.width); } - getControl(): IEditorControl | null { + getControl(): IEditorControl | undefined { if (this.masterEditor) { return this.masterEditor.getControl(); } - return null; + return undefined; } getMasterEditor(): IEditor | undefined { diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index b43c137b1c..c61d5f04d7 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -227,7 +227,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { private isFileBinaryError(error: Error[]): boolean; private isFileBinaryError(error: Error): boolean; - private isFileBinaryError(error: any): boolean { + private isFileBinaryError(error: Error | Error[]): boolean { if (types.isArray(error)) { const errors = error; return errors.some(e => this.isFileBinaryError(e)); @@ -312,9 +312,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { return control.saveViewState(); } - private toDiffEditorViewStateResource(modelOrInput: IDiffEditorModel | DiffEditorInput): URI | null { - let original: URI | null; - let modified: URI | null; + private toDiffEditorViewStateResource(modelOrInput: IDiffEditorModel | DiffEditorInput): URI | undefined { + let original: URI | undefined; + let modified: URI | undefined; if (modelOrInput instanceof DiffEditorInput) { original = modelOrInput.originalInput.getResource(); @@ -325,7 +325,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { } if (!original || !modified) { - return null; + return undefined; } // create a URI that is the Base64 concatenation of original + modified resource diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index ddba49574f..20ccc92db4 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -19,7 +19,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextFileService, SaveReason, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { isDiffEditor, isCodeEditor, ICodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { isDiffEditor, isCodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -241,7 +241,11 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { } protected retrieveTextEditorViewState(resource: URI): IEditorViewState | null { - const control = this.getControl() as ICodeEditor; + const control = this.getControl(); + if (!isCodeEditor(control)) { + return null; + } + const model = control.getModel(); if (!model) { return null; // view state always needs a model @@ -302,7 +306,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { } } - protected getResource(): URI | null { + protected getResource(): URI | undefined { const codeEditor = getCodeEditor(this.editorControl); if (codeEditor) { const model = codeEditor.getModel(); @@ -315,7 +319,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { return this.input.getResource(); } - return null; + return undefined; } protected abstract getAriaLabel(): string; diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index c71e8fcc5f..e57872bfb5 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -8,7 +8,7 @@ import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { Action, IAction, IRunEvent } from 'vs/base/common/actions'; +import { IAction, IRunEvent } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; @@ -123,10 +123,10 @@ export abstract class TitleControl extends Themable { protected abstract handleBreadcrumbsEnablementChange(): void; protected createEditorActionsToolBar(container: HTMLElement): void { - const context = { groupId: this.group.id } as IEditorCommandsContext; + const context: IEditorCommandsContext = { groupId: this.group.id }; this.editorActionsToolbar = this._register(new ToolBar(container, this.contextMenuService, { - actionItemProvider: action => this.actionItemProvider(action as Action), + actionItemProvider: action => this.actionItemProvider(action), orientation: ActionsOrientation.HORIZONTAL, ariaLabel: localize('araLabelEditorActions', "Editor actions"), getKeyBinding: action => this.getKeybinding(action), @@ -156,7 +156,7 @@ export abstract class TitleControl extends Themable { })); } - private actionItemProvider(action: Action): IActionItem | undefined { + private actionItemProvider(action: IAction): IActionItem | undefined { const activeControl = this.group.activeControl; // Check Active Editor @@ -303,7 +303,7 @@ export abstract class TitleControl extends Themable { this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => actions, - getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.getIndexOfEditor(editor) } as IEditorCommandsContext), + getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.getIndexOfEditor(editor) }), getKeyBinding: (action) => this.getKeybinding(action), onHide: () => { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index f2e54681ee..dd2adc09cc 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -57,7 +57,7 @@ export interface INotificationsToastController { export function registerNotificationCommands(center: INotificationsCenterController, toasts: INotificationsToastController): void { - function getNotificationFromContext(listService: IListService, context?: any): INotificationViewItem | undefined { + function getNotificationFromContext(listService: IListService, context?: unknown): INotificationViewItem | undefined { if (isNotificationViewItem(context)) { return context; } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 8706689698..6f6fafbf74 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -16,7 +16,7 @@ import { IAction, IActionRunner } from 'vs/base/common/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { DropdownMenuActionItem, IContextMenuProvider } from 'vs/base/browser/ui/dropdown/dropdown'; +import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { INotificationViewItem, NotificationViewItem, NotificationViewItemLabelKind, INotificationMessage, ChoiceAction } from 'vs/workbench/common/notifications'; import { ClearNotificationAction, ExpandNotificationAction, CollapseNotificationAction, ConfigureNotificationAction } from 'vs/workbench/browser/parts/notifications/notificationsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -221,7 +221,7 @@ export class NotificationRenderer implements IListRenderer { if (action && action instanceof ConfigureNotificationAction) { - const item = new DropdownMenuActionItem(action, action.configurationActions, this.contextMenuService as IContextMenuProvider, undefined, this.actionRunner, undefined, action.class as string); + const item = new DropdownMenuActionItem(action, action.configurationActions, this.contextMenuService, undefined, this.actionRunner, undefined, action.class); data.toDispose.push(item); return item; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 18d30c5951..522491d0bf 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -19,7 +19,7 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntry } from 'vs/platf 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'; -import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme'; +import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; @@ -329,11 +329,7 @@ class StatusBarEntryItem implements IStatusbarItem { el.appendChild(textContainer); - return { - dispose: () => { - toDispose = dispose(toDispose); - } - }; + return toDisposable(() => toDispose = dispose(toDispose)); } private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): IDisposable { @@ -354,7 +350,7 @@ class StatusBarEntryItem implements IStatusbarItem { return combinedDisposable(disposable); } - private executeCommand(id: string, args?: any[]) { + private executeCommand(id: string, args?: unknown[]) { args = args || []; // Maintain old behaviour of always focusing the editor here @@ -398,6 +394,11 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item a:active { background-color: ${statusBarItemActiveBackground}; }`); } + const statusBarProminentItemForeground = theme.getColor(STATUS_BAR_PROMINENT_ITEM_FOREGROUND); + if (statusBarProminentItemForeground) { + collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item .status-bar-info { color: ${statusBarProminentItemForeground}; }`); + } + const statusBarProminentItemBackground = theme.getColor(STATUS_BAR_PROMINENT_ITEM_BACKGROUND); if (statusBarProminentItemBackground) { collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item .status-bar-info { background-color: ${statusBarProminentItemBackground}; }`); diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 8dfb1816d0..9c42b1eac4 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { IMenubarMenu, IMenubarMenuItemAction, IMenubarMenuItemSubmenu, IMenubarKeybinding, IMenubarService, IMenubarData, MenubarMenuItem } from 'vs/platform/menubar/common/menubar'; import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWindowService, MenuBarVisibility, IWindowsService, getTitleBarStyle, URIType } from 'vs/platform/windows/common/windows'; +import { IWindowService, MenuBarVisibility, IWindowsService, getTitleBarStyle, IURIToOpen } from 'vs/platform/windows/common/windows'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IAction, Action } from 'vs/base/common/actions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -355,36 +355,35 @@ export class MenubarControl extends Disposable { return label; } - private createOpenRecentMenuAction(recent: IRecent, isFile: boolean): IAction & { uri: URI } { + private createOpenRecentMenuAction(recent: IRecent): IAction & { uri: URI } { let label: string; let uri: URI; let commandId: string; - let typeHint: URIType | undefined; + let uriToOpen: IURIToOpen; if (isRecentFolder(recent)) { uri = recent.folderUri; label = recent.label || this.labelService.getWorkspaceLabel(uri, { verbose: true }); commandId = 'openRecentFolder'; - typeHint = 'folder'; + uriToOpen = { folderUri: uri }; } else if (isRecentWorkspace(recent)) { uri = recent.workspace.configPath; label = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); commandId = 'openRecentWorkspace'; - typeHint = 'file'; + uriToOpen = { workspaceUri: uri }; } else { uri = recent.fileUri; label = recent.label || this.labelService.getUriLabel(uri); commandId = 'openRecentFile'; - typeHint = 'file'; + uriToOpen = { fileUri: uri }; } const ret: IAction = new Action(commandId, unmnemonicLabel(label), undefined, undefined, (event) => { const openInNewWindow = event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))); - return this.windowService.openWindow([{ uri, typeHint }], { - forceNewWindow: openInNewWindow, - forceOpenWorkspaceAsFile: isFile + return this.windowService.openWindow([uriToOpen], { + forceNewWindow: openInNewWindow }); }); @@ -403,7 +402,7 @@ export class MenubarControl extends Disposable { if (workspaces.length > 0) { for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) { - result.push(this.createOpenRecentMenuAction(workspaces[i], false)); + result.push(this.createOpenRecentMenuAction(workspaces[i])); } result.push(new Separator()); @@ -411,7 +410,7 @@ export class MenubarControl extends Disposable { if (files.length > 0) { for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) { - result.push(this.createOpenRecentMenuAction(files[i], true)); + result.push(this.createOpenRecentMenuAction(files[i])); } result.push(new Separator()); diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 9710103443..570431b31c 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -178,7 +178,7 @@ export abstract class ViewletPanel extends Panel implements IView { return undefined; } - getActionsContext(): any { + getActionsContext(): unknown { return undefined; } diff --git a/src/vs/workbench/browser/quickopen.ts b/src/vs/workbench/browser/quickopen.ts index dc841b4604..c078e46b13 100644 --- a/src/vs/workbench/browser/quickopen.ts +++ b/src/vs/workbench/browser/quickopen.ts @@ -138,7 +138,7 @@ export class QuickOpenHandlerDescriptor { constructor(ctor: IConstructorSignature0, id: string, prefix: string, contextKey: string | undefined, description: string, instantProgress?: boolean); constructor(ctor: IConstructorSignature0, id: string, prefix: string, contextKey: string | undefined, helpEntries: QuickOpenHandlerHelpEntry[], instantProgress?: boolean); - constructor(ctor: IConstructorSignature0, id: string, prefix: string, contextKey: string | undefined, param: any, instantProgress: boolean = false) { + constructor(ctor: IConstructorSignature0, id: string, prefix: string, contextKey: string | undefined, param: string | QuickOpenHandlerHelpEntry[], instantProgress: boolean = false) { this.ctor = ctor; this.id = id; this.prefix = prefix; @@ -325,7 +325,7 @@ export class QuickOpenAction extends Action { this.enabled = !!this.quickOpenService; } - run(context?: any): Promise { + run(): Promise { // Show with prefix this.quickOpenService.show(this.prefix); diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 377cd7f83f..b745e76e57 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -93,7 +93,7 @@ export class Workbench extends Layout { } private previousUnexpectedError: { message: string | undefined, time: number } = { message: undefined, time: 0 }; - private handleUnexpectedError(error: any, logService: ILogService): void { + private handleUnexpectedError(error: unknown, logService: ILogService): void { const message = toErrorMessage(error, true); if (!message) { return; diff --git a/src/vs/workbench/buildfile.js b/src/vs/workbench/buildfile.js index 11cb83e6cd..231554eb98 100644 --- a/src/vs/workbench/buildfile.js +++ b/src/vs/workbench/buildfile.js @@ -25,8 +25,8 @@ exports.collectModules = function () { createModuleDescription('vs/workbench/services/search/node/searchApp', []), - createModuleDescription('vs/workbench/services/files/node/watcher/unix/watcherApp', []), - createModuleDescription('vs/workbench/services/files/node/watcher/nsfw/watcherApp', []), + createModuleDescription('vs/workbench/services/files2/node/watcher/unix/watcherApp', []), + createModuleDescription('vs/workbench/services/files2/node/watcher/nsfw/watcherApp', []), createModuleDescription('vs/workbench/services/extensions/node/extensionHostProcess', []), ]; diff --git a/src/vs/workbench/common/composite.ts b/src/vs/workbench/common/composite.ts index 692188a4ca..2ff7bf89b8 100644 --- a/src/vs/workbench/common/composite.ts +++ b/src/vs/workbench/common/composite.ts @@ -40,7 +40,7 @@ export interface IComposite { /** * Returns the underlying control of this composite. */ - getControl(): ICompositeControl | null; + getControl(): ICompositeControl | undefined; /** * Asks the underlying control to focus. diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 6158ecf66f..3a04999fa8 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -91,7 +91,7 @@ export interface IEditor { /** * Returns the underlying control of this editor. */ - getControl(): IEditorControl | null; + getControl(): IEditorControl | undefined; /** * Asks the underlying control to focus. @@ -279,7 +279,7 @@ export interface IEditorInput extends IDisposable { /** * Returns the associated resource of this input. */ - getResource(): URI | null; + getResource(): URI | undefined; /** * Unique type identifier for this inpput. @@ -319,7 +319,7 @@ export interface IEditorInput extends IDisposable { /** * Returns if the other object matches this input. */ - matches(other: any): boolean; + matches(other: unknown): boolean; } /** @@ -347,8 +347,8 @@ export abstract class EditorInput extends Disposable implements IEditorInput { /** * Returns the associated resource of this input if any. */ - getResource(): URI | null { - return null; + getResource(): URI | undefined { + return undefined; } /** @@ -392,7 +392,7 @@ export abstract class EditorInput extends Disposable implements IEditorInput { * * Subclasses should extend if they can contribute. */ - getTelemetryDescriptor(): { [key: string]: any } { + getTelemetryDescriptor(): { [key: string]: unknown } { /* __GDPR__FRAGMENT__ "EditorTelemetryDescriptor" : { "typeId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } @@ -453,7 +453,7 @@ export abstract class EditorInput extends Disposable implements IEditorInput { /** * Returns true if this input is identical to the otherInput. */ - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { return this === otherInput; } @@ -623,7 +623,7 @@ export class SideBySideEditorInput extends EditorInput { return this.description; } - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; } @@ -684,7 +684,7 @@ export interface IEditorInputWithOptions { options?: IEditorOptions | ITextEditorOptions; } -export function isEditorInputWithOptions(obj: any): obj is IEditorInputWithOptions { +export function isEditorInputWithOptions(obj: unknown): obj is IEditorInputWithOptions { const editorInputWithOptions = obj as IEditorInputWithOptions; return !!editorInputWithOptions && !!editorInputWithOptions.editor; @@ -948,7 +948,7 @@ export class EditorCommandsContextActionRunner extends ActionRunner { super(); } - run(action: IAction, context?: any): Promise { + run(action: IAction): Promise { return super.run(action, this.context); } } @@ -1007,7 +1007,7 @@ export function toResource(editor: IEditorInput | null | undefined, options?: IR const resource = editor.getResource(); if (!options || !options.filter) { - return resource; // return early if no filter is specified + return types.withUndefinedAsNull(resource); // return early if no filter is specified } if (!resource) { diff --git a/src/vs/workbench/common/editor/binaryEditorModel.ts b/src/vs/workbench/common/editor/binaryEditorModel.ts index f2901e1655..5a36e81554 100644 --- a/src/vs/workbench/common/editor/binaryEditorModel.ts +++ b/src/vs/workbench/common/editor/binaryEditorModel.ts @@ -72,7 +72,7 @@ export class BinaryEditorModel extends EditorModel { return this.etag; } - load(): Promise { + load(): Promise { // Make sure to resolve up to date stat for file resources if (this.fileService.canHandleResource(this.resource)) { diff --git a/src/vs/workbench/common/editor/dataUriEditorInput.ts b/src/vs/workbench/common/editor/dataUriEditorInput.ts index c94c8635ec..467f2317b0 100644 --- a/src/vs/workbench/common/editor/dataUriEditorInput.ts +++ b/src/vs/workbench/common/editor/dataUriEditorInput.ts @@ -60,10 +60,10 @@ export class DataUriEditorInput extends EditorInput { } resolve(): Promise { - return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load().then(m => m as BinaryEditorModel); + return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load(); } - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; } diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index a68e8bb0a4..4154668947 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -21,18 +21,20 @@ export class DiffEditorModel extends EditorModel { this._modifiedModel = modifiedModel; } - get originalModel(): EditorModel | null { + get originalModel(): IEditorModel | null { if (!this._originalModel) { return null; } - return this._originalModel as EditorModel; + + return this._originalModel; } - get modifiedModel(): EditorModel | null { + get modifiedModel(): IEditorModel | null { if (!this._modifiedModel) { return null; } - return this._modifiedModel as EditorModel; + + return this._modifiedModel; } load(): Promise { @@ -43,7 +45,7 @@ export class DiffEditorModel extends EditorModel { } isResolved(): boolean { - return !!this.originalModel && this.originalModel.isResolved() && !!this.modifiedModel && this.modifiedModel.isResolved(); + return this.originalModel instanceof EditorModel && this.originalModel.isResolved() && this.modifiedModel instanceof EditorModel && this.modifiedModel.isResolved(); } dispose(): void { diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 252024d1ca..297a9a3253 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { Extensions, IEditorInputFactoryRegistry, EditorInput, toResource, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, SideBySideEditorInput, CloseDirection } from 'vs/workbench/common/editor'; +import { Extensions, IEditorInputFactoryRegistry, EditorInput, toResource, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorInput } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; @@ -54,7 +54,7 @@ export interface ISerializedEditorGroup { } export function isSerializedEditorGroup(obj?: any): obj is ISerializedEditorGroup { - const group = obj as ISerializedEditorGroup; + const group: ISerializedEditorGroup = obj; return obj && typeof obj === 'object' && Array.isArray(group.editors) && Array.isArray(group.mru); } @@ -146,7 +146,7 @@ export class EditorGroup extends Disposable { getEditor(index: number): EditorInput | null; getEditor(resource: URI): EditorInput | null; - getEditor(arg1: any): EditorInput | null { + getEditor(arg1: number | URI): EditorInput | null { if (typeof arg1 === 'number') { return this.editors[arg1]; } @@ -507,7 +507,7 @@ export class EditorGroup extends Disposable { private splice(index: number, del: boolean, editor?: EditorInput): void { const editorToDeleteOrReplace = this.editors[index]; - const args: any[] = [index, del ? 1 : 0]; + const args: (number | EditorInput)[] = [index, del ? 1 : 0]; if (editor) { args.push(editor); } @@ -567,7 +567,7 @@ export class EditorGroup extends Disposable { } } - indexOf(candidate: EditorInput | null, editors = this.editors): number { + indexOf(candidate: IEditorInput | null, editors = this.editors): number { if (!candidate) { return -1; } @@ -620,7 +620,7 @@ export class EditorGroup extends Disposable { this.mru.unshift(editor); } - private matches(editorA: EditorInput | null, editorB: EditorInput | null): boolean { + private matches(editorA: IEditorInput | null, editorB: IEditorInput | null): boolean { return !!editorA && !!editorB && editorA.matches(editorB); } @@ -710,10 +710,11 @@ export class EditorGroup extends Disposable { this.editors = coalesce(data.editors.map(e => { const factory = registry.getEditorInputFactory(e.id); if (factory) { - const editor = factory.deserialize(this.instantiationService, e.value)!; - - this.registerEditorListeners(editor); - this.updateResourceMap(editor, false /* add */); + const editor = factory.deserialize(this.instantiationService, e.value); + if (editor) { + this.registerEditorListeners(editor); + this.updateResourceMap(editor, false /* add */); + } // {{SQL CARBON EDIT}} return CustomInputConverter.convertEditorInput(editor, undefined, this.instantiationService); diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 7bafadbb2d..00633b8523 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -74,14 +74,14 @@ export class ResourceEditorInput extends EditorInput { ref.dispose(); this.modelReference = null; - return Promise.reject(new Error(`Unexpected model for ResourceInput: ${this.resource}`)); + return Promise.reject(new Error(`Unexpected model for ResourceInput: ${this.resource}`)); } return model; }); } - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; } diff --git a/src/vs/workbench/common/editor/textDiffEditorModel.ts b/src/vs/workbench/common/editor/textDiffEditorModel.ts index 37d7cfc51f..16b266c9fd 100644 --- a/src/vs/workbench/common/editor/textDiffEditorModel.ts +++ b/src/vs/workbench/common/editor/textDiffEditorModel.ts @@ -13,6 +13,10 @@ import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; * and the modified version. */ export class TextDiffEditorModel extends DiffEditorModel { + + protected readonly _originalModel: BaseTextEditorModel; + protected readonly _modifiedModel: BaseTextEditorModel; + private _textDiffEditorModel: IDiffEditorModel | null; constructor(originalModel: BaseTextEditorModel, modifiedModel: BaseTextEditorModel) { @@ -22,11 +26,11 @@ export class TextDiffEditorModel extends DiffEditorModel { } get originalModel(): BaseTextEditorModel { - return this._originalModel as BaseTextEditorModel; + return this._originalModel; } get modifiedModel(): BaseTextEditorModel { - return this._modifiedModel as BaseTextEditorModel; + return this._modifiedModel; } load(): Promise { diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index 7eb60256df..4928bb315d 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -224,7 +224,7 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport return model; } - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; } diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index d52d1f2182..3f3fcb57dd 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -206,7 +206,7 @@ export interface INotificationViewItem { equals(item: INotificationViewItem): boolean; } -export function isNotificationViewItem(obj: any): obj is INotificationViewItem { +export function isNotificationViewItem(obj: unknown): obj is INotificationViewItem { return obj instanceof NotificationViewItem; } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index e3a623f10f..6a125726cf 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -291,6 +291,12 @@ export const STATUS_BAR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.hov hc: Color.white.transparent(0.12) }, nls.localize('statusBarItemHoverBackground', "Status bar item background color when hovering. The status bar is shown in the bottom of the window.")); +export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', { + dark: STATUS_BAR_FOREGROUND, + light: STATUS_BAR_FOREGROUND, + hc: STATUS_BAR_FOREGROUND +}, nls.localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); + export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', { dark: Color.black.transparent(0.5), light: Color.black.transparent(0.5), diff --git a/src/vs/workbench/common/viewlet.ts b/src/vs/workbench/common/viewlet.ts index 0ec64b9d5d..3ec5dd0402 100644 --- a/src/vs/workbench/common/viewlet.ts +++ b/src/vs/workbench/common/viewlet.ts @@ -6,7 +6,6 @@ import { IComposite } from 'vs/workbench/common/composite'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -export const SidebarVisibleContext = new RawContextKey('sidebarVisible', false); export const SideBarVisibleContext = new RawContextKey('sideBarVisible', false); export const SidebarFocusContext = new RawContextKey('sideBarFocus', false); export const ActiveViewletContext = new RawContextKey('activeViewlet', ''); diff --git a/src/vs/workbench/contrib/backup/common/backupRestorer.ts b/src/vs/workbench/contrib/backup/common/backupRestorer.ts index ea76c7833f..007d12c506 100644 --- a/src/vs/workbench/contrib/backup/common/backupRestorer.ts +++ b/src/vs/workbench/contrib/backup/common/backupRestorer.ts @@ -49,7 +49,7 @@ export class BackupRestorer implements IWorkbenchContribution { } private doResolveOpenedBackups(backups: URI[]): Promise { - const restorePromises: Promise[] = []; + const restorePromises: Promise[] = []; const unresolved: URI[] = []; backups.forEach(backup => { diff --git a/src/vs/workbench/contrib/cli/node/cli.contribution.ts b/src/vs/workbench/contrib/cli/node/cli.contribution.ts index 1b22a53c7a..e187bec14a 100644 --- a/src/vs/workbench/contrib/cli/node/cli.contribution.ts +++ b/src/vs/workbench/contrib/cli/node/cli.contribution.ts @@ -8,7 +8,7 @@ import * as path from 'vs/base/common/path'; import * as cp from 'child_process'; import * as pfs from 'vs/base/node/pfs'; import * as platform from 'vs/base/common/platform'; -import { nfcall } from 'vs/base/common/async'; +import { promisify } from 'util'; import { Action } from 'vs/base/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -105,7 +105,7 @@ class InstallAction extends Action { case 0 /* OK */: const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'' + getSource() + '\' \'' + this.target + '\'\\" with administrator privileges"'; - nfcall(cp.exec, command, {}) + promisify(cp.exec)(command, {}) .then(undefined, _ => Promise.reject(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'.")))) .then(resolve, reject); break; @@ -172,7 +172,7 @@ class UninstallAction extends Action { case 0 /* OK */: const command = 'osascript -e "do shell script \\"rm \'' + this.target + '\'\\" with administrator privileges"'; - nfcall(cp.exec, command, {}) + promisify(cp.exec)(command, {}) .then(undefined, _ => Promise.reject(new Error(nls.localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", this.target)))) .then(resolve, reject); break; diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index b7aa643264..bf78c1b654 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -42,7 +42,7 @@ export interface ICommentService { readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; - setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void; + setWorkspaceComments(owner: string, commentsByResource: CommentThread[] | CommentThread2[]): void; removeWorkspaceComments(owner: string): void; registerCommentController(owner: string, commentControl: MainThreadCommentController): void; unregisterCommentController(owner: string): void; diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 6a60a2e530..6ebd0c0878 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -60,7 +60,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _resizeObserver: any; private _onDidClose = new Emitter(); private _onDidCreateThread = new Emitter(); - private _isCollapsed: boolean; + private _isExpanded?: boolean; private _collapseAction: Action; private _commentGlyph?: CommentGlyphWidget; private _submitActionsDisposables: IDisposable[]; @@ -74,7 +74,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return this._owner; } public get commentThread(): modes.CommentThread { - return this._commentThread; + return this._commentThread as modes.CommentThread; } public get extensionId(): string | undefined { @@ -101,7 +101,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget ) { super(editor, { keepEditorSelection: true }); this._resizeObserver = null; - this._isCollapsed = _commentThread.collapsibleState !== modes.CommentThreadCollapsibleState.Expanded; + this._isExpanded = _commentThread.collapsibleState ? _commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded : undefined; this._globalToDispose = []; this._submitActionsDisposables = []; this._formActions = null; @@ -144,7 +144,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } public reveal(commentId?: string) { - if (this._isCollapsed) { + if (!this._isExpanded) { this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); } @@ -205,7 +205,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } public collapse(): Promise { - if (this._commentThread.comments.length === 0) { + if (this._commentThread.comments && this._commentThread.comments.length === 0) { if ((this._commentThread as modes.CommentThread2).commentThreadHandle === undefined) { this.dispose(); return Promise.resolve(); @@ -217,7 +217,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - this._isCollapsed = true; this.hide(); return Promise.resolve(); } @@ -230,25 +229,25 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } toggleExpand(lineNumber: number) { - if (this._isCollapsed) { - this.show({ lineNumber: lineNumber, column: 1 }, 2); - } else { + if (this._isExpanded) { this.hide(); if (this._commentThread === null || this._commentThread.threadId === null) { this.dispose(); } + } else { + this.show({ lineNumber: lineNumber, column: 1 }, 2); } } async update(commentThread: modes.CommentThread | modes.CommentThread2) { const oldCommentsLen = this._commentElements.length; - const newCommentsLen = commentThread.comments.length; + const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0; let commentElementsToDel: CommentNode[] = []; let commentElementsToDelIndex: number[] = []; for (let i = 0; i < oldCommentsLen; i++) { let comment = this._commentElements[i].comment; - let newComment = commentThread.comments.filter(c => c.commentId === comment.commentId); + let newComment = commentThread.comments ? commentThread.comments.filter(c => c.commentId === comment.commentId) : []; if (newComment.length) { this._commentElements[i].update(newComment[0]); @@ -267,7 +266,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget let lastCommentElement: HTMLElement | null = null; let newCommentNodeList: CommentNode[] = []; for (let i = newCommentsLen - 1; i >= 0; i--) { - let currentComment = commentThread.comments[i]; + let currentComment = commentThread.comments![i]; let oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.commentId === currentComment.commentId); if (oldCommentNode.length) { oldCommentNode[0].update(currentComment); @@ -291,6 +290,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentElements = newCommentNodeList; this.createThreadLabel(); + if (this._formActions && this._commentEditor.hasModel()) { + dom.clearNode(this._formActions); + const model = this._commentEditor.getModel(); + this.createCommentWidgetActions2(this._formActions, model); + } + // Move comment glyph widget and show position if the line has changed. const lineNumber = this._commentThread.range.startLineNumber; let shouldMoveWidget = false; @@ -305,9 +310,22 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this.createReplyButton(); } - if (shouldMoveWidget && !this._isCollapsed) { + if (this._commentThread.comments && this._commentThread.comments.length === 0) { + this.expandReplyArea(); + } + + if (shouldMoveWidget && this._isExpanded) { this.show({ lineNumber, column: 1 }, 2); } + + // The collapsible state is not initialized yet. + if (this._isExpanded === undefined) { + if (this._commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded) { + this.show({ lineNumber, column: 1 }, 2); + } else { + this.hide(); + } + } } updateDraftMode(draftMode: modes.DraftMode | undefined) { @@ -344,14 +362,16 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentsElement.setAttribute('role', 'presentation'); this._commentElements = []; - for (const comment of this._commentThread.comments) { - const newCommentNode = this.createNewCommentNode(comment); + if (this._commentThread.comments) { + for (const comment of this._commentThread.comments) { + const newCommentNode = this.createNewCommentNode(comment); - this._commentElements.push(newCommentNode); - this._commentsElement.appendChild(newCommentNode.domNode); + this._commentElements.push(newCommentNode); + this._commentsElement.appendChild(newCommentNode.domNode); + } } - const hasExistingComments = this._commentThread.comments.length > 0; + const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form')); this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions(), this._parentEditor, this); @@ -426,9 +446,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget if (hasExistingComments) { this.createReplyButton(); } else { - if (!dom.hasClass(this._commentForm, 'expand')) { - dom.addClass(this._commentForm, 'expand'); - this._commentEditor.focus(); + if (this._commentThread.comments && this._commentThread.comments.length === 0) { + this.expandReplyArea(); } } @@ -463,20 +482,20 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - if (shouldMoveWidget && !this._isCollapsed) { + if (shouldMoveWidget && this._isExpanded) { this.show({ lineNumber, column: 1 }, 2); } })); this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeCollasibleState(state => { - if (state === modes.CommentThreadCollapsibleState.Expanded && this._isCollapsed) { + if (state === modes.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { const lineNumber = this._commentThread.range.startLineNumber; this.show({ lineNumber, column: 1 }, 2); return; } - if (state === modes.CommentThreadCollapsibleState.Collapsed && !this._isCollapsed) { + if (state === modes.CommentThreadCollapsibleState.Collapsed && this._isExpanded) { this.hide(); return; } @@ -499,13 +518,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } // If there are no existing comments, place focus on the text area. This must be done after show, which also moves focus. - if ((this._commentThread as modes.CommentThread).reply && !this._commentThread.comments.length) { + // if this._commentThread.comments is undefined, it doesn't finish initialization yet, so we don't focus the editor immediately. + if ((this._commentThread as modes.CommentThread).reply && this._commentThread.comments && !this._commentThread.comments.length) { this._commentEditor.focus(); } else if (this._commentEditor.getModel()!.getValueLength() > 0) { - if (!dom.hasClass(this._commentForm, 'expand')) { - dom.addClass(this._commentForm, 'expand'); - } - this._commentEditor.focus(); + this.expandReplyArea(); } } @@ -616,8 +633,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget */ private createCommentWidgetActions2(container: HTMLElement, model: ITextModel) { let commentThread = this._commentThread as modes.CommentThread2; + const { acceptInputCommand, additionalCommands } = commentThread; - const { acceptInputCommand } = commentThread; if (acceptInputCommand) { const button = new Button(container); this._disposables.push(attachButtonStyler(button, this.themeService)); @@ -642,20 +659,22 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget })); } - commentThread.additionalCommands.reverse().forEach(command => { - const button = new Button(container); - this._disposables.push(attachButtonStyler(button, this.themeService)); + if (additionalCommands) { + additionalCommands.reverse().forEach(command => { + const button = new Button(container); + this._disposables.push(attachButtonStyler(button, this.themeService)); - button.label = command.title; - this._disposables.push(button.onDidClick(async () => { - commentThread.input = { - uri: this._commentEditor.getModel()!.uri, - value: this._commentEditor.getValue() - }; - this.commentService.setActiveCommentThread(this._commentThread); - await this.commandService.executeCommand(command.id, ...(command.arguments || [])); - })); - }); + button.label = command.title; + this._disposables.push(button.onDidClick(async () => { + commentThread.input = { + uri: this._commentEditor.getModel()!.uri, + value: this._commentEditor.getValue() + }; + this.commentService.setActiveCommentThread(this._commentThread); + await this.commandService.executeCommand(command.id, ...(command.arguments || [])); + })); + }); + } } private createNewCommentNode(comment: modes.Comment): CommentNode { @@ -676,15 +695,15 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentElements.splice(deletedElementIndex, 1); } - const deletedCommentIndex = arrays.firstIndex(this._commentThread.comments, comment => comment.commentId === deletedNodeId); + const deletedCommentIndex = arrays.firstIndex(this._commentThread.comments!, comment => comment.commentId === deletedNodeId); if (deletedCommentIndex > -1) { - this._commentThread.comments.splice(deletedCommentIndex, 1); + this._commentThread.comments!.splice(deletedCommentIndex, 1); } this._commentsElement.removeChild(deletedNode.domNode); deletedNode.dispose(); - if (this._commentThread.comments.length === 0) { + if (this._commentThread.comments!.length === 0) { this.dispose(); } })); @@ -786,7 +805,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } if (label === undefined) { - if (this._commentThread.comments.length) { + if (this._commentThread.comments && this._commentThread.comments.length) { const participantsList = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); label = nls.localize('commentThreadParticipants', "Participants: {0}", participantsList); } else { @@ -825,7 +844,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } _refresh() { - if (!this._isCollapsed && this._bodyElement) { + if (this._isExpanded && this._bodyElement) { let dimensions = dom.getClientArea(this._bodyElement); const headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2); const lineHeight = this.editor.getConfiguration().lineHeight; @@ -841,7 +860,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const model = this._commentEditor && this._commentEditor.getModel(); if (model) { const valueLength = model.getValueLength(); - const hasExistingComments = this._commentThread.comments.length > 0; + const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; const placeholder = valueLength > 0 ? '' : hasExistingComments @@ -996,13 +1015,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } show(rangeOrPos: IRange | IPosition, heightInLines: number): void { - this._isCollapsed = false; + this._isExpanded = true; super.show(rangeOrPos, heightInLines); this._refresh(); } hide() { - this._isCollapsed = true; + this._isExpanded = false; // Focus the container so that the comment editor will be blurred before it is hidden this.editor.focus(); super.hide(); diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index eedcd1341b..ad0f96ad6b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -265,9 +265,15 @@ export class ReviewController implements IEditorContribution { if (commentThreadWidget.length === 1) { commentThreadWidget[0].reveal(commentId); } else if (fetchOnceIfNotExist) { - this.beginCompute().then(_ => { - this.revealCommentThread(threadId, commentId, false); - }); + if (this._computePromise) { + this._computePromise.then(_ => { + this.revealCommentThread(threadId, commentId, false); + }); + } else { + this.beginCompute().then(_ => { + this.revealCommentThread(threadId, commentId, false); + }); + } } } diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index b5f229aa9e..ca770a151b 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -41,12 +41,12 @@ export class ResourceWithCommentThreads { constructor(resource: URI, commentThreads: CommentThread[]) { this.id = resource.toString(); this.resource = resource; - this.commentThreads = commentThreads.filter(thread => thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(resource, thread)); + this.commentThreads = commentThreads.filter(thread => thread.comments && thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(resource, thread)); } public static createCommentNode(resource: URI, commentThread: CommentThread): CommentNode { const { threadId, comments, range } = commentThread; - const commentNodes: CommentNode[] = comments.map(comment => new CommentNode(threadId!, resource, comment, range)); + const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(threadId!, resource, comment, range)); if (commentNodes.length > 1) { commentNodes[0].replies = commentNodes.slice(1, commentNodes.length); } @@ -107,7 +107,7 @@ export class CommentsModel { const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource); if (existingResource.length) { const resource = existingResource[0]; - if (thread.comments.length) { + if (thread.comments && thread.comments.length) { resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(resource.resource, thread)); } } else { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 1d2adc8cb6..3b9210ec0f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -558,6 +558,7 @@ export function openBreakpointSource(breakpoint: IBreakpoint, sideBySide: boolea preserveFocus, selection, revealIfVisible: true, + revealIfOpened: true, revealInCenterIfOutsideViewport: true, pinned: !preserveFocus } diff --git a/src/vs/workbench/contrib/debug/browser/debugActionItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionItems.ts index 690db74e63..1da6c8d30d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionItems.ts @@ -12,7 +12,7 @@ import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selec import { SelectActionItem, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugService, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IDebugSession, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; @@ -192,9 +192,10 @@ export class StartDebugActionItem implements IActionItem { export class FocusSessionActionItem extends SelectActionItem { constructor( action: IAction, - @IDebugService protected debugService: IDebugService, + @IDebugService protected readonly debugService: IDebugService, @IThemeService themeService: IThemeService, @IContextViewService contextViewService: IContextViewService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(null, action, [], -1, contextViewService, { ariaLabel: nls.localize('debugSession', 'Debug Session') }); @@ -234,6 +235,9 @@ export class FocusSessionActionItem extends SelectActionItem { } protected getSessions(): ReadonlyArray { - return this.debugService.getModel().getSessions(); + const hideSubSessions = this.configurationService.getValue('debug').hideSubSessions; + const sessions = this.debugService.getModel().getSessions(); + + return hideSubSessions ? sessions.filter(s => !s.parentSession) : sessions; } } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 54bbbc42ad..4865970c42 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -9,7 +9,7 @@ import { List } from 'vs/base/browser/ui/list/listWidget'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService } from 'vs/platform/list/browser/listService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, REPL_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, REPL_ID, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable, Breakpoint, FunctionBreakpoint, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { IExtensionsViewlet, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -30,6 +30,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { startDebugging } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; @@ -183,6 +184,12 @@ export function registerCommands(): void { const debugService = accessor.get(IDebugService); if (!session || !session.getId) { session = debugService.getViewModel().focusedSession; + const configurationService = accessor.get(IConfigurationService); + const hideSubSessions = configurationService.getValue('debug').hideSubSessions; + // Stop should be sent to the root parent session + while (hideSubSessions && session && session.parentSession) { + session = session.parentSession; + } } debugService.stopSession(session).then(undefined, onUnexpectedError); diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 63afde46e6..2ecd3d574e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -23,7 +23,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { registerColor, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { localize } from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -67,7 +67,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeService themeService: IThemeService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @IContextViewService contextViewService: IContextViewService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, @IContextMenuService contextMenuService: IContextMenuService, @@ -89,7 +88,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { orientation: ActionsOrientation.HORIZONTAL, actionItemProvider: (action: IAction) => { if (action.id === FocusSessionAction.ID) { - return new FocusSessionActionItem(action, this.debugService, this.themeService, contextViewService); + return this.instantiationService.createInstance(FocusSessionActionItem, action); } if (action instanceof MenuItemAction) { return new MenuItemActionItem(action, this.keybindingService, this.notificationService, contextMenuService); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 20cf5a754e..9562f1221c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -136,7 +136,7 @@ export class DebugViewlet extends ViewContainerViewlet { return this.startDebugActionItem; } if (action.id === FocusSessionAction.ID) { - return new FocusSessionActionItem(action, this.debugService, this.themeService, this.contextViewService); + return new FocusSessionActionItem(action, this.debugService, this.themeService, this.contextViewService, this.configurationService); } if (action instanceof MenuItemAction) { return new MenuItemActionItem(action, this.keybindingService, this.notificationService, this.contextMenuService); diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index a46f39169f..df8c8ea403 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -138,7 +138,7 @@ export class LinkDetector { private onLinkClick(event: IMouseEvent, resource: uri, line: number, column: number = 0): void { const selection = window.getSelection(); - if (selection.type === 'Range') { + if (!selection || selection.type === 'Range') { return; // do not navigate when user is selecting } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 2742fb34d5..298a7604a1 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -23,7 +23,7 @@ import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { ltrim } from 'vs/base/common/strings'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { ResourceLabels, IResourceLabelProps, IResourceLabelOptions, IResourceLabel, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; +import { ResourceLabels, IResourceLabelProps, IResourceLabelOptions, IResourceLabel } from 'vs/workbench/browser/labels'; import { FileKind } from 'vs/platform/files/common/files'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; @@ -416,7 +416,7 @@ export class LoadedScriptsView extends ViewletPanel { const root = new RootTreeItem(this.debugService.getModel(), this.environmentService, this.contextService, this.labelService); - this.treeLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer); + this.treeLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this.disposables.push(this.treeLabels); this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new LoadedScriptsDelegate(), diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index fb42faceb2..4c4f832c40 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -158,9 +158,15 @@ display: flex; } +.debug-viewlet .debug-call-stack .thread > .name, +.debug-viewlet .debug-call-stack .session > .name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; +} + .debug-viewlet .debug-call-stack .thread > .state, .debug-viewlet .debug-call-stack .session > .state { - flex: 1; text-align: right; overflow: hidden; text-overflow: ellipsis; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 99b7d2a100..8a69b5e031 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -424,6 +424,7 @@ export interface IDebugConfiguration { internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; extensionHostDebugAdapter: boolean; enableAllHovers: boolean; + hideSubSessions: boolean; console: { fontSize: number; fontFamily: string; diff --git a/src/vs/workbench/contrib/debug/common/debugSource.ts b/src/vs/workbench/contrib/debug/common/debugSource.ts index 00140127da..e0b20b0cae 100644 --- a/src/vs/workbench/contrib/debug/common/debugSource.ts +++ b/src/vs/workbench/contrib/debug/common/debugSource.ts @@ -93,6 +93,7 @@ export class Source { preserveFocus, selection, revealIfVisible: true, + revealIfOpened: true, revealInCenterIfOutsideViewport: true, pinned: pinned || (!preserveFocus && !this.inMemory) } diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 84b33596ec..4b19c48314 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -68,7 +68,7 @@ export class ViewModel implements IViewModel { this.loadedScriptsSupportedContextKey.set(session ? !!session.capabilities.supportsLoadedSourcesRequest : false); this.stepBackSupportedContextKey.set(session ? !!session.capabilities.supportsStepBack : false); this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false); - const attach = !!session && session.configuration.request === 'attach' && !isExtensionHostDebugging(session.configuration); + const attach = !!session && !session.parentSession && session.configuration.request === 'attach' && !isExtensionHostDebugging(session.configuration); this.focusedSessionIsAttach.set(attach); if (shouldEmitForSession) { diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts index 1b889f6c56..a1c641c524 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts @@ -33,9 +33,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; -import { IBroadcastService } from 'vs/workbench/services/broadcast/common/broadcast'; -import { IRemoteConsoleLog, parse, getFirstFrame } from 'vs/base/common/console'; +import { parse, getFirstFrame } from 'vs/base/common/console'; import { TaskEvent, TaskEventKind, TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -47,6 +45,7 @@ import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_ import { isExtensionHostDebugging } from 'vs/workbench/contrib/debug/common/debugUtils'; import { isErrorWithActions, createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_BREAKPOINTS_ACTIVATED_KEY = 'debug.breakpointactivated'; @@ -98,7 +97,6 @@ export class DebugService implements IDebugService { @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IBroadcastService private readonly broadcastService: IBroadcastService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IContextKeyService contextKeyService: IContextKeyService, @@ -109,6 +107,7 @@ export class DebugService implements IDebugService { @ITaskService private readonly taskService: ITaskService, @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService ) { this.toDispose = []; @@ -136,34 +135,31 @@ export class DebugService implements IDebugService { this.toDispose.push(this.storageService.onWillSaveState(this.saveState, this)); this.lifecycleService.onShutdown(this.dispose, this); - this.toDispose.push(this.broadcastService.onBroadcast(broadcast => { - const session = this.model.getSession(broadcast.payload.debugId, true); + this.toDispose.push(this.extensionHostDebugService.onAttachSession(data => { + const session = this.model.getSession(data.id, true); if (session) { - switch (broadcast.channel) { - - case EXTENSION_ATTACH_BROADCAST_CHANNEL: - // EH was started in debug mode -> attach to it - session.configuration.request = 'attach'; - session.configuration.port = broadcast.payload.port; - this.launchOrAttachToSession(session); - break; - - case EXTENSION_TERMINATE_BROADCAST_CHANNEL: - // EH was terminated - session.disconnect(); - break; - - case EXTENSION_LOG_BROADCAST_CHANNEL: - // extension logged output -> show it in REPL - const extensionOutput = broadcast.payload.logEntry; - const sev = extensionOutput.severity === 'warn' ? severity.Warning : extensionOutput.severity === 'error' ? severity.Error : severity.Info; - const { args, stack } = parse(extensionOutput); - const frame = !!stack ? getFirstFrame(stack) : undefined; - session.logToRepl(sev, args, frame); - break; - } + // EH was started in debug mode -> attach to it + session.configuration.request = 'attach'; + session.configuration.port = data.port; + this.launchOrAttachToSession(session).then(undefined, errors.onUnexpectedError); } - }, this)); + })); + this.toDispose.push(this.extensionHostDebugService.onTerminateSession(sessionId => { + const session = this.model.getSession(sessionId); + if (session) { + session.disconnect().then(undefined, errors.onUnexpectedError); + } + })); + this.toDispose.push(this.extensionHostDebugService.onLogToSession(data => { + const session = this.model.getSession(data.id, 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 frame = !!stack ? getFirstFrame(stack) : undefined; + session.logToRepl(sev, args, frame); + } + })); this.toDispose.push(this.viewModel.onDidFocusStackFrame(() => { this.onStateChange(); @@ -443,8 +439,10 @@ export class DebugService implements IDebugService { } this.viewModel.firstSessionStart = false; - - if (this.model.getSessions().length > 1) { + const hideSubSessions = this.configurationService.getValue('debug').hideSubSessions; + const sessions = this.model.getSessions(); + const shownSessions = hideSubSessions ? sessions.filter(s => !s.parentSession) : sessions; + if (shownSessions.length > 1) { this.viewModel.setMultiSessionView(true); } @@ -510,10 +508,7 @@ export class DebugService implements IDebugService { // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 if (isExtensionHostDebugging(session.configuration) && session.state === State.Running && session.configuration.noDebug) { - this.broadcastService.broadcast({ - channel: EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, - payload: [session.root.uri.toString()] - }); + this.extensionHostDebugService.close(session.root.uri); } this.telemetryDebugSessionStop(session, adapterExitEvent); @@ -561,10 +556,7 @@ export class DebugService implements IDebugService { } if (isExtensionHostDebugging(session.configuration) && session.root) { - return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? this.broadcastService.broadcast({ - channel: EXTENSION_RELOAD_BROADCAST_CHANNEL, - payload: [session.root.uri.toString()] - }) : undefined); + return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? this.extensionHostDebugService.reload(session.root.uri) : undefined); } const shouldFocus = this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId(); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index e2b2e16a04..1b1876b33c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -5,19 +5,20 @@ import * as dom from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; import { Action } from 'vs/base/common/actions'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { WorkbenchTreeController, WorkbenchTree, IListService } from 'vs/platform/list/browser/listService'; +import { IListService, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export interface IExtensionTemplateData { icon: HTMLImageElement; @@ -39,49 +40,40 @@ export interface IExtensionData { parent: IExtensionData | null; } -export class DataSource implements IDataSource { +export class AsyncDataSource implements IAsyncDataSource { - public getId(tree: ITree, { extension, parent }: IExtensionData): string { - return parent ? this.getId(tree, parent) + '/' + extension.identifier.id : extension.identifier.id; - } - - public hasChildren(tree: ITree, { hasChildren }: IExtensionData): boolean { + public hasChildren({ hasChildren }: IExtensionData): boolean { return hasChildren; } - public getChildren(tree: ITree, extensionData: IExtensionData): Promise { + public getChildren(extensionData: IExtensionData): Promise { return extensionData.getChildren(); } - public getParent(tree: ITree, { parent }: IExtensionData): Promise { - return Promise.resolve(parent); +} + +export class VirualDelegate implements IListVirtualDelegate { + + public getHeight(element: IExtensionData): number { + return 62; + } + public getTemplateId({ extension }: IExtensionData): string { + return extension ? ExtensionRenderer.TEMPLATE_ID : UnknownExtensionRenderer.TEMPLATE_ID; } } -export class Renderer implements IRenderer { +export class ExtensionRenderer implements IListRenderer, IExtensionTemplateData> { - private static readonly EXTENSION_TEMPLATE_ID = 'extension-template'; - private static readonly UNKNOWN_EXTENSION_TEMPLATE_ID = 'unknown-extension-template'; + static readonly TEMPLATE_ID = 'extension-template'; constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { } - public getHeight(tree: ITree, element: IExtensionData): number { - return 62; + public get templateId(): string { + return ExtensionRenderer.TEMPLATE_ID; } - public getTemplateId(tree: ITree, { extension }: IExtensionData): string { - return extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID; - } - - public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any { - if (Renderer.EXTENSION_TEMPLATE_ID === templateId) { - return this.renderExtensionTemplate(tree, container); - } - return this.renderUnknownExtensionTemplate(tree, container); - } - - private renderExtensionTemplate(tree: ITree, container: HTMLElement): IExtensionTemplateData { + public renderTemplate(container: HTMLElement): IExtensionTemplateData { dom.addClass(container, 'extension'); const icon = dom.append(container, dom.$('img.icon')); @@ -91,8 +83,6 @@ export class Renderer implements IRenderer { const name = dom.append(header, dom.$('span.name')); const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction); const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => { - tree.setFocus(openExtensionAction.extensionData); - tree.setSelection([openExtensionAction.extensionData]); openExtensionAction.run(e.ctrlKey || e.metaKey); e.stopPropagation(); e.preventDefault(); @@ -113,25 +103,8 @@ export class Renderer implements IRenderer { }; } - private renderUnknownExtensionTemplate(tree: ITree, container: HTMLElement): IUnknownExtensionTemplateData { - const messageContainer = dom.append(container, dom.$('div.unknown-extension')); - dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error"); - dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:"); - - const identifier = dom.append(messageContainer, dom.$('span.message')); - return { identifier }; - } - - public renderElement(tree: ITree, element: IExtensionData, templateId: string, templateData: any): void { - if (templateId === Renderer.EXTENSION_TEMPLATE_ID) { - this.renderExtension(tree, element, templateData); - return; - } - this.renderUnknownExtension(tree, element, templateData); - } - - private renderExtension(tree: ITree, extensionData: IExtensionData, data: IExtensionTemplateData): void { - const extension = extensionData.extension; + public renderElement(node: ITreeNode, index: number, data: IExtensionTemplateData): void { + const extension = node.element.extension; const onError = Event.once(domEvent(data.icon, 'error')); onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables); data.icon.src = extension.iconUrl; @@ -146,54 +119,36 @@ export class Renderer implements IRenderer { data.name.textContent = extension.displayName; data.identifier.textContent = extension.identifier.id; data.author.textContent = extension.publisherDisplayName; - data.extensionData = extensionData; + data.extensionData = node.element; } - private renderUnknownExtension(tree: ITree, { extension }: IExtensionData, data: IUnknownExtensionTemplateData): void { - data.identifier.textContent = extension.identifier.id; - } - - public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { - if (templateId === Renderer.EXTENSION_TEMPLATE_ID) { - templateData.extensionDisposables = dispose((templateData).extensionDisposables); - } + public disposeTemplate(templateData: IExtensionTemplateData): void { + templateData.extensionDisposables = dispose((templateData).extensionDisposables); } } -export class Controller extends WorkbenchTreeController { +export class UnknownExtensionRenderer implements IListRenderer, IUnknownExtensionTemplateData> { - constructor( - @IExtensionsWorkbenchService private readonly extensionsWorkdbenchService: IExtensionsWorkbenchService, - @IConfigurationService configurationService: IConfigurationService - ) { - super({}, configurationService); + static readonly TEMPLATE_ID = 'unknown-extension-template'; - // TODO@Sandeep this should be a command - this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, (tree: ITree, event: any) => this.openExtension(tree, true)); + public get templateId(): string { + return UnknownExtensionRenderer.TEMPLATE_ID; } - protected onLeftClick(tree: ITree, element: IExtensionData, event: IMouseEvent): boolean { - let currentFocused = tree.getFocus(); - if (super.onLeftClick(tree, element, event)) { - if (element.parent === null) { - if (currentFocused) { - tree.setFocus(currentFocused); - } else { - tree.focusFirst(); - } - return true; - } - } - return false; + public renderTemplate(container: HTMLElement): IUnknownExtensionTemplateData { + const messageContainer = dom.append(container, dom.$('div.unknown-extension')); + dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error"); + dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:"); + + const identifier = dom.append(messageContainer, dom.$('span.message')); + return { identifier }; } - public openExtension(tree: ITree, sideByside: boolean): boolean { - const element: IExtensionData = tree.getFocus(); - if (element.extension) { - this.extensionsWorkdbenchService.open(element.extension, sideByside); - return true; - } - return false; + public renderElement(node: ITreeNode, index: number, data: IUnknownExtensionTemplateData): void { + data.identifier.textContent = node.element.extension.identifier.id; + } + + public disposeTemplate(data: IUnknownExtensionTemplateData): void { } } @@ -218,7 +173,7 @@ class OpenExtensionAction extends Action { } } -export class ExtensionsTree extends WorkbenchTree { +export class ExtensionsTree extends WorkbenchAsyncDataTree { constructor( input: IExtensionData, @@ -227,30 +182,37 @@ export class ExtensionsTree extends WorkbenchTree { @IListService listService: IListService, @IThemeService themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IExtensionsWorkbenchService extensionsWorkdbenchService: IExtensionsWorkbenchService ) { - const renderer = instantiationService.createInstance(Renderer); - const controller = instantiationService.createInstance(Controller); + const delegate = new VirualDelegate(); + const dataSource = new AsyncDataSource(); + const renderers = [instantiationService.createInstance(ExtensionRenderer), instantiationService.createInstance(UnknownExtensionRenderer)]; + const identityProvider = { + getId({ extension, parent }: IExtensionData): string { + return parent ? this.getId(parent) + '/' + extension.identifier.id : extension.identifier.id; + } + }; super( container, + delegate, + renderers, + dataSource, { - dataSource: new DataSource(), - renderer, - controller - }, { - indentPixels: 40, - twistiePixels: 20 + indent: 40, + identityProvider, + multipleSelectionSupport: false }, - contextKeyService, listService, themeService, instantiationService, configurationService + contextKeyService, listService, themeService, configurationService, keybindingService, accessibilityService ); this.setInput(input); this.disposables.push(this.onDidChangeSelection(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - controller.openExtension(this, false); - } + extensionsWorkdbenchService.open(event.elements[0], event.browserEvent instanceof MouseEvent && (event.browserEvent.ctrlKey || event.browserEvent.metaKey || event.browserEvent.altKey)); })); } } \ No newline at end of file diff --git a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts index 734bc6e5ca..6b29ab787f 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts @@ -27,7 +27,7 @@ export class ExtensionsInput extends EditorInput { return localize('extensionsInputName', "Extension: {0}", this.extension.displayName); } - matches(other: any): boolean { + matches(other: unknown): boolean { if (!(other instanceof ExtensionsInput)) { return false; } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts index 58b552254a..96190e0e15 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts @@ -33,7 +33,6 @@ import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/we import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; @@ -136,7 +135,7 @@ class NavBar { } dispose(): void { - this.actionbar = dispose(this.actionbar); + dispose(this.actionbar); } } @@ -671,7 +670,7 @@ export class ExtensionEditor extends BaseEditor { }); } - private renderDependencies(container: HTMLElement, extensionDependencies: IExtensionDependencies): Tree { + private renderDependencies(container: HTMLElement, extensionDependencies: IExtensionDependencies): ExtensionsTree { class ExtensionData implements IExtensionData { private readonly extensionDependencies: IExtensionDependencies; @@ -720,7 +719,7 @@ export class ExtensionEditor extends BaseEditor { return Promise.resolve({ focus() { extensionsPackTree.domFocus(); } }); } - private renderExtensionPack(container: HTMLElement, extension: IExtension): Tree { + private renderExtensionPack(container: HTMLElement, extension: IExtension): ExtensionsTree { const extensionsWorkbenchService = this.extensionsWorkbenchService; class ExtensionData implements IExtensionData { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts index bc3def4a9d..b383a6cd15 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts @@ -10,17 +10,17 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { tmpdir } from 'os'; +import * as os from 'os'; import { join } from 'vs/base/common/path'; import { writeFile } from 'vs/base/node/pfs'; -import { IExtensionHostProfileService, ReportExtensionIssueAction } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; +import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput'; -import { generateUuid } from 'vs/base/common/uuid'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchContribution { @@ -30,11 +30,11 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont constructor( @IExtensionService private readonly _extensionService: IExtensionService, @IExtensionHostProfileService private readonly _extensionProfileService: IExtensionHostProfileService, - @IExtensionsWorkbenchService private readonly _anotherExtensionService: IExtensionsWorkbenchService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILogService private readonly _logService: ILogService, @INotificationService private readonly _notificationService: INotificationService, @IEditorService private readonly _editorService: IEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); this._register(_extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this)); @@ -132,16 +132,12 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont return; } - // add to running extensions view - this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile); // print message to log - const path = join(tmpdir(), `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`); + const path = join(os.tmpdir(), `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`); await writeFile(path, JSON.stringify(profile.data)); this._logService.warn(`UNRESPONSIVE extension host, '${top.id}' took ${top!.percentage}% of ${duration / 1e3}ms, saved PROFILE here: '${path}'`, data); - // send telemetry - const id = generateUuid(); /* __GDPR__ "exthostunresponsive" : { @@ -151,24 +147,22 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont } */ this._telemetryService.publicLog('exthostunresponsive', { - id, duration, data, }); + // add to running extensions view + this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile); + // prompt: when really slow/greedy if (!(top.percentage >= 99 && top.total >= 5e6)) { return; } - // prompt: only when you can file an issue - const reportAction = new ReportExtensionIssueAction({ - marketplaceInfo: this._anotherExtensionService.local.filter(value => ExtensionIdentifier.equals(value.identifier.id, extension.identifier))[0], - description: extension, - unresponsiveProfile: profile, - status: undefined, - }); - if (!reportAction.enabled) { + const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, extension, profile); + + if (!action) { + // cannot report issues against this extension... return; } @@ -190,18 +184,8 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont label: localize('show', 'Show Extensions'), run: () => this._editorService.openEditor(new RuntimeExtensionsInput()) }, - { - label: localize('report', "Report Issue"), - run: () => { - /* __GDPR__ - "exthostunresponsive/report" : { - "id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } - */ - this._telemetryService.publicLog('exthostunresponsive/report', { id }); - return reportAction.run(); - } - }], + action + ], { silent: true } ); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts new file mode 100644 index 0000000000..fece37e6a3 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions.ts @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import pkg from 'vs/platform/product/node/package'; +import { Action } from 'vs/base/common/actions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { URI } from 'vs/base/common/uri'; +import { IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { localize } from 'vs/nls'; +import { IRequestService } from 'vs/platform/request/node/request'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { asText } from 'vs/base/node/request'; +import { join } from 'vs/base/common/path'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; + +abstract class RepoInfo { + readonly base: string; + readonly owner: string; + readonly repo: string; + + static fromExtension(desc: IExtensionDescription): RepoInfo | undefined { + + let result: RepoInfo | undefined; + + // scheme:auth/OWNER/REPO/issues/ + if (desc.bugs && typeof desc.bugs.url === 'string') { + const base = URI.parse(desc.bugs.url); + const match = /\/([^/]+)\/([^/]+)\/issues\/?$/.exec(desc.bugs.url); + if (match) { + result = { + base: base.with({ path: null, fragment: null, query: null }).toString(true), + owner: match[1], + repo: match[2] + }; + } + } + // scheme:auth/OWNER/REPO.git + if (!result && desc.repository && typeof desc.repository.url === 'string') { + const base = URI.parse(desc.repository.url); + const match = /\/([^/]+)\/([^/]+)(\.git)?$/.exec(desc.repository.url); + if (match) { + result = { + base: base.with({ path: null, fragment: null, query: null }).toString(true), + owner: match[1], + repo: match[2] + }; + } + } + + // for now only GH is supported + if (result && result.base.indexOf('github') === -1) { + result = undefined; + } + + return result; + } +} + +export class SlowExtensionAction extends Action { + + constructor( + readonly extension: IExtensionDescription, + readonly profile: IExtensionHostProfile, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super('report.slow', localize('cmd.reportOrShow', "Performance Issue"), 'extension-action report-issue'); + this.enabled = Boolean(RepoInfo.fromExtension(extension)); + } + + async run(): Promise { + const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, this.extension, this.profile); + if (action) { + await action.run(); + } + } +} + +export async function createSlowExtensionAction( + accessor: ServicesAccessor, + extension: IExtensionDescription, + profile: IExtensionHostProfile +): Promise { + + const info = RepoInfo.fromExtension(extension); + if (!info) { + return undefined; + } + + const requestService = accessor.get(IRequestService); + const instaService = accessor.get(IInstantiationService); + const url = `https://api.github.com/search/issues?q=is:issue+state:open+in:title+repo:${info.owner}/${info.repo}+%22Extension+causes+high+cpu+load%22`; + const res = await requestService.request({ url }, CancellationToken.None); + const rawText = await asText(res); + if (!rawText) { + return undefined; + } + + const data = <{ total_count: number; }>JSON.parse(rawText); + if (!data || typeof data.total_count !== 'number') { + return undefined; + } else if (data.total_count === 0) { + return instaService.createInstance(ReportExtensionSlowAction, extension, info, profile); + } else { + return instaService.createInstance(ShowExtensionSlowAction, extension, info, profile); + } +} + +class ReportExtensionSlowAction extends Action { + + constructor( + readonly extension: IExtensionDescription, + readonly repoInfo: RepoInfo, + readonly profile: IExtensionHostProfile, + @IDialogService private readonly _dialogService: IDialogService, + ) { + super('report.slow', localize('cmd.report', "Report Issue")); + } + + async run(): Promise { + + // rewrite pii (paths) and store on disk + const profiler = await import('v8-inspect-profiler'); + const data = profiler.rewriteAbsolutePaths({ profile: this.profile.data }, 'pii_removed'); + const path = join(os.homedir(), `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`); + await profiler.writeProfile(data, path).then(undefined, onUnexpectedError); + + // build issue + const title = encodeURIComponent('Extension causes high cpu load'); + const osVersion = `${os.type()} ${os.arch()} ${os.release()}`; + const message = `:warning: Make sure to **attach** this file from your *home*-directory:\n:warning:\`${path}\`\n\nFind more details here: https://github.com/Microsoft/vscode/wiki/Explain:-extension-causes-high-cpu-load`; + const body = encodeURIComponent(`- Issue Type: \`Performance\` +- Extension Name: \`${this.extension.name}\` +- Extension Version: \`${this.extension.version}\` +- OS Version: \`${osVersion}\` +- VSCode version: \`${pkg.version}\`\n\n${message}`); + + const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues/new/?body=${body}&title=${title}`; + window.open(url); + + this._dialogService.show( + Severity.Info, + localize('attach.title', "Did you attach the CPU-Profile?"), + [localize('ok', 'OK')], + { detail: localize('attach.msg', "This is a reminder to make sure that you have not forgotten to attach '{0}' to the issue you have just created.", path) } + ); + } +} + +class ShowExtensionSlowAction extends Action { + + constructor( + readonly extension: IExtensionDescription, + readonly repoInfo: RepoInfo, + readonly profile: IExtensionHostProfile, + @IDialogService private readonly _dialogService: IDialogService, + ) { + super('show.slow', localize('cmd.show', "Show Issues")); + } + + async run(): Promise { + + // rewrite pii (paths) and store on disk + const profiler = await import('v8-inspect-profiler'); + const data = profiler.rewriteAbsolutePaths({ profile: this.profile.data }, 'pii_removed'); + const path = join(os.homedir(), `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`); + await profiler.writeProfile(data, path).then(undefined, onUnexpectedError); + + // show issues + const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues?utf8=✓&q=is%3Aissue+state%3Aopen+%22Extension+causes+high+cpu+load%22`; + window.open(url); + + this._dialogService.show( + Severity.Info, + localize('attach.title', "Did you attach the CPU-Profile?"), + [localize('ok', 'OK')], + { detail: localize('attach.msg2', "This is a reminder to make sure that you have not forgotten to attach '{0}' to an existing performance issue.", path) } + ); + } +} diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionEditor.css index 33b0be9b62..0a791ccae3 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionEditor.css @@ -300,11 +300,11 @@ border-color: rgb(238, 238, 238); } -.extension-editor .subcontent .monaco-tree-row .content .unknown-extension { +.extension-editor .subcontent .monaco-list-row .content .unknown-extension { line-height: 62px; } -.extension-editor .subcontent .monaco-tree-row .content .unknown-extension > .error-marker { +.extension-editor .subcontent .monaco-list-row .content .unknown-extension > .error-marker { background-color: #BE1100; padding: 2px 4px; font-weight: bold; @@ -312,46 +312,46 @@ color: #CCC; } -.extension-editor .subcontent .monaco-tree-row .unknown-extension > .message { +.extension-editor .subcontent .monaco-list-row .unknown-extension > .message { padding-left: 10px; font-weight: bold; font-size: 14px; } -.extension-editor .subcontent .monaco-tree-row .extension { +.extension-editor .subcontent .monaco-list-row .extension { display: flex; align-items: center; } -.extension-editor .subcontent .monaco-tree-row .extension > .details { +.extension-editor .subcontent .monaco-list-row .extension > .details { flex: 1; overflow: hidden; padding-left: 10px; } -.extension-editor .subcontent .monaco-tree-row .extension > .details > .header { +.extension-editor .subcontent .monaco-list-row .extension > .details > .header { display: flex; align-items: center; - height: 19px; + line-height: 19px; overflow: hidden; } -.extension-editor .subcontent .monaco-tree-row .extension > .icon { +.extension-editor .subcontent .monaco-list-row .extension > .icon { height: 40px; width: 40px; object-fit: contain; } -.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name { +.extension-editor .subcontent .monaco-list-row .extension > .details > .header > .name { font-weight: bold; font-size: 16px; } -.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .name:hover { +.extension-editor .subcontent .monaco-list-row .extension > .details > .header > .name:hover { text-decoration: underline; } -.extension-editor .subcontent .monaco-tree-row .extension > .details > .header > .identifier { +.extension-editor .subcontent .monaco-list-row .extension > .details > .header > .identifier { font-size: 90%; opacity: 0.6; margin-left: 10px; @@ -360,14 +360,14 @@ border-radius: 4px; } -.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer { +.extension-editor .subcontent .monaco-list-row .extension > .details > .footer { display: flex; - height: 19px; + line-height: 19px; overflow: hidden; padding-top: 5px; } -.extension-editor .subcontent .monaco-tree-row .extension > .details > .footer > .author { +.extension-editor .subcontent .monaco-list-row .extension > .details > .footer > .author { font-size: 90%; font-weight: 600; opacity: 0.6; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index f9858e871d..bfc72e821a 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -40,10 +40,9 @@ import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/cont import { IStorageService } from 'vs/platform/storage/common/storage'; import { ILabelService } from 'vs/platform/label/common/label'; import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; -import { join } from 'vs/base/common/path'; -import { onUnexpectedError } from 'vs/base/common/errors'; 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'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -308,7 +307,10 @@ export class RuntimeExtensionsEditor extends BaseEditor { data.activationTime.textContent = activationTimes.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`; data.actionbar.clear(); - if (element.unresponsiveProfile || isNonEmptyArray(element.status.runtimeErrors)) { + if (element.unresponsiveProfile) { + data.actionbar.push(this._instantiationService.createInstance(SlowExtensionAction, element.description, element.unresponsiveProfile), { icon: true, label: true }); + } + if (isNonEmptyArray(element.status.runtimeErrors)) { data.actionbar.push(new ReportExtensionIssueAction(element), { icon: true, label: true }); } @@ -471,7 +473,6 @@ export class ReportExtensionIssueAction extends Action { private static _label = nls.localize('reportExtensionIssue', "Report Issue"); private readonly _url: string; - private readonly _task?: () => Promise; constructor(extension: { description: IExtensionDescription; @@ -484,15 +485,10 @@ export class ReportExtensionIssueAction extends Action { && extension.marketplaceInfo.type === ExtensionType.User && !!extension.description.repository && !!extension.description.repository.url; - const { url, task } = ReportExtensionIssueAction._generateNewIssueUrl(extension); - this._url = url; - this._task = task; + this._url = ReportExtensionIssueAction._generateNewIssueUrl(extension); } async run(): Promise { - if (this._task) { - await this._task(); - } window.open(this._url); } @@ -501,9 +497,9 @@ export class ReportExtensionIssueAction extends Action { marketplaceInfo: IExtension; status?: IExtensionsStatus; unresponsiveProfile?: IExtensionHostProfile - }): { url: string, task?: () => Promise } { + }): string { + - let task: (() => Promise) | undefined; let baseUrl = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User && extension.description.repository ? extension.description.repository.url : undefined; if (!!baseUrl) { baseUrl = `${baseUrl.indexOf('.git') !== -1 ? baseUrl.substr(0, baseUrl.length - 4) : baseUrl}/issues/new/`; @@ -511,28 +507,10 @@ export class ReportExtensionIssueAction extends Action { baseUrl = product.reportIssueUrl; } - let title: string; - let message: string; - let reason: string; - if (extension.unresponsiveProfile) { - // unresponsive extension host caused - reason = 'Performance'; - title = 'Extension causes high cpu load'; - let path = join(os.homedir(), `${extension.description.identifier.value}-unresponsive.cpuprofile.txt`); - task = async () => { - const profiler = await import('v8-inspect-profiler'); - const data = profiler.rewriteAbsolutePaths({ profile: extension.unresponsiveProfile!.data }, 'pii_removed'); - profiler.writeProfile(data, path).then(undefined, onUnexpectedError); - }; - message = `:warning: Make sure to **attach** this file from your *home*-directory:\n:warning:\`${path}\`\n\nFind more details here: https://github.com/Microsoft/vscode/wiki/Explain:-extension-causes-high-cpu-load`; - - } else { - // generic - reason = 'Bug'; - title = 'Extension issue'; - message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:'; - clipboard.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```'); - } + let reason = 'Bug'; + let title = 'Extension issue'; + let message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:'; + clipboard.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```'); const osVersion = `${os.type()} ${os.arch()} ${os.release()}`; const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&'; @@ -544,10 +522,7 @@ export class ReportExtensionIssueAction extends Action { - VSCode version: \`${pkg.version}\`\n\n${message}` ); - return { - url: `${baseUrl}${queryStringPrefix}body=${body}&title=${encodeURIComponent(title)}`, - task - }; + return `${baseUrl}${queryStringPrefix}body=${body}&title=${encodeURIComponent(title)}`; } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts index 9eda275298..505183761c 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts @@ -23,7 +23,7 @@ export class RuntimeExtensionsInput extends EditorInput { return nls.localize('extensionsInputName', "Running Extensions"); } - matches(other: any): boolean { + matches(other: unknown): boolean { if (!(other instanceof RuntimeExtensionsInput)) { return false; } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts index fc79f50c9c..989c4f7785 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -9,6 +9,7 @@ import * as path from 'vs/base/common/path'; import * as fs from 'fs'; import * as os from 'os'; import * as uuid from 'vs/base/common/uuid'; +import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { IExtensionGalleryService, IGalleryExtensionAssets, IGalleryExtension, IExtensionManagementService, IExtensionEnablementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier @@ -25,9 +26,7 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; -import * as extfs from 'vs/base/node/extfs'; +import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IPager } from 'vs/base/common/paging'; import { assign } from 'vs/base/common/objects'; diff --git a/src/vs/workbench/contrib/feedback/electron-browser/feedback.ts b/src/vs/workbench/contrib/feedback/electron-browser/feedback.ts index cea80b6fa3..704362a938 100644 --- a/src/vs/workbench/contrib/feedback/electron-browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/electron-browser/feedback.ts @@ -99,7 +99,7 @@ export class FeedbackDropdown extends Dropdown { y: position.top - 9, // above status bar width: position.width, height: position.height - } as IAnchor; + }; } protected renderContents(container: HTMLElement): IDisposable { diff --git a/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts index 4deefc14f5..5d1b9da8e9 100644 --- a/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts @@ -5,7 +5,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; -import { FeedbackDropdown, IFeedback, IFeedbackDelegate, FEEDBACK_VISIBLE_CONFIG, IFeedbackDropdownOptions } from 'vs/workbench/contrib/feedback/electron-browser/feedback'; +import { FeedbackDropdown, IFeedback, IFeedbackDelegate, FEEDBACK_VISIBLE_CONFIG } from 'vs/workbench/contrib/feedback/electron-browser/feedback'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import product from 'vs/platform/product/node/product'; @@ -125,14 +125,14 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { this.dropdown = this._register(this.instantiationService.createInstance(FeedbackDropdown, this.container, { contextViewProvider: this.contextViewService, feedbackService: this.instantiationService.createInstance(TwitterFeedbackService), - onFeedbackVisibilityChange: visible => { + onFeedbackVisibilityChange: (visible: boolean) => { if (visible) { addClass(this.container, 'has-beak'); } else { removeClass(this.container, 'has-beak'); } } - } as IFeedbackDropdownOptions)); + })); this.updateStyles(); diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index 31f3096447..d085192756 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -12,7 +12,7 @@ import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/te import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { distinct, coalesce } from 'vs/base/common/arrays'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -34,10 +34,9 @@ import { QueryInput } from 'sql/parts/query/common/queryInput'; export class FileEditorTracker extends Disposable implements IWorkbenchContribution { - protected closeOnFileDelete: boolean; - - private modelLoadQueue: ResourceQueue; - private activeOutOfWorkspaceWatchers: ResourceMap; + private closeOnFileDelete: boolean; + private modelLoadQueue = new ResourceQueue(); + private activeOutOfWorkspaceWatchers = new ResourceMap(); constructor( @IEditorService private readonly editorService: IEditorService, @@ -52,9 +51,6 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut ) { super(); - this.modelLoadQueue = new ResourceQueue(); - this.activeOutOfWorkspaceWatchers = new ResourceMap(); - this.onConfigurationUpdated(configurationService.getValue()); this.registerListeners(); @@ -358,9 +354,9 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut }); // Handle no longer visible out of workspace resources - this.activeOutOfWorkspaceWatchers.forEach(resource => { + this.activeOutOfWorkspaceWatchers.keys().forEach(resource => { if (!visibleOutOfWorkspacePaths.get(resource)) { - this.fileService.unwatch(resource); + dispose(this.activeOutOfWorkspaceWatchers.get(resource)); this.activeOutOfWorkspaceWatchers.delete(resource); } }); @@ -368,8 +364,8 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // Handle newly visible out of workspace resources visibleOutOfWorkspacePaths.forEach(resource => { if (!this.activeOutOfWorkspaceWatchers.get(resource)) { - this.fileService.watch(resource); - this.activeOutOfWorkspaceWatchers.set(resource, resource); + const disposable = this.fileService.watch(resource); + this.activeOutOfWorkspaceWatchers.set(resource, disposable); } }); } @@ -377,8 +373,8 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut dispose(): void { super.dispose(); - // Dispose watchers if any - this.activeOutOfWorkspaceWatchers.forEach(resource => this.fileService.unwatch(resource)); + // Dispose remaining watchers if any + this.activeOutOfWorkspaceWatchers.forEach(disposable => dispose(disposable)); this.activeOutOfWorkspaceWatchers.clear(); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 0d4d71c122..87677fc4de 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -99,7 +99,7 @@ export class TextFileEditor extends BaseTextEditor { // React to editors closing to preserve or clear view state. This needs to happen // in the onWillCloseEditor because at that time the editor has not yet // been disposed and we can safely persist the view state still as needed. - this.groupListener = dispose(this.groupListener); + dispose(this.groupListener); this.groupListener = ((group as IEditorGroupView).onWillCloseEditor(e => this.onWillCloseEditorInGroup(e))); } @@ -178,7 +178,7 @@ export class TextFileEditor extends BaseTextEditor { if ((error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { this.openAsFolder(input); - return Promise.reject(new Error(nls.localize('openFolderError', "File is a directory"))); + return Promise.reject(new Error(nls.localize('openFolderError', "File is a directory"))); } // Offer to create a file from the error if we have a file not found and the name is valid @@ -296,7 +296,7 @@ export class TextFileEditor extends BaseTextEditor { } dispose(): void { - this.groupListener = dispose(this.groupListener); + dispose(this.groupListener); super.dispose(); } diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index ff1a6e2ea3..557442c890 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/explorerviewlet'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; -import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, OpenEditorsVisibleCondition, VIEW_CONTAINER } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, VIEW_CONTAINER } from 'vs/workbench/contrib/files/common/files'; import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { ExplorerView } from 'vs/workbench/contrib/files/browser/views/explorerView'; @@ -103,7 +103,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor name: OpenEditorsView.NAME, ctorDescriptor: { ctor: OpenEditorsView }, order: 0, - when: OpenEditorsVisibleCondition, + when: OpenEditorsVisibleContext, canToggleVisibility: true, focusCommand: { id: 'workbench.files.action.focusOpenEditorsView', diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index e71d03b3a7..3bdede378a 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -15,7 +15,7 @@ import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/c import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService, ExplorerResourceMoveableToTrash } from 'vs/workbench/contrib/files/common/files'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from 'vs/workbench/browser/actions/workspaceCommands'; import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { AutoSaveContext } from 'vs/workbench/services/textfile/common/textfiles'; @@ -63,7 +63,7 @@ const MOVE_FILE_TO_TRASH_ID = 'moveFileToTrash'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: MOVE_FILE_TO_TRASH_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.has('config.files.enableTrash')), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace @@ -75,7 +75,7 @@ const DELETE_FILE_ID = 'deleteFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.has('config.files.enableTrash')), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), primary: KeyMod.Shift | KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace @@ -86,7 +86,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.not('config.files.enableTrash')), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace @@ -517,7 +517,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { title: nls.localize('deleteFile', "Delete Permanently"), precondition: ExplorerResourceNotReadonlyContext }, - when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ContextKeyExpr.has('config.files.enableTrash')) + when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -528,7 +528,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { title: nls.localize('deleteFile', "Delete Permanently"), precondition: ExplorerResourceNotReadonlyContext }, - when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ContextKeyExpr.not('config.files.enableTrash')) + when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash.toNegated()) }); // Empty Editor Group Context Menu diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 493c3d2817..ec41105911 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -17,12 +17,12 @@ import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService, AutoSaveConfiguration } from 'vs/platform/files/common/files'; -import { toResource, IUntitledResourceInput, ITextEditor } from 'vs/workbench/common/editor'; +import { toResource, ITextEditor } from 'vs/workbench/common/editor'; import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IInstantiationService, ServicesAccessor, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; @@ -68,36 +68,14 @@ export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste"); export const FileCopiedContext = new RawContextKey('fileCopied', false); -export class BaseErrorReportingAction extends Action { +const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; - constructor( - id: string, - label: string, - private _notificationService: INotificationService - ) { - super(id, label); +function onError(notificationService: INotificationService, error: any): void { + if (error.message === 'string') { + error = error.message; } - public get notificationService() { - return this._notificationService; - } - - protected onError(error: any): void { - if (error.message === 'string') { - error = error.message; - } - - this._notificationService.error(toErrorMessage(error, false)); - } - - protected onErrorWithRetry(error: any, retry: () => Promise): void { - this._notificationService.prompt(Severity.Error, toErrorMessage(error, false), - [{ - label: nls.localize('retry', "Retry"), - run: () => retry() - }] - ); - } + notificationService.error(toErrorMessage(error, false)); } function refreshIfSeparator(value: string, explorerService: IExplorerService): void { @@ -108,66 +86,26 @@ function refreshIfSeparator(value: string, explorerService: IExplorerService): v } /* New File */ -export class NewFileAction extends BaseErrorReportingAction { +export class NewFileAction extends Action { static readonly ID = 'workbench.files.action.createFileFromExplorer'; static readonly LABEL = nls.localize('createNewFile', "New File"); private toDispose: IDisposable[] = []; constructor( - private getElement: () => ExplorerItem, - @INotificationService notificationService: INotificationService, - @IExplorerService private explorerService: IExplorerService, - @IFileService private fileService: IFileService, - @IEditorService private editorService: IEditorService + @IExplorerService explorerService: IExplorerService, + @ICommandService private commandService: ICommandService ) { - super('explorer.newFile', NEW_FILE_LABEL, notificationService); + super('explorer.newFile', NEW_FILE_LABEL); this.class = 'explorer-action new-file'; - this.toDispose.push(this.explorerService.onDidChangeEditable(e => { - const elementIsBeingEdited = this.explorerService.isEditable(e); + this.toDispose.push(explorerService.onDidChangeEditable(e => { + const elementIsBeingEdited = explorerService.isEditable(e); this.enabled = !elementIsBeingEdited; })); } run(): Promise { - let folder: ExplorerItem; - const element = this.getElement(); - if (element) { - folder = element.isDirectory ? element : element.parent!; - } else { - folder = this.explorerService.roots[0]; - } - - if (folder.isReadonly) { - return Promise.reject(new Error('Parent folder is readonly.')); - } - - const stat = new NewExplorerItem(folder, false); - return folder.fetchChildren(this.fileService, this.explorerService).then(() => { - folder.addChild(stat); - - const onSuccess = (value: string) => { - return this.fileService.createFile(resources.joinPath(folder.resource, value)).then(stat => { - refreshIfSeparator(value, this.explorerService); - return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); - }, (error) => { - this.onErrorWithRetry(error, () => onSuccess(value)); - }); - }; - - this.explorerService.setEditable(stat, { - validationMessage: value => validateFileName(stat, value), - onFinish: (value, success) => { - folder.removeChild(stat); - this.explorerService.setEditable(stat, null); - if (success) { - onSuccess(value); - } else { - this.explorerService.select(folder.resource).then(undefined, onUnexpectedError); - } - } - }); - }); + return this.commandService.executeCommand(NEW_FILE_COMMAND_ID); } dispose(): void { @@ -177,65 +115,26 @@ export class NewFileAction extends BaseErrorReportingAction { } /* New Folder */ -export class NewFolderAction extends BaseErrorReportingAction { +export class NewFolderAction extends Action { static readonly ID = 'workbench.files.action.createFolderFromExplorer'; static readonly LABEL = nls.localize('createNewFolder', "New Folder"); private toDispose: IDisposable[] = []; constructor( - private getElement: () => ExplorerItem, - @INotificationService notificationService: INotificationService, - @IFileService private fileService: IFileService, - @IExplorerService private explorerService: IExplorerService + @IExplorerService explorerService: IExplorerService, + @ICommandService private commandService: ICommandService ) { - super('explorer.newFolder', NEW_FOLDER_LABEL, notificationService); + super('explorer.newFolder', NEW_FOLDER_LABEL); this.class = 'explorer-action new-folder'; - this.toDispose.push(this.explorerService.onDidChangeEditable(e => { - const elementIsBeingEdited = this.explorerService.isEditable(e); + this.toDispose.push(explorerService.onDidChangeEditable(e => { + const elementIsBeingEdited = explorerService.isEditable(e); this.enabled = !elementIsBeingEdited; })); } run(): Promise { - let folder: ExplorerItem; - const element = this.getElement(); - if (element) { - folder = element.isDirectory ? element : element.parent!; - } else { - folder = this.explorerService.roots[0]; - } - - if (folder.isReadonly) { - return Promise.reject(new Error('Parent folder is readonly.')); - } - - const stat = new NewExplorerItem(folder, true); - return folder.fetchChildren(this.fileService, this.explorerService).then(() => { - folder.addChild(stat); - - const onSuccess = (value: string) => { - return this.fileService.createFolder(resources.joinPath(folder.resource, value)).then(stat => { - refreshIfSeparator(value, this.explorerService); - return this.explorerService.select(stat.resource, true); - }, (error) => { - this.onErrorWithRetry(error, () => onSuccess(value)); - }); - }; - - this.explorerService.setEditable(stat, { - validationMessage: value => validateFileName(stat, value), - onFinish: (value, success) => { - folder.removeChild(stat); - this.explorerService.setEditable(stat, null); - if (success) { - onSuccess(value); - } else { - this.explorerService.select(folder.resource).then(undefined, onUnexpectedError); - } - } - }); - }); + return this.commandService.executeCommand(NEW_FOLDER_COMMAND_ID); } dispose(): void { @@ -267,229 +166,210 @@ export class GlobalNewUntitledFileAction extends Action { } } -class BaseDeleteFileAction extends BaseErrorReportingAction { - - private static readonly CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; - - private skipConfirm: boolean; - - constructor( - private elements: ExplorerItem[], - private useTrash: boolean, - @IFileService private readonly fileService: IFileService, - @INotificationService notificationService: INotificationService, - @IDialogService private readonly dialogService: IDialogService, - @ITextFileService private readonly textFileService: ITextFileService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super('moveFileToTrash', MOVE_FILE_TO_TRASH_LABEL, notificationService); - - this.useTrash = useTrash && elements.every(e => !extpath.isUNC(e.resource.fsPath)); // on UNC shares there is no trash - this.enabled = this.elements && this.elements.every(e => !e.isReadonly); +function deleteFiles(serviceAccesor: ServicesAccessor, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { + let primaryButton: string; + if (useTrash) { + primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); + } else { + primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete"); } - public run(): Promise { + const distinctElements = resources.distinctParents(elements, e => e.resource); + const textFileService = serviceAccesor.get(ITextFileService); + const dialogService = serviceAccesor.get(IDialogService); + const configurationService = serviceAccesor.get(IConfigurationService); + const fileService = serviceAccesor.get(IFileService); - let primaryButton: string; - if (this.useTrash) { - primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); - } else { - primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete"); - } - - const distinctElements = resources.distinctParents(this.elements, e => e.resource); - - // Handle dirty - let confirmDirtyPromise: Promise = Promise.resolve(true); - const dirty = this.textFileService.getDirty().filter(d => distinctElements.some(e => resources.isEqualOrParent(d, e.resource, !isLinux /* ignorecase */))); - if (dirty.length) { - let message: string; - if (distinctElements.length > 1) { - message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?"); - } else if (distinctElements[0].isDirectory) { - if (dirty.length === 1) { - message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder with unsaved changes in 1 file. Do you want to continue?"); - } else { - message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length); - } + // Handle dirty + let confirmDirtyPromise: Promise = Promise.resolve(true); + const dirty = textFileService.getDirty().filter(d => distinctElements.some(e => resources.isEqualOrParent(d, e.resource, !isLinux /* ignorecase */))); + if (dirty.length) { + let message: string; + if (distinctElements.length > 1) { + message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?"); + } else if (distinctElements[0].isDirectory) { + if (dirty.length === 1) { + message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder with unsaved changes in 1 file. Do you want to continue?"); } else { - message = nls.localize('dirtyMessageFileDelete', "You are deleting a file with unsaved changes. Do you want to continue?"); + message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length); } - - confirmDirtyPromise = this.dialogService.confirm({ - message, - type: 'warning', - detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."), - primaryButton - }).then(res => { - if (!res.confirmed) { - return false; - } - - this.skipConfirm = true; // since we already asked for confirmation - return this.textFileService.revertAll(dirty).then(() => true); - }); + } else { + message = nls.localize('dirtyMessageFileDelete', "You are deleting a file with unsaved changes. Do you want to continue?"); } - // Check if file is dirty in editor and save it to avoid data loss - return confirmDirtyPromise.then(confirmed => { - if (!confirmed) { - return null; + confirmDirtyPromise = dialogService.confirm({ + message, + type: 'warning', + detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."), + primaryButton + }).then(res => { + if (!res.confirmed) { + return false; } - let confirmDeletePromise: Promise; - - // Check if we need to ask for confirmation at all - if (this.skipConfirm || (this.useTrash && this.configurationService.getValue(BaseDeleteFileAction.CONFIRM_DELETE_SETTING_KEY) === false)) { - confirmDeletePromise = Promise.resolve({ confirmed: true } as IConfirmationResult); - } - - // Confirm for moving to trash - else if (this.useTrash) { - const message = this.getMoveToTrashMessage(distinctElements); - - confirmDeletePromise = this.dialogService.confirm({ - message, - detail: isWindows ? nls.localize('undoBin', "You can restore from the Recycle Bin.") : nls.localize('undoTrash', "You can restore from the Trash."), - primaryButton, - checkbox: { - label: nls.localize('doNotAskAgain', "Do not ask me again") - }, - type: 'question' - }); - } - - // Confirm for deleting permanently - else { - const message = this.getDeleteMessage(distinctElements); - confirmDeletePromise = this.dialogService.confirm({ - message, - detail: nls.localize('irreversible', "This action is irreversible!"), - primaryButton, - type: 'warning' - }); - } - - return confirmDeletePromise.then(confirmation => { - - // Check for confirmation checkbox - let updateConfirmSettingsPromise: Promise = Promise.resolve(undefined); - if (confirmation.confirmed && confirmation.checkboxChecked === true) { - updateConfirmSettingsPromise = this.configurationService.updateValue(BaseDeleteFileAction.CONFIRM_DELETE_SETTING_KEY, false, ConfigurationTarget.USER); - } - - return updateConfirmSettingsPromise.then(() => { - - // Check for confirmation - if (!confirmation.confirmed) { - return Promise.resolve(null); - } - - // Call function - const servicePromise = Promise.all(distinctElements.map(e => this.fileService.del(e.resource, { useTrash: this.useTrash, recursive: true }))) - .then(undefined, (error: any) => { - // Handle error to delete file(s) from a modal confirmation dialog - let errorMessage: string; - let detailMessage: string | undefined; - let primaryButton: string; - if (this.useTrash) { - errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?"); - detailMessage = nls.localize('irreversible', "This action is irreversible!"); - primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); - } else { - errorMessage = toErrorMessage(error, false); - primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry"); - } - - return this.dialogService.confirm({ - message: errorMessage, - detail: detailMessage, - type: 'warning', - primaryButton - }).then(res => { - - if (res.confirmed) { - if (this.useTrash) { - this.useTrash = false; // Delete Permanently - } - - this.skipConfirm = true; - - return this.run(); - } - - return Promise.resolve(undefined); - }); - }); - - return servicePromise; - }); - }); + skipConfirm = true; // since we already asked for confirmation + return textFileService.revertAll(dirty).then(() => true); }); } - private getMoveToTrashMessage(distinctElements: ExplorerItem[]): string { - if (this.containsBothDirectoryAndFile(distinctElements)) { - return getConfirmMessage(nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + // Check if file is dirty in editor and save it to avoid data loss + return confirmDirtyPromise.then(confirmed => { + if (!confirmed) { + return undefined; } - if (distinctElements.length > 1) { - if (distinctElements[0].isDirectory) { - return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + let confirmDeletePromise: Promise; + + // Check if we need to ask for confirmation at all + if (skipConfirm || (useTrash && configurationService.getValue(CONFIRM_DELETE_SETTING_KEY) === false)) { + confirmDeletePromise = Promise.resolve({ confirmed: true }); + } + + // Confirm for moving to trash + else if (useTrash) { + const message = getMoveToTrashMessage(distinctElements); + + confirmDeletePromise = dialogService.confirm({ + message, + detail: isWindows ? nls.localize('undoBin', "You can restore from the Recycle Bin.") : nls.localize('undoTrash', "You can restore from the Trash."), + primaryButton, + checkbox: { + label: nls.localize('doNotAskAgain', "Do not ask me again") + }, + type: 'question' + }); + } + + // Confirm for deleting permanently + else { + const message = getDeleteMessage(distinctElements); + confirmDeletePromise = dialogService.confirm({ + message, + detail: nls.localize('irreversible', "This action is irreversible!"), + primaryButton, + type: 'warning' + }); + } + + return confirmDeletePromise.then(confirmation => { + + // Check for confirmation checkbox + let updateConfirmSettingsPromise: Promise = Promise.resolve(undefined); + if (confirmation.confirmed && confirmation.checkboxChecked === true) { + updateConfirmSettingsPromise = configurationService.updateValue(CONFIRM_DELETE_SETTING_KEY, false, ConfigurationTarget.USER); } - return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); - } + return updateConfirmSettingsPromise.then(() => { + // Check for confirmation + if (!confirmation.confirmed) { + return Promise.resolve(undefined); + } + + // Call function + const servicePromise = Promise.all(distinctElements.map(e => fileService.del(e.resource, { useTrash: useTrash, recursive: true }))) + .then(undefined, (error: any) => { + // Handle error to delete file(s) from a modal confirmation dialog + let errorMessage: string; + let detailMessage: string | undefined; + let primaryButton: string; + if (useTrash) { + errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?"); + detailMessage = nls.localize('irreversible', "This action is irreversible!"); + primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); + } else { + errorMessage = toErrorMessage(error, false); + primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry"); + } + + return dialogService.confirm({ + message: errorMessage, + detail: detailMessage, + type: 'warning', + primaryButton + }).then(res => { + + if (res.confirmed) { + if (useTrash) { + useTrash = false; // Delete Permanently + } + + skipConfirm = true; + + return deleteFiles(serviceAccesor, elements, useTrash, skipConfirm); + } + + return Promise.resolve(); + }); + }); + + return servicePromise; + }); + }); + }); +} + +function getMoveToTrashMessage(distinctElements: ExplorerItem[]): string { + if (containsBothDirectoryAndFile(distinctElements)) { + return getConfirmMessage(nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + } + + if (distinctElements.length > 1) { if (distinctElements[0].isDirectory) { - return nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name); + return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); } - return nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name); + return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); } - private getDeleteMessage(distinctElements: ExplorerItem[]): string { - if (this.containsBothDirectoryAndFile(distinctElements)) { - return getConfirmMessage(nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); - } + if (distinctElements[0].isDirectory) { + return nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name); + } - if (distinctElements.length > 1) { - if (distinctElements[0].isDirectory) { - return getConfirmMessage(nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); - } + return nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name); +} - return getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); - } +function getDeleteMessage(distinctElements: ExplorerItem[]): string { + if (containsBothDirectoryAndFile(distinctElements)) { + return getConfirmMessage(nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + } + if (distinctElements.length > 1) { if (distinctElements[0].isDirectory) { - return nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name); + return getConfirmMessage(nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); } - return nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name); + return getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); } - private containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean { - const directories = distinctElements.filter(element => element.isDirectory); - const files = distinctElements.filter(element => !element.isDirectory); - - return directories.length > 0 && files.length > 0; + if (distinctElements[0].isDirectory) { + return nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name); } + + return nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name); +} + +function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean { + const directories = distinctElements.filter(element => element.isDirectory); + const files = distinctElements.filter(element => !element.isDirectory); + + return directories.length > 0 && files.length > 0; } let pasteShouldMove = false; // Paste File/Folder -class PasteFileAction extends BaseErrorReportingAction { +class PasteFileAction extends Action { public static readonly ID = 'filesExplorer.paste'; constructor( private element: ExplorerItem, @IFileService private fileService: IFileService, - @INotificationService notificationService: INotificationService, + @INotificationService private notificationService: INotificationService, @IEditorService private readonly editorService: IEditorService, @IExplorerService private readonly explorerService: IExplorerService ) { - super(PasteFileAction.ID, PASTE_FILE_LABEL, notificationService); + super(PasteFileAction.ID, PASTE_FILE_LABEL); if (!this.element) { this.element = this.explorerService.roots[0]; @@ -528,9 +408,9 @@ class PasteFileAction extends BaseErrorReportingAction { } return undefined; - }, e => this.onError(e)); + }, e => onError(this.notificationService, e)); }, error => { - this.onError(new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile"))); + onError(this.notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile"))); }); } } @@ -701,7 +581,7 @@ export class ToggleAutoSaveAction extends Action { } } -export abstract class BaseSaveAllAction extends BaseErrorReportingAction { +export abstract class BaseSaveAllAction extends Action { private toDispose: IDisposable[]; private lastIsDirty: boolean; @@ -711,9 +591,9 @@ export abstract class BaseSaveAllAction extends BaseErrorReportingAction { @ITextFileService private readonly textFileService: ITextFileService, @IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService, @ICommandService protected commandService: ICommandService, - @INotificationService notificationService: INotificationService, + @INotificationService private notificationService: INotificationService, ) { - super(id, label, notificationService); + super(id, label); this.toDispose = []; this.lastIsDirty = this.textFileService.isDirty(); @@ -747,7 +627,7 @@ export abstract class BaseSaveAllAction extends BaseErrorReportingAction { public run(context?: any): Promise { return this.doRun(context).then(() => true, error => { - this.onError(error); + onError(this.notificationService, error); return false; }); } @@ -918,7 +798,7 @@ export class ShowOpenedFileInNewWindow extends Action { const fileResource = toResource(this.editorService.activeEditor, { supportSideBySide: true }); if (fileResource) { if (this.fileService.canHandleResource(fileResource)) { - this.windowService.openWindow([{ uri: fileResource, typeHint: 'file' }], { forceNewWindow: true, forceOpenWorkspaceAsFile: true }); + this.windowService.openWindow([{ fileUri: fileResource }], { forceNewWindow: true }); } else { this.notificationService.info(nls.localize('openFileToShowInNewWindow.unsupportedschema', "The active editor must contain an openable resource.")); } @@ -1002,7 +882,7 @@ export class CompareWithClipboardAction extends Action { private static readonly SCHEME = 'clipboardCompare'; - private registrationDisposal: IDisposable; + private registrationDisposal: IDisposable | undefined; constructor( id: string, @@ -1029,7 +909,8 @@ export class CompareWithClipboardAction extends Action { const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name); return this.editorService.openEditor({ leftResource: resource.with({ scheme: CompareWithClipboardAction.SCHEME }), rightResource: resource, label: editorLabel }).finally(() => { - this.registrationDisposal = dispose(this.registrationDisposal); + dispose(this.registrationDisposal); + this.registrationDisposal = undefined; }); } @@ -1039,7 +920,8 @@ export class CompareWithClipboardAction extends Action { public dispose(): void { super.dispose(); - this.registrationDisposal = dispose(this.registrationDisposal); + dispose(this.registrationDisposal); + this.registrationDisposal = undefined; } } @@ -1073,43 +955,82 @@ function getContext(listWidget: ListWidget): IExplorerContext { return { stat, selection: selection && typeof stat !== 'undefined' && selection.indexOf(stat) >= 0 ? selection : [] }; } -// TODO@isidor these commands are calling into actions due to the complex inheritance action structure. -// It should be the other way around, that actions call into commands. -function openExplorerAndRunAction(accessor: ServicesAccessor, constructor: IConstructorSignature1<() => ExplorerItem, Action>): Promise { - const instantiationService = accessor.get(IInstantiationService); +function onErrorWithRetry(notificationService: INotificationService, error: any, retry: () => Promise): void { + notificationService.prompt(Severity.Error, toErrorMessage(error, false), + [{ + label: nls.localize('retry', "Retry"), + run: () => retry() + }] + ); +} + +async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise { const listService = accessor.get(IListService); + const explorerService = accessor.get(IExplorerService); + const fileService = accessor.get(IFileService); + const editorService = accessor.get(IEditorService); const viewletService = accessor.get(IViewletService); const activeViewlet = viewletService.getActiveViewlet(); - let explorerPromise = Promise.resolve(activeViewlet); - if (!activeViewlet || activeViewlet.getId() !== VIEWLET_ID) { - explorerPromise = viewletService.openViewlet(VIEWLET_ID, true); + if (!activeViewlet || activeViewlet.getId() !== VIEWLET_ID || !listService.lastFocusedList) { + await viewletService.openViewlet(VIEWLET_ID, true); } - return explorerPromise.then((explorer: ExplorerViewlet) => { - const explorerView = explorer.getExplorerView(); - if (explorerView && explorerView.isBodyVisible() && listService.lastFocusedList) { - explorerView.focus(); - const { stat } = getContext(listService.lastFocusedList); - const action = instantiationService.createInstance(constructor, () => stat); - - return action.run(); + const list = listService.lastFocusedList; + if (list) { + const { stat } = getContext(list); + let folder: ExplorerItem; + if (stat) { + folder = stat.isDirectory ? stat : stat.parent!; + } else { + folder = explorerService.roots[0]; } - return undefined; - }); + if (folder.isReadonly) { + throw new Error('Parent folder is readonly.'); + } + + const newStat = new NewExplorerItem(folder, isFolder); + await folder.fetchChildren(fileService, explorerService); + + 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)); + return createPromise.then(created => { + refreshIfSeparator(value, explorerService); + return isFolder ? explorerService.select(created.resource, true) + : editorService.openEditor({ resource: created.resource, options: { pinned: true } }).then(() => undefined); + }, (error) => { + onErrorWithRetry(accessor.get(INotificationService), error, () => onSuccess(value)); + }); + }; + + explorerService.setEditable(newStat, { + validationMessage: value => validateFileName(newStat, value), + onFinish: (value, success) => { + folder.removeChild(newStat); + explorerService.setEditable(newStat, null); + if (success) { + onSuccess(value); + } else { + explorerService.select(folder.resource).then(undefined, onUnexpectedError); + } + } + }); + } } CommandsRegistry.registerCommand({ id: NEW_FILE_COMMAND_ID, handler: (accessor) => { - return openExplorerAndRunAction(accessor, NewFileAction); + openExplorerAndCreate(accessor, false).then(undefined, onUnexpectedError); } }); CommandsRegistry.registerCommand({ id: NEW_FOLDER_COMMAND_ID, handler: (accessor) => { - return openExplorerAndRunAction(accessor, NewFolderAction); + openExplorerAndCreate(accessor, true).then(undefined, onUnexpectedError); } }); @@ -1140,29 +1061,25 @@ export const renameHandler = (accessor: ServicesAccessor) => { }; export const moveFileToTrashHandler = (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); if (!listService.lastFocusedList) { return Promise.resolve(); } const explorerContext = getContext(listService.lastFocusedList); - const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; + const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!]; - const moveFileToTrashAction = instantiationService.createInstance(BaseDeleteFileAction, stats, true); - return moveFileToTrashAction.run(); + return deleteFiles(accessor, stats, true); }; export const deleteFileHandler = (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); const listService = accessor.get(IListService); if (!listService.lastFocusedList) { return Promise.resolve(); } const explorerContext = getContext(listService.lastFocusedList); - const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; + const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!]; - const deleteFileAction = instantiationService.createInstance(BaseDeleteFileAction, stats, false); - return deleteFileAction.run(); + return deleteFiles(accessor, stats, false); }; export const copyFileHandler = (accessor: ServicesAccessor) => { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 2b26a14c80..595e79b513 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -315,11 +315,6 @@ configurationRegistry.registerConfiguration({ ], 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) }, - 'files.useExperimentalFileWatcher': { - 'type': 'boolean', - 'default': false, - 'description': nls.localize('useExperimentalFileWatcher', "Use the new experimental file watcher.") - }, 'files.defaultLanguage': { 'type': 'string', 'description': nls.localize('defaultLanguage', "The default language mode that is assigned to new files.") diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 8c97d0514d..078704fc99 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -14,14 +14,14 @@ import { coalesce } from 'vs/base/common/arrays'; // Commands can get exeucted from a command pallete, from a context menu or from some list using a keybinding // To cover all these cases we need to properly compute the resource on which the command is being executed -export function getResourceForCommand(resource: URI | object, listService: IListService, editorService: IEditorService): URI | null { +export function getResourceForCommand(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): URI | null { if (URI.isUri(resource)) { return resource; } let list = listService.lastFocusedList; if (list && list.getHTMLElement() === document.activeElement) { - let focus: any; + let focus: unknown; if (list instanceof List) { const focused = list.getFocusedElements(); if (focused.length) { @@ -44,7 +44,7 @@ export function getResourceForCommand(resource: URI | object, listService: IList return editorService.activeEditor ? toResource(editorService.activeEditor, { supportSideBySide: true }) : null; } -export function getMultiSelectedResources(resource: URI | object, listService: IListService, editorService: IEditorService): Array { +export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): Array { const list = listService.lastFocusedList; if (list && list.getHTMLElement() === document.activeElement) { // Explorer diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 7b086699ab..a349f88803 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { Action, IAction } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut } from 'vs/workbench/contrib/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut, ExplorerResourceMoveableToTrash } from 'vs/workbench/contrib/files/common/files'; import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView } from 'vs/workbench/contrib/files/browser/fileActions'; import { toResource } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -39,7 +39,7 @@ import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemAc import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/browser/parts/views/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; @@ -49,6 +49,7 @@ import { isEqualOrParent } from 'vs/base/common/resources'; import { values } from 'vs/base/common/map'; import { first } from 'vs/base/common/arrays'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; @@ -61,6 +62,7 @@ export class ExplorerView extends ViewletPanel { private folderContext: IContextKey; private readonlyContext: IContextKey; private rootContext: IContextKey; + private resourceMoveableToTrash: IContextKey; // Refresh is needed on the initial explorer open private shouldRefresh = true; @@ -85,7 +87,8 @@ export class ExplorerView extends ViewletPanel { @ITelemetryService private readonly telemetryService: ITelemetryService, @IExplorerService private readonly explorerService: IExplorerService, @IStorageService private readonly storageService: IStorageService, - @IClipboardService private clipboardService: IClipboardService + @IClipboardService private clipboardService: IClipboardService, + @IFileService private readonly fileService: IFileService ) { super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); @@ -94,6 +97,7 @@ export class ExplorerView extends ViewletPanel { this.folderContext = ExplorerFolderContext.bindTo(contextKeyService); this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService); this.rootContext = ExplorerRootContext.bindTo(contextKeyService); + this.resourceMoveableToTrash = ExplorerResourceMoveableToTrash.bindTo(contextKeyService); const decorationProvider = new ExplorerDecorationsProvider(this.explorerService, contextService); decorationService.registerDecorationsProvider(decorationProvider); @@ -217,12 +221,8 @@ export class ExplorerView extends ViewletPanel { getActions(): IAction[] { const actions: Action[] = []; - const getFocus = () => { - const focus = this.tree.getFocus(); - return focus.length > 0 ? focus[0] : undefined; - }; - actions.push(this.instantiationService.createInstance(NewFileAction, getFocus)); - actions.push(this.instantiationService.createInstance(NewFolderAction, getFocus)); + actions.push(this.instantiationService.createInstance(NewFileAction)); + actions.push(this.instantiationService.createInstance(NewFolderAction)); actions.push(this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL)); actions.push(this.instantiationService.createInstance(CollapseAction, this.tree, true, 'explorer-action collapse-explorer')); @@ -267,7 +267,7 @@ export class ExplorerView extends ViewletPanel { private createTree(container: HTMLElement): void { this.filter = this.instantiationService.createInstance(FilesFilter); this.disposables.push(this.filter); - const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer); + const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this.disposables.push(explorerLabels); const updateWidth = (stat: ExplorerItem) => this.tree.updateWidth(stat); @@ -405,6 +405,14 @@ export class ExplorerView extends ViewletPanel { this.folderContext.set((isSingleFolder && !stat) || !!stat && stat.isDirectory); this.readonlyContext.set(!!stat && stat.isReadonly); this.rootContext.set(!stat || (stat && stat.isRoot)); + + if (stat) { + const enableTrash = this.configurationService.getValue().files.enableTrash; + const hasCapability = this.fileService.hasCapability(stat.resource, FileSystemProviderCapabilities.Trash); + this.resourceMoveableToTrash.set(enableTrash && hasCapability); + } else { + this.resourceMoveableToTrash.reset(); + } } // General methods @@ -462,7 +470,7 @@ export class ExplorerView extends ViewletPanel { } else { const rawViewState = this.storageService.get(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, StorageScope.WORKSPACE); if (rawViewState) { - viewState = JSON.parse(rawViewState) as IAsyncDataTreeViewState; + viewState = JSON.parse(rawViewState); } } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index fdeb56faab..f66537ac4e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -309,7 +309,7 @@ export class FilesFilter implements ITreeFilter { const excludesConfigCopy = deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods - this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression); + this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) }); }); return needsRefresh; @@ -334,7 +334,7 @@ export class FilesFilter implements ITreeFilter { } public dispose(): void { - this.workspaceFolderChangeListener = dispose(this.workspaceFolderChangeListener); + dispose(this.workspaceFolderChangeListener); } } @@ -735,7 +735,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move") }); } else { - confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult); + confirmPromise = Promise.resolve({ confirmed: true }); } return confirmPromise.then(res => { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 3dfaf616e3..36e110e1fe 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -25,7 +25,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list'; -import { ResourceLabels, IResourceLabel, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; +import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -212,7 +212,7 @@ export class OpenEditorsView extends ViewletPanel { if (this.listLabels) { this.listLabels.clear(); } - this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer); + this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this.list = this.instantiationService.createInstance(WorkbenchList, container, delegate, [ new EditorGroupRenderer(this.keybindingService, this.instantiationService), new OpenEditorRenderer(this.listLabels, this.instantiationService, this.keybindingService, this.configurationService) diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index 61c232b335..0726da889b 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -296,7 +296,7 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { super.dispose(); } - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; } diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index fe9660e44a..67af5d3cc8 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -70,7 +70,7 @@ export class ExplorerModel implements IDisposable { } dispose(): void { - this._listener = dispose(this._listener); + dispose(this._listener); } } @@ -244,7 +244,7 @@ export class ExplorerItem { } fetchChildren(fileService: IFileService, explorerService: IExplorerService): Promise { - let promise: Promise = Promise.resolve(undefined); + let promise: Promise = Promise.resolve(undefined); if (!this._isDirectoryResolved) { // Resolve metadata only when the mtime is needed since this can be expensive // Mtime is only used when the sort order is 'modified' diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index a322521942..fb9995f432 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -22,6 +22,7 @@ import { Schemas } from 'vs/base/common/network'; 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'; /** * Explorer viewlet id. @@ -67,30 +68,20 @@ export const IExplorerService = createDecorator('explorerServi /** * Context Keys to use with keybindings for the Explorer and Open Editors view */ -const explorerViewletVisibleId = 'explorerViewletVisible'; -const filesExplorerFocusId = 'filesExplorerFocus'; -const openEditorsVisibleId = 'openEditorsVisible'; -const openEditorsFocusId = 'openEditorsFocus'; -const explorerViewletFocusId = 'explorerViewletFocus'; -const explorerResourceIsFolderId = 'explorerResourceIsFolder'; -const explorerResourceReadonly = 'explorerResourceReadonly'; -const explorerResourceIsRootId = 'explorerResourceIsRoot'; -const explorerResourceCutId = 'explorerResourceCut'; - -export const ExplorerViewletVisibleContext = new RawContextKey(explorerViewletVisibleId, true); -export const ExplorerFolderContext = new RawContextKey(explorerResourceIsFolderId, false); -export const ExplorerResourceReadonlyContext = new RawContextKey(explorerResourceReadonly, false); +export const ExplorerViewletVisibleContext = new RawContextKey('explorerViewletVisible', true); +export const ExplorerFolderContext = new RawContextKey('explorerResourceIsFolder', false); +export const ExplorerResourceReadonlyContext = new RawContextKey('explorerResourceReadonly', false); export const ExplorerResourceNotReadonlyContext = ExplorerResourceReadonlyContext.toNegated(); -export const ExplorerRootContext = new RawContextKey(explorerResourceIsRootId, false); -export const ExplorerResourceCut = new RawContextKey(explorerResourceCutId, false); -export const FilesExplorerFocusedContext = new RawContextKey(filesExplorerFocusId, true); -export const OpenEditorsVisibleContext = new RawContextKey(openEditorsVisibleId, false); -export const OpenEditorsFocusedContext = new RawContextKey(openEditorsFocusId, true); -export const ExplorerFocusedContext = new RawContextKey(explorerViewletFocusId, true); +export const ExplorerRootContext = new RawContextKey('explorerResourceIsRoot', false); +export const ExplorerResourceCut = new RawContextKey('explorerResourceCut', false); +export const ExplorerResourceMoveableToTrash = new RawContextKey('explorerResourceMoveableToTrash', false); +export const FilesExplorerFocusedContext = new RawContextKey('filesExplorerFocus', true); +export const OpenEditorsVisibleContext = new RawContextKey('openEditorsVisible', false); +export const OpenEditorsFocusedContext = new RawContextKey('openEditorsFocus', true); +export const ExplorerFocusedContext = new RawContextKey('explorerViewletFocus', true); -export const OpenEditorsVisibleCondition = ContextKeyExpr.has(openEditorsVisibleId); -export const FilesExplorerFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(explorerViewletVisibleId), ContextKeyExpr.has(filesExplorerFocusId), ContextKeyExpr.not(InputFocusedContextKey)); -export const ExplorerFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(explorerViewletVisibleId), ContextKeyExpr.has(explorerViewletFocusId), ContextKeyExpr.not(InputFocusedContextKey)); +export const FilesExplorerFocusCondition = ContextKeyExpr.and(ExplorerViewletVisibleContext, FilesExplorerFocusedContext, ContextKeyExpr.not(InputFocusedContextKey)); +export const ExplorerFocusCondition = ContextKeyExpr.and(ExplorerViewletVisibleContext, ExplorerFocusedContext, ContextKeyExpr.not(InputFocusedContextKey)); /** * Text file editor id. @@ -141,7 +132,7 @@ export const SortOrderConfiguration = { export type SortOrder = 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified'; export class FileOnDiskContentProvider implements ITextModelContentProvider { - private fileWatcher: IDisposable; + private fileWatcherDisposable: IDisposable | undefined; constructor( @ITextFileService private readonly textFileService: ITextFileService, @@ -158,17 +149,17 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { return this.resolveEditorModel(resource).then(codeEditorModel => { // Make sure to keep contents on disk up to date when it changes - if (!this.fileWatcher) { - this.fileWatcher = this.fileService.onFileChanges(changes => { + if (!this.fileWatcherDisposable) { + this.fileWatcherDisposable = this.fileService.onFileChanges(changes => { if (changes.contains(fileOnDiskResource, FileChangeType.UPDATED)) { this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes } }); if (codeEditorModel) { - const disposeListener = codeEditorModel.onWillDispose(() => { - disposeListener.dispose(); - this.fileWatcher = dispose(this.fileWatcher); + once(codeEditorModel.onWillDispose)(() => { + dispose(this.fileWatcherDisposable); + this.fileWatcherDisposable = undefined; }); } } @@ -204,7 +195,8 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { } dispose(): void { - this.fileWatcher = dispose(this.fileWatcher); + dispose(this.fileWatcherDisposable); + this.fileWatcherDisposable = undefined; } } diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 610610c04b..08a664ada5 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -20,7 +20,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextModel } from 'vs/editor/common/model'; @@ -28,6 +28,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IModeService } from 'vs/editor/common/services/modeService'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; @@ -40,6 +41,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { constructor( @IExtensionService private readonly _extensionService: IExtensionService, + @IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService, @IConfigurationService private readonly _configService: IConfigurationService, @INotificationService private readonly _notificationService: INotificationService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @@ -83,18 +85,17 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { if (defaultFormatter) { // formatter available return defaultFormatter; + } - } else { - // formatter gone - const extension = await this._extensionService.getExtension(defaultFormatterId); + // bad -> formatter gone + const extension = await this._extensionService.getExtension(defaultFormatterId); + if (extension && this._extensionEnablementService.isEnabled(toExtension(extension))) { + // formatter does not target this file const label = this._labelService.getUriLabel(document.uri, { relative: true }); - const message = extension - ? nls.localize('miss', "Extension '{0}' cannot format '{1}'", extension.displayName || extension.name, label) - : nls.localize('gone', "Extension '{0}' is configured as formatter but not available", defaultFormatterId); + const message = nls.localize('miss', "Extension '{0}' cannot format '{1}'", extension.displayName || extension.name, label); this._statusbarService.setStatusMessage(message, 4000); return undefined; } - } else if (formatter.length === 1) { // ok -> nothing configured but only one formatter available return formatter[0]; @@ -102,7 +103,9 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId(); const silent = mode === FormattingMode.Silent; - const message = nls.localize('config.needed', "There are multiple formatters for {0}-files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName)); + const message = !defaultFormatterId + ? nls.localize('config.needed', "There are multiple formatters for {0}-files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName)) + : nls.localize('config.bad', "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", defaultFormatterId); return new Promise((resolve, reject) => { this._notificationService.prompt( diff --git a/src/vs/workbench/contrib/markers/browser/markersModel.ts b/src/vs/workbench/contrib/markers/browser/markersModel.ts index 877b89ccb8..866ea41b62 100644 --- a/src/vs/workbench/contrib/markers/browser/markersModel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersModel.ts @@ -7,7 +7,7 @@ import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IMarker, MarkerSeverity, IRelatedInformation, IMarkerData } from 'vs/platform/markers/common/markers'; -import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { isFalsyOrEmpty, mergeSort } from 'vs/base/common/arrays'; import { values } from 'vs/base/common/map'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; @@ -146,7 +146,7 @@ export class MarkersModel { if (isFalsyOrEmpty(rawMarkers)) { this.resourcesByUri.delete(resource.toString()); } else { - const markers = rawMarkers.map(rawMarker => { + const markers = mergeSort(rawMarkers.map(rawMarker => { let relatedInformation: RelatedInformation[] | undefined = undefined; if (rawMarker.relatedInformation) { @@ -154,9 +154,7 @@ export class MarkersModel { } return new Marker(rawMarker, relatedInformation); - }); - - markers.sort(compareMarkers); + }), compareMarkers); this.resourcesByUri.set(resource.toString(), new ResourceMarkers(resource, markers)); } diff --git a/src/vs/workbench/contrib/markers/browser/markersPanel.ts b/src/vs/workbench/contrib/markers/browser/markersPanel.ts index c41992654a..346939d3aa 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersPanel.ts @@ -43,6 +43,7 @@ import { domEvent } from 'vs/base/browser/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { IMarker } from 'vs/platform/markers/common/markers'; +import { withUndefinedAsNull } from 'vs/base/common/types'; function createModelIterator(model: MarkersModel): Iterator> { const resourcesIt = Iterator.fromArray(model.resourceMarkers); @@ -459,7 +460,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { private setCurrentActiveEditor(): void { const activeEditor = this.editorService.activeEditor; - this.currentActiveResource = activeEditor ? activeEditor.getResource() : null; + this.currentActiveResource = activeEditor ? withUndefinedAsNull(activeEditor.getResource()) : null; } private onSelected(): void { diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 7f1760a4e3..6c20daaa90 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -601,7 +601,7 @@ export class MarkerViewModel extends Disposable { return Promise.resolve(model); } if (waitForModel) { - if (this.modelPromise === null) { + if (!this.modelPromise) { this.modelPromise = createCancelablePromise(cancellationToken => { return new Promise((c) => { this._register(this.modelService.onModelAdded(model => { diff --git a/src/vs/workbench/contrib/output/common/outputLinkProvider.ts b/src/vs/workbench/contrib/output/common/outputLinkProvider.ts index 2fdece1d66..d8f7b6d4a3 100644 --- a/src/vs/workbench/contrib/output/common/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/common/outputLinkProvider.ts @@ -19,7 +19,7 @@ export class OutputLinkProvider { private worker?: MonacoWebWorker; private disposeWorkerScheduler: RunOnceScheduler; - private linkProviderRegistration: IDisposable; + private linkProviderRegistration: IDisposable | undefined; constructor( @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -48,7 +48,8 @@ export class OutputLinkProvider { }); } } else { - this.linkProviderRegistration = dispose(this.linkProviderRegistration); + dispose(this.linkProviderRegistration); + this.linkProviderRegistration = undefined; } // Dispose worker to recreate with folders on next provideLinks request diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts index a177ee14f8..8378284147 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { dirname, join, basename } from 'vs/base/common/path'; -import { del, exists, readdir, readFile } from 'vs/base/node/pfs'; +import { exists, readdir, readFile, rimraf } from 'vs/base/node/pfs'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -50,7 +50,7 @@ export class StartupProfiler implements IWorkbenchContribution { const removeArgs: string[] = ['--prof-startup']; const markerFile = readFile(profileFilenamePrefix).then(value => removeArgs.push(...value.toString().split('|'))) - .then(() => del(profileFilenamePrefix)) // (1) delete the file to tell the main process to stop profiling + .then(() => rimraf(profileFilenamePrefix)) // (1) delete the file to tell the main process to stop profiling .then(() => new Promise(resolve => { // (2) wait for main that recreates the fail to signal profiling has stopped const check = () => { exists(profileFilenamePrefix).then(exists => { @@ -63,7 +63,7 @@ export class StartupProfiler implements IWorkbenchContribution { }; check(); })) - .then(() => del(profileFilenamePrefix)); // (3) finally delete the file again + .then(() => rimraf(profileFilenamePrefix)); // (3) finally delete the file again markerFile.then(() => { return readdir(dir).then(files => files.filter(value => value.indexOf(prefix) === 0)); diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts b/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts index cd2e9e34dc..2a595ecc95 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupTimings.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { appendFile } from 'fs'; -import { nfcall, timeout } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; +import { promisify } from 'util'; import { onUnexpectedError } from 'vs/base/common/errors'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -59,7 +60,7 @@ export class StartupTimings implements IWorkbenchContribution { } private async _appendStartupTimes(isStandardStartup: boolean) { - let appendTo = this._envService.args['prof-append-timers']; + const appendTo = this._envService.args['prof-append-timers']; if (!appendTo) { // nothing to do return; @@ -71,7 +72,7 @@ export class StartupTimings implements IWorkbenchContribution { this._timerService.startupMetrics, timeout(15000), // wait: cached data creation, telemetry sending ]).then(([startupMetrics]) => { - return nfcall(appendFile, appendTo, `${startupMetrics.ellapsed}\t${product.nameShort}\t${(product.commit || '').slice(0, 10) || '0000000000'}\t${sessionId}\t${isStandardStartup ? 'standard_start' : 'NO_standard_start'}\n`); + return promisify(appendFile)(appendTo, `${startupMetrics.ellapsed}\t${product.nameShort}\t${(product.commit || '').slice(0, 10) || '0000000000'}\t${sessionId}\t${isStandardStartup ? 'standard_start' : 'NO_standard_start'}\n`); }).then(() => { this._windowsService.quit(); }).catch(err => { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index 1e4010191a..56385e912f 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -23,6 +23,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { SearchWidget, SearchOptions } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; +import { withNullAsUndefined } from 'vs/base/common/types'; export interface KeybindingsSearchOptions extends SearchOptions { recordEnter?: boolean; @@ -257,7 +258,7 @@ export class DefineKeybindingWidget extends Widget { this._chordPart = chordPart; dom.clearNode(this._outputNode); dom.clearNode(this._showExistingKeybindingsNode); - new KeybindingLabel(this._outputNode, OS).set(this._firstPart); + new KeybindingLabel(this._outputNode, OS).set(withNullAsUndefined(this._firstPart)); if (this._chordPart) { this._outputNode.appendChild(document.createTextNode(nls.localize('defineKeybinding.chordsTo', "chord to"))); new KeybindingLabel(this._outputNode, OS).set(this._chordPart); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 470ba29d3d..2363faf286 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -330,6 +330,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor this._register(this.searchWidget.onDidChange(searchValue => { clearInputAction.enabled = !!searchValue; this.delayedFiltering.trigger(() => this.filterKeybindings()); + this.updateSearchOptions(); })); this._register(this.searchWidget.onEscape(() => this.recordKeysAction.checked = false)); @@ -344,6 +345,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor if (e.checked !== undefined) { this.renderKeybindingsEntries(false); } + this.updateSearchOptions(); })); const recordKeysActionKeybinding = this.keybindingsService.lookupKeybinding(KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS); @@ -364,6 +366,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor this.searchWidget.stopRecordingKeys(); this.searchWidget.focus(); } + this.updateSearchOptions(); } })); @@ -383,6 +386,17 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor this.actionBar.push([this.recordKeysAction, this.sortByPrecedenceAction, clearInputAction], { label: false, icon: true }); } + private updateSearchOptions(): void { + const keybindingsEditorInput = this.input as KeybindingsEditorInput; + if (keybindingsEditorInput) { + keybindingsEditorInput.searchOptions = { + searchValue: this.searchWidget.getValue(), + recordKeybindings: !!this.recordKeysAction.checked, + sortByPrecedence: !!this.sortByPrecedenceAction.checked + }; + } + } + private createRecordingBadge(container: HTMLElement): HTMLElement { const recordingBadge = DOM.append(container, DOM.$('.recording-badge.disabled')); recordingBadge.textContent = localize('recording', "Recording Keys"); @@ -482,6 +496,13 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor }, {}); await this.keybindingsEditorModel.resolve(editorActionsLabels); this.renderKeybindingsEntries(false, preserveFocus); + if (input.searchOptions) { + this.recordKeysAction.checked = input.searchOptions.recordKeybindings; + this.sortByPrecedenceAction.checked = input.searchOptions.sortByPrecedence; + this.searchWidget.setValue(input.searchOptions.searchValue); + } else { + this.updateSearchOptions(); + } } } @@ -876,7 +897,7 @@ class ActionsColumn extends Column { } dispose(): void { - this.actionBar = dispose(this.actionBar); + dispose(this.actionBar); } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index f203182fe2..db3c6895dc 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -170,7 +170,7 @@ export class PreferencesEditor extends BaseEditor { this.sideBySidePreferencesWidget.layout(new DOM.Dimension(dimension.width, dimension.height - headerHeight)); } - getControl(): IEditorControl | null { + getControl(): IEditorControl | undefined { return this.sideBySidePreferencesWidget.getControl(); } @@ -890,8 +890,8 @@ class SideBySidePreferencesWidget extends Widget { } } - getControl(): IEditorControl | null { - return this.editablePreferencesEditor ? this.editablePreferencesEditor.getControl() : null; + getControl(): IEditorControl | undefined { + return this.editablePreferencesEditor ? this.editablePreferencesEditor.getControl() : undefined; } clearInput(): void { diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index f547ea3963..5453db8b2f 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -53,7 +53,7 @@ export class PreferencesContribution implements IWorkbenchContribution { private handleSettingsEditorOverride(): void { // dispose any old listener we had - this.editorOpeningListener = dispose(this.editorOpeningListener); + dispose(this.editorOpeningListener); // install editor opening listener unless user has disabled this if (!!this.configurationService.getValue(USE_SPLIT_JSON_SETTING)) { @@ -144,7 +144,7 @@ export class PreferencesContribution implements IWorkbenchContribution { } dispose(): void { - this.editorOpeningListener = dispose(this.editorOpeningListener); - this.settingsListener = dispose(this.settingsListener); + dispose(this.editorOpeningListener); + dispose(this.settingsListener); } } diff --git a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts index ffd4a1ff7e..ee2adb178c 100644 --- a/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/commandsHandler.ts @@ -19,7 +19,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; -import { registerEditorAction, EditorAction, IEditorCommandMenuOptions } from 'vs/editor/browser/editorExtensions'; +import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { LRUCache } from 'vs/base/common/map'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -145,7 +145,7 @@ export class ShowAllCommandsAction extends Action { super(id, label); } - run(context?: any): Promise { + run(): Promise { const config = this.configurationService.getValue(); const restoreInput = config.workbench && config.workbench.commandPalette && config.workbench.commandPalette.preserveInput === true; @@ -174,7 +174,7 @@ export class ClearCommandHistoryAction extends Action { super(id, label); } - run(context?: any): Promise { + run(): Promise { const commandHistoryLength = resolveCommandHistory(this.configurationService); if (commandHistoryLength > 0) { commandHistory = new LRUCache(commandHistoryLength); @@ -196,7 +196,7 @@ class CommandPaletteEditorAction extends EditorAction { menuOpts: { group: 'z_commands', order: 1 - } as IEditorCommandMenuOptions + } }); } 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 b601ed8153..f60c2f0d86 100644 --- a/src/vs/workbench/contrib/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/electron-browser/relauncher.contribution.ts @@ -18,14 +18,12 @@ 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 { equals } from 'vs/base/common/objects'; interface IConfiguration extends IWindowsConfiguration { update: { mode: string; }; telemetry: { enableCrashReporter: boolean }; keyboard: { touchbar: { enabled: boolean } }; workbench: { list: { horizontalScrolling: boolean }, useExperimentalGridLayout: boolean }; - files: { useExperimentalFileWatcher: boolean, watcherExclude: object }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { @@ -38,8 +36,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private enableCrashReporter: boolean; private touchbarEnabled: boolean; private treeHorizontalScrolling: boolean; - private experimentalFileWatcher: boolean; - private fileWatcherExclude: object; private useGridLayout: boolean; constructor( @@ -47,8 +43,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo @IWindowService private readonly windowService: IWindowService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEnvironmentService private readonly envService: IEnvironmentService, - @IDialogService private readonly dialogService: IDialogService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IDialogService private readonly dialogService: IDialogService ) { super(); @@ -96,20 +91,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo changed = true; } - // Experimental File Watcher - if (config.files && typeof config.files.useExperimentalFileWatcher === 'boolean' && config.files.useExperimentalFileWatcher !== this.experimentalFileWatcher) { - this.experimentalFileWatcher = config.files.useExperimentalFileWatcher; - changed = true; - } - - // File Watcher Excludes (only if in folder workspace mode) - if (!this.experimentalFileWatcher && this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { - if (config.files && typeof config.files.watcherExclude === 'object' && !equals(config.files.watcherExclude, this.fileWatcherExclude)) { - this.fileWatcherExclude = config.files.watcherExclude; - changed = true; - } - } - // macOS: Touchbar config if (isMacintosh && config.keyboard && config.keyboard.touchbar && typeof config.keyboard.touchbar.enabled === 'boolean' && config.keyboard.touchbar.enabled !== this.touchbarEnabled) { this.touchbarEnabled = config.keyboard.touchbar.enabled; @@ -164,7 +145,7 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor private firstFolderResource?: URI; private extensionHostRestarter: RunOnceScheduler; - private onDidChangeWorkspaceFoldersUnbind: IDisposable; + private onDidChangeWorkspaceFoldersUnbind: IDisposable | undefined; constructor( @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -216,7 +197,8 @@ export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWor // Ignore the workspace folder changes in EMPTY or FOLDER state else { - this.onDidChangeWorkspaceFoldersUnbind = dispose(this.onDidChangeWorkspaceFoldersUnbind); + dispose(this.onDidChangeWorkspaceFoldersUnbind); + this.onDidChangeWorkspaceFoldersUnbind = undefined; } } diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 69a28c6ee1..4883f834bd 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -32,6 +32,7 @@ display: flex; align-items: center; flex-wrap: wrap; + height: 100%; } .scm-viewlet .monaco-list-row > .scm-provider > .monaco-action-bar { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index df08c1c32a..7b5e2d0aa8 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -15,7 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListEvent, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { VIEWLET_ID, ISCMService, ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType, VIEW_CONTAINER } from 'vs/workbench/contrib/scm/common/scm'; -import { ResourceLabels, IResourceLabel, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; +import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -829,7 +829,7 @@ export class RepositoryPanel extends ViewletPanel { const actionItemProvider = (action: IAction) => this.getActionItem(action); - this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer); + this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this.disposables.push(this.listLabels); const renderers = [ diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 6b52729679..66a5013973 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -361,7 +361,7 @@ const searchInFolderCommand: ICommandHandler = (accessor, resource?: URI) => { const panelService = accessor.get(IPanelService); const fileService = accessor.get(IFileService); const configurationService = accessor.get(IConfigurationService); - const resources = resource && getMultiSelectedResources(resource, listService, accessor.get(IEditorService)); + const resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService)); return openSearchView(viewletService, panelService, configurationService, true).then(searchView => { if (resources && resources.length && searchView) { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 43eaa5e4f4..58761037f7 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -40,7 +40,7 @@ import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, edi import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; +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'; @@ -625,7 +625,7 @@ export class SearchView extends ViewletPanel { } }; - this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer)); + this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility })); this.tree = this._register(>this.instantiationService.createInstance(WorkbenchObjectTree, this.resultsElement, delegate, diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 51d6f2417e..0391626e0a 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -114,20 +114,16 @@ namespace snippetExt { } function watch(service: IFileService, resource: URI, callback: (type: FileChangeType, resource: URI) => any): IDisposable { - let listener = service.onFileChanges(e => { - for (const change of e.changes) { - if (resources.isEqualOrParent(change.resource, resource)) { - callback(change.type, change.resource); + return combinedDisposable([ + service.watch(resource), + service.onFileChanges(e => { + for (const change of e.changes) { + if (resources.isEqualOrParent(change.resource, resource)) { + callback(change.type, change.resource); + } } - } - }); - service.watch(resource); - return { - dispose() { - listener.dispose(); - service.unwatch(resource); - } - }; + }) + ]); } class SnippetsService implements ISnippetsService { diff --git a/src/vs/workbench/contrib/stats/node/workspaceStats.ts b/src/vs/workbench/contrib/stats/node/workspaceStats.ts index 231cd415e8..98ddf9c7ab 100644 --- a/src/vs/workbench/contrib/stats/node/workspaceStats.ts +++ b/src/vs/workbench/contrib/stats/node/workspaceStats.ts @@ -558,7 +558,7 @@ export class WorkspaceStats implements IWorkbenchContribution { this.notificationService.prompt(Severity.Info, localize('workspaceFound', "This folder contains a workspace file '{0}'. Do you want to open it? [Learn more]({1}) about workspace files.", workspaceFile, 'https://go.microsoft.com/fwlink/?linkid=2025315'), [{ label: localize('openWorkspace', "Open Workspace"), - run: () => this.windowService.openWindow([{ uri: joinPath(folder, workspaceFile), typeHint: 'file' }]) + run: () => this.windowService.openWindow([{ workspaceUri: joinPath(folder, workspaceFile) }]) }, doNotShowAgain]); } @@ -571,7 +571,7 @@ export class WorkspaceStats implements IWorkbenchContribution { workspaces.map(workspace => ({ label: workspace } as IQuickPickItem)), { placeHolder: localize('selectToOpen', "Select a workspace to open") }).then(pick => { if (pick) { - this.windowService.openWindow([{ uri: joinPath(folder, pick.label), typeHint: 'file' }]); + this.windowService.openWindow([{ workspaceUri: joinPath(folder, pick.label) }]); } }); } diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index dd1fd246e6..0012a92f5d 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -534,7 +534,7 @@ interface MetaData { } -function _isEmpty(this: void, value: T, properties: MetaData[] | undefined): boolean { +function _isEmpty(this: void, value: T | undefined, properties: MetaData[] | undefined): boolean { if (value === undefined || value === null || properties === undefined) { return true; } @@ -551,11 +551,11 @@ function _isEmpty(this: void, value: T, properties: MetaData[] | unde return true; } -function _assignProperties(this: void, target: T, source: T, properties: MetaData[]): T { - if (_isEmpty(source, properties)) { +function _assignProperties(this: void, target: T | undefined, source: T | undefined, properties: MetaData[]): T | undefined { + if (!source || _isEmpty(source, properties)) { return target; } - if (_isEmpty(target, properties)) { + if (!target || _isEmpty(target, properties)) { return source; } for (let meta of properties) { @@ -573,11 +573,11 @@ function _assignProperties(this: void, target: T, source: T, properties: Meta return target; } -function _fillProperties(this: void, target: T, source: T, properties: MetaData[] | undefined): T { - if (_isEmpty(source, properties)) { +function _fillProperties(this: void, target: T | undefined, source: T | undefined, properties: MetaData[] | undefined): T | undefined { + if (!source || _isEmpty(source, properties)) { return target; } - if (_isEmpty(target, properties)) { + if (!target || _isEmpty(target, properties)) { return source; } for (let meta of properties!) { @@ -595,11 +595,11 @@ function _fillProperties(this: void, target: T, source: T, properties: MetaDa return target; } -function _fillDefaults(this: void, target: T, defaults: T, properties: MetaData[], context: ParseContext): T | undefined { +function _fillDefaults(this: void, target: T | undefined, defaults: T | undefined, properties: MetaData[], context: ParseContext): T | undefined { if (target && Object.isFrozen(target)) { return target; } - if (target === undefined || target === null) { + if (target === undefined || target === null || defaults === undefined || defaults === null) { if (defaults !== undefined && defaults !== null) { return Objects.deepClone(defaults); } else { @@ -715,7 +715,7 @@ namespace ShellConfiguration { return _assignProperties(target, source, properties); } - export function fillProperties(this: void, target: Tasks.ShellConfiguration, source: Tasks.ShellConfiguration): Tasks.ShellConfiguration { + export function fillProperties(this: void, target: Tasks.ShellConfiguration, source: Tasks.ShellConfiguration): Tasks.ShellConfiguration | undefined { return _fillProperties(target, source, properties); } @@ -1016,7 +1016,7 @@ namespace CommandConfiguration { return target; } - export function fillProperties(target: Tasks.CommandConfiguration, source: Tasks.CommandConfiguration): Tasks.CommandConfiguration { + export function fillProperties(target: Tasks.CommandConfiguration, source: Tasks.CommandConfiguration): Tasks.CommandConfiguration | undefined { return _fillProperties(target, source, properties); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index fc7b5d53e5..2a42147d5a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -263,6 +263,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('terminal.integrated.windowsEnableConpty', "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false."), type: 'boolean', default: true + }, + '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', + default: false } } }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index c5f80bdbb7..ccd5c4e1f3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -765,6 +765,14 @@ export class TerminalInstance implements ITerminalInstance { } public forceRedraw(): void { + if (this._configHelper.config.experimentalRefreshOnResume) { + if (this._xterm.getOption('rendererType') !== 'dom') { + this._xterm.setOption('rendererType', 'dom'); + // Do this asynchronously to clear our the texture atlas as all terminals will not + // be using canvas + setTimeout(() => this._xterm.setOption('rendererType', 'canvas'), 0); + } + } this._xterm.refresh(0, this._xterm.rows - 1); } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index d10e51219e..44158db7cb 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; + experimentalRefreshOnResume: boolean; } export interface ITerminalConfigHelper { diff --git a/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts index 34a5679f9d..92245dd849 100644 --- a/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts @@ -20,9 +20,9 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IRequestService } from 'vs/platform/request/node/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils'; -import { IWebviewEditorService } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorService'; +import { IWebviewEditorService } from 'vs/workbench/contrib/webview/browser/webviewEditorService'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { WebviewEditorInput } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInput'; +import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js b/src/vs/workbench/contrib/webview/browser/pre/main.js similarity index 70% rename from src/vs/workbench/contrib/webview/electron-browser/webview-pre.js rename to src/vs/workbench/contrib/webview/browser/pre/main.js index a704462b01..9e5fda5f0f 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -3,56 +3,104 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // @ts-check -(function () { - 'use strict'; +'use strict'; - // @ts-ignore - const ipcRenderer = require('electron').ipcRenderer; +/** + * Use polling to track focus of main webview and iframes within the webview + * + * @param {Object} handlers + * @param {() => void} handlers.onFocus + * @param {() => void} handlers.onBlur + */ +const trackFocus = ({ onFocus, onBlur }) => { + const interval = 50; + let isFocused = document.hasFocus(); + setInterval(() => { + const isCurrentlyFocused = document.hasFocus(); + if (isCurrentlyFocused === isFocused) { + return; + } + isFocused = isCurrentlyFocused; + if (isCurrentlyFocused) { + onFocus(); + } else { + onBlur(); + } + }, interval); +}; - const registerVscodeResourceScheme = (function () { - let hasRegistered = false; - return () => { - if (hasRegistered) { - return; - } +const getActiveFrame = () => { + return /** @type {HTMLIFrameElement} */ (document.getElementById('active-frame')); +}; - hasRegistered = true; +const getPendingFrame = () => { + return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame')); +}; - // @ts-ignore - require('electron').webFrame.registerURLSchemeAsPrivileged('vscode-resource', { - secure: true, - bypassCSP: false, - allowServiceWorkers: false, - supportFetchAPI: true, - corsEnabled: true - }); - }; - }()); +const defaultCssRules = ` + body { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); + font-size: var(--vscode-editor-font-size); + margin: 0; + padding: 0 20px; + } - /** - * Use polling to track focus of main webview and iframes within the webview - * - * @param {Object} handlers - * @param {() => void} handlers.onFocus - * @param {() => void} handlers.onBlur - */ - const trackFocus = ({ onFocus, onBlur }) => { - const interval = 50; - let isFocused = document.hasFocus(); - setInterval(() => { - const isCurrentlyFocused = document.hasFocus(); - if (isCurrentlyFocused === isFocused) { - return; - } - isFocused = isCurrentlyFocused; - if (isCurrentlyFocused) { - onFocus(); - } else { - onBlur(); - } - }, interval); - }; + img { + max-width: 100%; + max-height: 100%; + } + a { + color: var(--vscode-textLink-foreground); + } + + a:hover { + color: var(--vscode-textLink-activeForeground); + } + + a:focus, + input:focus, + select:focus, + textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; + } + + code { + color: var(--vscode-textPreformat-foreground); + } + + blockquote { + background: var(--vscode-textBlockQuote-background); + border-color: var(--vscode-textBlockQuote-border); + } + + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar-thumb { + background-color: var(--vscode-scrollbarSlider-background); + } + ::-webkit-scrollbar-thumb:hover { + background-color: var(--vscode-scrollbarSlider-hoverBackground); + } + ::-webkit-scrollbar-thumb:active { + background-color: var(--vscode-scrollbarSlider-activeBackground); + }`; + +/** + * @typedef {{ postMessage: (channel: string, data?: any) => void, onMessage: (channel: string, handler: any) => void }} HostCommunications + */ + +/** + * @param {HostCommunications} host + */ +module.exports = function createWebviewManager(host) { // state let firstLoad = true; let loadTimeout; @@ -82,14 +130,6 @@ } }; - const getActiveFrame = () => { - return /** @type {HTMLIFrameElement} */ (document.getElementById('active-frame')); - }; - - const getPendingFrame = () => { - return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame')); - }; - /** * @param {MouseEvent} event */ @@ -111,7 +151,7 @@ scrollTarget.scrollIntoView(); } } else { - ipcRenderer.sendToHost('did-click-link', node.href); + host.postMessage('did-click-link', node.href); } event.preventDefault(); break; @@ -124,7 +164,7 @@ * @param {KeyboardEvent} e */ const handleInnerKeydown = (e) => { - ipcRenderer.sendToHost('did-keydown', { + host.postMessage('did-keydown', { key: e.key, keyCode: e.keyCode, code: e.code, @@ -137,7 +177,7 @@ }; const onMessage = (message) => { - ipcRenderer.sendToHost(message.data.command, message.data.data); + host.postMessage(message.data.command, message.data.data); }; let isHandlingScroll = false; @@ -154,7 +194,7 @@ isHandlingScroll = true; window.requestAnimationFrame(() => { try { - ipcRenderer.sendToHost('did-scroll', progress); + host.postMessage('did-scroll', progress); } catch (e) { // noop } @@ -163,11 +203,7 @@ }; document.addEventListener('DOMContentLoaded', () => { - ipcRenderer.on('baseUrl', (_event, value) => { - initData.baseUrl = value; - }); - - ipcRenderer.on('styles', (_event, variables, activeTheme) => { + host.onMessage('styles', (_event, variables, activeTheme) => { initData.styles = variables; initData.activeTheme = activeTheme; @@ -180,7 +216,7 @@ }); // propagate focus - ipcRenderer.on('focus', () => { + host.onMessage('focus', () => { const target = getActiveFrame(); if (target) { target.contentWindow.focus(); @@ -188,11 +224,9 @@ }); // update iframe-contents - ipcRenderer.on('content', (_event, data) => { + host.onMessage('content', (_event, data) => { const options = data.options; - registerVscodeResourceScheme(); - const text = data.contents; const newDocument = new DOMParser().parseFromString(text, 'text/html'); @@ -202,13 +236,6 @@ } }); - // set base-url if applicable - if (initData.baseUrl && newDocument.head.getElementsByTagName('base').length === 0) { - const baseElement = newDocument.createElement('base'); - baseElement.href = initData.baseUrl; - newDocument.head.appendChild(baseElement); - } - // apply default script if (options.allowScripts) { const defaultScript = newDocument.createElement('script'); @@ -299,7 +326,7 @@ newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); newFrame.contentWindow.onbeforeunload = () => { if (isInDevelopmentMode) { // Allow reloads while developing a webview - ipcRenderer.sendToHost('do-reload'); + host.postMessage('do-reload'); return false; } @@ -361,11 +388,11 @@ newFrame.contentDocument.write(newDocument.documentElement.innerHTML); newFrame.contentDocument.close(); - ipcRenderer.sendToHost('did-set-content'); + host.postMessage('did-set-content', undefined); }); // Forward message to the embedded iframe - ipcRenderer.on('message', (_event, data) => { + host.onMessage('message', (_event, data) => { const pending = getPendingFrame(); if (!pending) { const target = getActiveFrame(); @@ -377,79 +404,23 @@ pendingMessages.push(data); }); - ipcRenderer.on('initial-scroll-position', (_event, progress) => { + host.onMessage('initial-scroll-position', (_event, progress) => { initData.initialScrollProgress = progress; }); - ipcRenderer.on('devtools-opened', () => { + host.onMessage('devtools-opened', () => { isInDevelopmentMode = true; }); trackFocus({ - onFocus: () => { ipcRenderer.sendToHost('did-focus'); }, - onBlur: () => { ipcRenderer.sendToHost('did-blur'); } + onFocus: () => host.postMessage('did-focus'), + onBlur: () => host.postMessage('did-blur') }); // Forward messages from the embedded iframe window.onmessage = onMessage; // signal ready - ipcRenderer.sendToHost('webview-ready', process.pid); + host.postMessage('webview-ready', process.pid); }); - - const defaultCssRules = ` - body { - background-color: var(--vscode-editor-background); - color: var(--vscode-editor-foreground); - font-family: var(--vscode-editor-font-family); - font-weight: var(--vscode-editor-font-weight); - font-size: var(--vscode-editor-font-size); - margin: 0; - padding: 0 20px; - } - - img { - max-width: 100%; - max-height: 100%; - } - - a { - color: var(--vscode-textLink-foreground); - } - - a:hover { - color: var(--vscode-textLink-activeForeground); - } - - a:focus, - input:focus, - select:focus, - textarea:focus { - outline: 1px solid -webkit-focus-ring-color; - outline-offset: -1px; - } - - code { - color: var(--vscode-textPreformat-foreground); - } - - blockquote { - background: var(--vscode-textBlockQuote-background); - border-color: var(--vscode-textBlockQuote-border); - } - - ::-webkit-scrollbar { - width: 10px; - height: 10px; - } - - ::-webkit-scrollbar-thumb { - background-color: var(--vscode-scrollbarSlider-background); - } - ::-webkit-scrollbar-thumb:hover { - background-color: var(--vscode-scrollbarSlider-hoverBackground); - } - ::-webkit-scrollbar-thumb:active { - background-color: var(--vscode-scrollbarSlider-activeBackground); - }`; -}()); \ No newline at end of file +}; diff --git a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts new file mode 100644 index 0000000000..f05591890d --- /dev/null +++ b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { isMacintosh } from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; +import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/contrib/webview/common/webview'; +import { CopyWebviewEditorCommand, CutWebviewEditorCommand, HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, PasteWebviewEditorCommand, RedoWebviewEditorCommand, ReloadWebviewAction, SelectAllWebviewEditorCommand, ShowWebViewEditorFindWidgetCommand, UndoWebviewEditorCommand } from '../browser/webviewCommands'; +import { WebviewEditor } from '../browser/webviewEditor'; +import { WebviewEditorInput } from '../browser/webviewEditorInput'; +import { IWebviewEditorService, WebviewEditorService } from '../browser/webviewEditorService'; + +(Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor( + WebviewEditor, + WebviewEditor.ID, + localize('webview.editor.label', "webview editor")), + [new SyncDescriptor(WebviewEditorInput)]); + +Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( + WebviewEditorInputFactory.ID, + WebviewEditorInputFactory); + +registerSingleton(IWebviewEditorService, WebviewEditorService, true); + + +const webviewDeveloperCategory = localize('developer', "Developer"); + +const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); + +export function registerWebViewCommands(editorId: string): void { + const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */); + + const showNextFindWidgetCommand = new ShowWebViewEditorFindWidgetCommand({ + id: ShowWebViewEditorFindWidgetCommand.ID, + precondition: contextKeyExpr, + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_F, + weight: KeybindingWeight.EditorContrib + } + }); + showNextFindWidgetCommand.register(); + + (new HideWebViewEditorFindCommand({ + id: HideWebViewEditorFindCommand.ID, + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE), + kbOpts: { + primary: KeyCode.Escape, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new SelectAllWebviewEditorCommand({ + id: SelectAllWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_A, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + // These commands are only needed on MacOS where we have to disable the menu bar commands + if (isMacintosh) { + (new CopyWebviewEditorCommand({ + id: CopyWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new PasteWebviewEditorCommand({ + id: PasteWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_V, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + + (new CutWebviewEditorCommand({ + id: CutWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_X, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new UndoWebviewEditorCommand({ + id: UndoWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new RedoWebviewEditorCommand({ + id: RedoWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z }, + weight: KeybindingWeight.EditorContrib + } + })).register(); + } +} + +registerWebViewCommands(WebviewEditor.ID); + +actionRegistry.registerWorkbenchAction( + new SyncActionDescriptor(OpenWebviewDeveloperToolsAction, OpenWebviewDeveloperToolsAction.ID, OpenWebviewDeveloperToolsAction.LABEL), + 'Webview Tools', + webviewDeveloperCategory); + +actionRegistry.registerWorkbenchAction( + new SyncActionDescriptor(ReloadWebviewAction, ReloadWebviewAction.ID, ReloadWebviewAction.LABEL), + 'Reload Webview', + webviewDeveloperCategory); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts similarity index 98% rename from src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts rename to src/vs/workbench/contrib/webview/browser/webviewCommands.ts index c96c8969ba..6cada72933 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts @@ -8,7 +8,7 @@ import { Command } from 'vs/editor/browser/editorExtensions'; import * as nls from 'vs/nls'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { WebviewEditor } from 'vs/workbench/contrib/webview/electron-browser/webviewEditor'; +import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; export class ShowWebViewEditorFindWidgetCommand extends Command { public static readonly ID = 'editor.action.webvieweditor.showFind'; diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts similarity index 84% rename from src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts rename to src/vs/workbench/contrib/webview/browser/webviewEditor.ts index 452f763fb4..eb035a02e6 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -8,8 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -17,19 +16,15 @@ import { IWindowService } from 'vs/platform/windows/common/windows'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { WebviewEditorInput } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInput'; +import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; +import { IWebviewService, Webview, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/contrib/webview/common/webview'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; -import { WebviewElement } from './webviewElement'; - -/** A context key that is set when the find widget in a webview is visible. */ -export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey('webviewFindWidgetVisible', false); export class WebviewEditor extends BaseEditor { - protected _webview: WebviewElement | undefined; + protected _webview: Webview | undefined; protected findWidgetVisible: IContextKey; public static readonly ID = 'WebviewEditor'; @@ -50,9 +45,8 @@ export class WebviewEditor extends BaseEditor { @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService private _contextKeyService: IContextKeyService, - @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, + @IWebviewService private readonly _webviewService: IWebviewService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEditorService private readonly _editorService: IEditorService, @IWindowService private readonly _windowService: IWindowService, @IStorageService storageService: IStorageService @@ -130,11 +124,11 @@ export class WebviewEditor extends BaseEditor { } public reload() { - this.withWebviewElement(webview => webview.reload()); + this.withWebview(webview => webview.reload()); } public layout(_dimension: DOM.Dimension): void { - this.withWebviewElement(webview => { + this.withWebview(webview => { this.doUpdateContainer(); webview.layout(); }); @@ -151,34 +145,34 @@ export class WebviewEditor extends BaseEditor { } }); } - this.withWebviewElement(webview => webview.focus()); + this.withWebview(webview => webview.focus()); } public selectAll(): void { - this.withWebviewElement(webview => webview.selectAll()); + this.withWebview(webview => webview.selectAll()); } public copy(): void { - this.withWebviewElement(webview => webview.copy()); + this.withWebview(webview => webview.copy()); } public paste(): void { - this.withWebviewElement(webview => webview.paste()); + this.withWebview(webview => webview.paste()); } public cut(): void { - this.withWebviewElement(webview => webview.cut()); + this.withWebview(webview => webview.cut()); } public undo(): void { - this.withWebviewElement(webview => webview.undo()); + this.withWebview(webview => webview.undo()); } public redo(): void { - this.withWebviewElement(webview => webview.redo()); + this.withWebview(webview => webview.redo()); } - private withWebviewElement(f: (element: WebviewElement) => void): void { + private withWebview(f: (element: Webview) => void): void { if (this._webview) { f(this._webview); } @@ -264,7 +258,7 @@ export class WebviewEditor extends BaseEditor { return rootPaths; } - private getWebview(input: WebviewEditorInput): WebviewElement { + private getWebview(input: WebviewEditorInput): Webview { if (this._webview) { return this._webview; } @@ -279,14 +273,12 @@ export class WebviewEditor extends BaseEditor { this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService); } - this._webview = this._instantiationService.createInstance(WebviewElement, - this._layoutService.getContainer(Parts.EDITOR_PART), + this._webview = this._webviewService.createWebview( { allowSvgs: true, extension: input.extension, enableFindWidget: input.options.enableFindWidget - }, - {}); + }, {}); this._webview.mountTo(this._webviewContent); input.webview = this._webview; diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts similarity index 91% rename from src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts rename to src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index 2376ad4d7b..16612e4235 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -11,7 +11,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { EditorInput, EditorModel, GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { WebviewEvents, WebviewInputOptions } from './webviewEditorService'; -import { WebviewElement, WebviewOptions } from './webviewElement'; +import { Webview, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview'; export class WebviewEditorInput extends EditorInput { private static handlePool = 0; @@ -57,7 +57,7 @@ export class WebviewEditorInput extends EditorInput { private _currentWebviewHtml: string = ''; public _events: WebviewEvents | undefined; private _container?: HTMLElement; - private _webview: WebviewElement | undefined; + private _webview?: Webview; private _webviewOwner: any; private _webviewDisposables: IDisposable[] = []; private _group?: GroupIdentifier; @@ -72,7 +72,6 @@ export class WebviewEditorInput extends EditorInput { constructor( public readonly viewType: string, - id: number | undefined, name: string, options: WebviewInputOptions, state: any, @@ -85,12 +84,7 @@ export class WebviewEditorInput extends EditorInput { ) { super(); - if (typeof id === 'number') { - this._id = id; - WebviewEditorInput.handlePool = Math.max(id, WebviewEditorInput.handlePool) + 1; - } else { - this._id = WebviewEditorInput.handlePool++; - } + this._id = WebviewEditorInput.handlePool++; this._name = name; this._options = options; @@ -103,10 +97,6 @@ export class WebviewEditorInput extends EditorInput { return WebviewEditorInput.typeId; } - public getId(): number { - return this._id; - } - private readonly _onDidChangeIcon = this._register(new Emitter()); public readonly onDidChangeIcon = this._onDidChangeIcon.event; @@ -161,7 +151,7 @@ export class WebviewEditorInput extends EditorInput { } public matches(other: IEditorInput): boolean { - return other === this || (other instanceof WebviewEditorInput && other._id === this._id); + return other === this; } public get group(): GroupIdentifier | undefined { @@ -234,11 +224,11 @@ export class WebviewEditorInput extends EditorInput { return this._container; } - public get webview(): WebviewElement | undefined { + public get webview(): Webview | undefined { return this._webview; } - public set webview(value: WebviewElement | undefined) { + public set webview(value: Webview | undefined) { this._webviewDisposables = dispose(this._webviewDisposables); this._webview = value; @@ -272,6 +262,7 @@ export class WebviewEditorInput extends EditorInput { } public claimWebview(owner: any) { + this._webviewOwner = owner; } @@ -315,7 +306,6 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput { constructor( viewType: string, - id: number | undefined, name: string, options: WebviewInputOptions, state: any, @@ -324,10 +314,10 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput { readonly location: URI; readonly id: ExtensionIdentifier }, - public readonly reviver: (input: WebviewEditorInput) => Promise, + private readonly reviver: (input: WebviewEditorInput) => Promise, @IWorkbenchLayoutService partService: IWorkbenchLayoutService, ) { - super(viewType, id, name, options, state, events, extension, partService); + super(viewType, name, options, state, events, extension, partService); } public async resolve(): Promise { diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts similarity index 96% rename from src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts rename to src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts index 2e42caed64..e76ea4b799 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts @@ -17,7 +17,6 @@ interface SerializedIconPath { interface SerializedWebview { readonly viewType: string; - readonly id: number; readonly title: string; readonly options: WebviewInputOptions; readonly extensionLocation: string | UriComponents | undefined; @@ -44,7 +43,6 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { const data: SerializedWebview = { viewType: input.viewType, - id: input.getId(), title: input.getName(), options: input.options, extensionLocation: input.extension ? input.extension.location : undefined, @@ -69,7 +67,7 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { const extensionLocation = reviveUri(data.extensionLocation); const extensionId = data.extensionId ? new ExtensionIdentifier(data.extensionId) : undefined; const iconPath = reviveIconPath(data.iconPath); - return this._webviewService.reviveWebview(data.viewType, data.id, data.title, iconPath, data.state, data.options, extensionLocation ? { + return this._webviewService.reviveWebview(data.viewType, data.title, iconPath, data.state, data.options, extensionLocation ? { location: extensionLocation, id: extensionId } : undefined, data.group); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts similarity index 96% rename from src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts rename to src/vs/workbench/contrib/webview/browser/webviewEditorService.ts index 02537fbb23..1151c54489 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts @@ -39,7 +39,6 @@ export interface IWebviewEditorService { reviveWebview( viewType: string, - id: number, title: string, iconPath: { light: URI, dark: URI } | undefined, state: any, @@ -143,7 +142,7 @@ export class WebviewEditorService implements IWebviewEditorService { }, events: WebviewEvents ): WebviewEditorInput { - const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, undefined, title, options, {}, events, extension); + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, {}, events, extension); this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus }, showOptions.group); return webviewInput; } @@ -165,7 +164,6 @@ export class WebviewEditorService implements IWebviewEditorService { public reviveWebview( viewType: string, - id: number, title: string, iconPath: { light: URI, dark: URI } | undefined, state: any, @@ -176,7 +174,7 @@ export class WebviewEditorService implements IWebviewEditorService { }, group: number | undefined, ): WebviewEditorInput { - const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, id, title, options, state, {}, extension, async (webview: WebviewEditorInput): Promise => { + const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, title, options, state, {}, extension, async (webview: WebviewEditorInput): Promise => { const didRevive = await this.tryRevive(webview); if (didRevive) { return Promise.resolve(undefined); @@ -220,7 +218,7 @@ export class WebviewEditorService implements IWebviewEditorService { // Revived webviews may not have an actively registered reviver but we still want to presist them // since a reviver should exist when it is actually needed. - return !(webview instanceof RevivedWebviewEditorInput); + return webview instanceof RevivedWebviewEditorInput; } private async tryRevive( diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewFindWidget.ts b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts similarity index 72% rename from src/vs/workbench/contrib/webview/electron-browser/webviewFindWidget.ts rename to src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts index 1c82a69131..56c7e5fb5a 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewFindWidget.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts @@ -4,52 +4,45 @@ *--------------------------------------------------------------------------------------------*/ import { SimpleFindWidget } from 'vs/editor/contrib/find/simpleFindWidget'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { WebviewElement } from './webviewElement'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; + +export interface WebviewFindDelegate { + find(value: string, previous: boolean): void; + startFind(value: string): void; + stopFind(keepSelection?: boolean): void; + focus(): void; +} export class WebviewFindWidget extends SimpleFindWidget { constructor( - private _webview: WebviewElement | undefined, + private readonly _delegate: WebviewFindDelegate, @IContextViewService contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService ) { super(contextViewService, contextKeyService); } - dispose() { - this._webview = undefined; - super.dispose(); - } - public find(previous: boolean) { - if (!this._webview) { - return; - } const val = this.inputValue; if (val) { - this._webview.find(val, { findNext: true, forward: !previous }); + this._delegate.find(val, previous); } } public hide() { super.hide(); - if (this._webview) { - this._webview.stopFind(true); - this._webview.focus(); - } + this._delegate.stopFind(true); + this._delegate.focus(); } public onInputChanged() { - if (!this._webview) { - return; - } const val = this.inputValue; if (val) { - this._webview.startFind(val); + this._delegate.startFind(val); } else { - this._webview.stopFind(false); + this._delegate.stopFind(false); } } diff --git a/src/vs/workbench/contrib/webview/common/webview.ts b/src/vs/workbench/contrib/webview/common/webview.ts new file mode 100644 index 0000000000..208d174467 --- /dev/null +++ b/src/vs/workbench/contrib/webview/common/webview.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +/** + * Set when the find widget in a webview is visible. + */ +export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey('webviewFindWidgetVisible', false); + +export const IWebviewService = createDecorator('webviewService'); + +/** + * Handles the creation of webview elements. + */ +export interface IWebviewService { + _serviceBrand: any; + + createWebview( + options: WebviewOptions, + contentOptions: WebviewContentOptions, + ): Webview; +} + +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; +} + +export interface Webview { + + contents: string; + options: WebviewContentOptions; + initialScrollProgress: number; + state: string | undefined; + + readonly onDidFocus: Event; + readonly onDidClickLink: Event; + readonly onDidScroll: Event<{ scrollYPercentage: number }>; + readonly onDidUpdateState: Event; + readonly onMessage: Event; + + sendMessage(data: any): void; + update( + value: string, + options: WebviewContentOptions, + retainContextWhenHidden: boolean + ): void; + + layout(): void; + mountTo(parent: HTMLElement): void; + focus(): void; + dispose(): void; + + + reload(): void; + selectAll(): void; + copy(): void; + paste(): void; + cut(): void; + undo(): void; + redo(): void; + + showFind(): void; + hideFind(): void; +} diff --git a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js new file mode 100644 index 0000000000..21e15e0eed --- /dev/null +++ b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +(function () { + 'use strict'; + + const registerVscodeResourceScheme = (function () { + let hasRegistered = false; + return () => { + if (hasRegistered) { + return; + } + hasRegistered = true; + + // @ts-ignore + require('electron').webFrame.registerURLSchemeAsPrivileged('vscode-resource', { + secure: true, + bypassCSP: false, + allowServiceWorkers: false, + supportFetchAPI: true, + corsEnabled: true + }); + }; + }()); + + // @ts-ignore + const ipcRenderer = require('electron').ipcRenderer; + + require('../../browser/pre/main')({ + postMessage: (channel, data) => { + ipcRenderer.sendToHost(channel, data); + }, + onMessage: (channel, handler) => { + ipcRenderer.on(channel, handler); + } + }); + + document.addEventListener('DOMContentLoaded', () => { + registerVscodeResourceScheme(); + }); +}()); \ No newline at end of file diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts index 14e37ec9d3..98bfa3e8b7 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts @@ -3,135 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { localize } from 'vs/nls'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; -import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; -import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory'; -import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand, SelectAllWebviewEditorCommand, CopyWebviewEditorCommand, PasteWebviewEditorCommand, CutWebviewEditorCommand, UndoWebviewEditorCommand, RedoWebviewEditorCommand } from './webviewCommands'; -import { WebviewEditor, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './webviewEditor'; -import { WebviewEditorInput } from './webviewEditorInput'; -import { IWebviewEditorService, WebviewEditorService } from './webviewEditorService'; -import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; -import { isMacintosh } from 'vs/base/common/platform'; +import { IWebviewService } from 'vs/workbench/contrib/webview/common/webview'; +import { WebviewService } from 'vs/workbench/contrib/webview/electron-browser/webviewService'; -(Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor( - WebviewEditor, - WebviewEditor.ID, - localize('webview.editor.label', "webview editor")), - [new SyncDescriptor(WebviewEditorInput)]); - -Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( - WebviewEditorInputFactory.ID, - WebviewEditorInputFactory); - -registerSingleton(IWebviewEditorService, WebviewEditorService, true); - - -const webviewDeveloperCategory = localize('developer', "Developer"); - -const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); - -export function registerWebViewCommands(editorId: string): void { - const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */); - - const showNextFindWidgetCommand = new ShowWebViewEditorFindWidgetCommand({ - id: ShowWebViewEditorFindWidgetCommand.ID, - precondition: contextKeyExpr, - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F, - weight: KeybindingWeight.EditorContrib - } - }); - showNextFindWidgetCommand.register(); - - (new HideWebViewEditorFindCommand({ - id: HideWebViewEditorFindCommand.ID, - precondition: ContextKeyExpr.and( - contextKeyExpr, - KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE), - kbOpts: { - primary: KeyCode.Escape, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - (new SelectAllWebviewEditorCommand({ - id: SelectAllWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_A, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - // These commands are only needed on MacOS where we have to disable the menu bar commands - if (isMacintosh) { - (new CopyWebviewEditorCommand({ - id: CopyWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_C, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - (new PasteWebviewEditorCommand({ - id: PasteWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_V, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - - (new CutWebviewEditorCommand({ - id: CutWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_X, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - (new UndoWebviewEditorCommand({ - id: UndoWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - (new RedoWebviewEditorCommand({ - id: RedoWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, - secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z }, - weight: KeybindingWeight.EditorContrib - } - })).register(); - } -} - -registerWebViewCommands(WebviewEditor.ID); - -actionRegistry.registerWorkbenchAction( - new SyncActionDescriptor(OpenWebviewDeveloperToolsAction, OpenWebviewDeveloperToolsAction.ID, OpenWebviewDeveloperToolsAction.LABEL), - 'Webview Tools', - webviewDeveloperCategory); - -actionRegistry.registerWorkbenchAction( - new SyncActionDescriptor(ReloadWebviewAction, ReloadWebviewAction.ID, ReloadWebviewAction.LABEL), - 'Reload Webview', - webviewDeveloperCategory); +registerSingleton(IWebviewService, WebviewService, true); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 12a5ffb15e..767536d6fe 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -17,36 +17,12 @@ 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 './webviewEditorService'; -import { WebviewFindWidget } from './webviewFindWidget'; +import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService'; +import { WebviewFindWidget } from '../browser/webviewFindWidget'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { WebviewContentOptions, WebviewPortMapping, WebviewOptions, Webview } from 'vs/workbench/contrib/webview/common/webview'; -export interface WebviewPortMapping { - readonly port: number; - readonly resolvedPort: number; -} - -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; @@ -316,7 +292,7 @@ class WebviewKeyboardHandler extends Disposable { } -export class WebviewElement extends Disposable { +export class WebviewElement extends Disposable implements Webview { private _webview: Electron.WebviewTag; private _ready: Promise; @@ -350,7 +326,7 @@ export class WebviewElement extends Disposable { this._webview.style.height = '0'; this._webview.style.outline = '0'; - this._webview.preload = require.toUrl('./webview-pre.js'); + this._webview.preload = require.toUrl('./pre/electron-index.js'); this._webview.src = 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E'; this._ready = new Promise(resolve => { @@ -539,10 +515,6 @@ export class WebviewElement extends Disposable { }); } - public set baseUrl(value: string) { - this._send('baseUrl', value); - } - public focus(): void { this._webview.focus(); this._send('focus'); @@ -641,12 +613,13 @@ export class WebviewElement extends Disposable { * * @param value The string to search for. Empty strings are ignored. */ - public find(value: string, options?: Electron.FindInPageOptions): void { + public find(value: string, previous: boolean): void { // Searching with an empty value will throw an exception if (!value) { return; } + const options = { findNext: true, forward: !previous }; if (!this._findStarted) { this.startFind(value, options); return; diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts new file mode 100644 index 0000000000..cebf24373f --- /dev/null +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWebviewService, WebviewOptions, WebviewContentOptions, Webview } from 'vs/workbench/contrib/webview/common/webview'; + +export class WebviewService implements IWebviewService { + _serviceBrand: any; + + constructor( + @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { } + + createWebview( + options: WebviewOptions, + contentOptions: WebviewContentOptions + ): Webview { + const element = this._instantiationService.createInstance(WebviewElement, + this._layoutService.getContainer(Parts.EDITOR_PART), + options, + contentOptions); + + return element; + } +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index 6557626a93..3d566bacd0 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -13,7 +13,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { onUnexpectedError, isPromiseCanceledError } from 'vs/base/common/errors'; -import { IWindowService, URIType } from 'vs/platform/windows/common/windows'; +import { IWindowService, IURIToOpen } from 'vs/platform/windows/common/windows'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { localize } from 'vs/nls'; @@ -344,16 +344,13 @@ class WelcomePage { private createListEntries(recents: (IRecentWorkspace | IRecentFolder)[]) { return recents.map(recent => { let fullPath: string; - let resource: URI; - let typeHint: URIType | undefined; + let uriToOpen: IURIToOpen; if (isRecentFolder(recent)) { - resource = recent.folderUri; + uriToOpen = { folderUri: recent.folderUri }; fullPath = recent.label || this.labelService.getWorkspaceLabel(recent.folderUri, { verbose: true }); - typeHint = 'folder'; } else { fullPath = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); - resource = recent.workspace.configPath; - typeHint = 'file'; + uriToOpen = { workspaceUri: recent.workspace.configPath }; } const { name, parentPath } = splitName(fullPath); @@ -376,7 +373,7 @@ class WelcomePage { id: 'openRecentFolder', from: telemetryFrom }); - this.windowService.openWindow([{ uri: resource, typeHint }], { forceNewWindow: e.ctrlKey || e.metaKey }); + this.windowService.openWindow([uriToOpen], { forceNewWindow: e.ctrlKey || e.metaKey }); e.preventDefault(); e.stopPropagation(); }); diff --git a/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput.ts b/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput.ts index 2a38ce1db7..4380447820 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughInput.ts @@ -123,7 +123,7 @@ export class WalkThroughInput extends EditorInput { return this.promise; } - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; } diff --git a/src/vs/workbench/electron-browser/actions/windowActions.ts b/src/vs/workbench/electron-browser/actions/windowActions.ts index ad6be4b884..a3ee23ad90 100644 --- a/src/vs/workbench/electron-browser/actions/windowActions.ts +++ b/src/vs/workbench/electron-browser/actions/windowActions.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/actions'; import { URI } from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; -import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowService, IWindowsService, IURIToOpen } from 'vs/platform/windows/common/windows'; import * as nls from 'vs/nls'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isMacintosh } from 'vs/base/common/platform'; @@ -18,7 +18,7 @@ import { FileKind } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { IQuickInputService, IQuickPickItem, IQuickInputButton, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickInputButton, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import product from 'vs/platform/product/node/product'; import { ICommandHandler } from 'vs/platform/commands/common/commands'; @@ -235,7 +235,7 @@ export abstract class BaseSwitchWindow extends Action { iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : undefined, buttons: (!this.isQuickNavigate() && currentWindowId !== win.id) ? [this.closeWindowAction] : undefined - } as (IQuickPickItem & { payload: number }); + }; }); const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; @@ -339,38 +339,36 @@ export abstract class BaseOpenRecentAction extends Action { private openRecent(recentWorkspaces: Array, recentFiles: IRecentFile[]): void { const toPick = (recent: IRecent, labelService: ILabelService, buttons: IQuickInputButton[] | undefined) => { - let resource: URI | undefined; + let uriToOpen: IURIToOpen | undefined; + let iconClasses: string[]; let fullLabel: string | undefined; - let fileKind: FileKind | undefined; + let resource: URI | undefined; if (isRecentFolder(recent)) { resource = recent.folderUri; - fullLabel = recent.label || labelService.getWorkspaceLabel(recent.folderUri, { verbose: true }); - fileKind = FileKind.FOLDER; + iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FOLDER); + uriToOpen = { folderUri: resource }; + fullLabel = recent.label || labelService.getWorkspaceLabel(resource, { verbose: true }); } else if (isRecentWorkspace(recent)) { resource = recent.workspace.configPath; + iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER); + uriToOpen = { workspaceUri: resource }; fullLabel = recent.label || labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); - fileKind = FileKind.ROOT_FOLDER; } else { resource = recent.fileUri; - fullLabel = recent.label || labelService.getUriLabel(recent.fileUri); - fileKind = FileKind.FILE; + iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FILE); + uriToOpen = { fileUri: resource }; + fullLabel = recent.label || labelService.getUriLabel(resource); } const { name, parentPath } = splitName(fullLabel); return { - iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), + iconClasses, label: name, description: parentPath, buttons, - resource, - fileKind, + uriToOpen, + resource }; }; - - const runPick = (uri: URI, isFile: boolean, keyMods: IKeyMods) => { - const forceNewWindow = keyMods.ctrlCmd; - return this.windowService.openWindow([{ uri, typeHint: isFile ? 'file' : 'folder' }], { forceNewWindow, forceOpenWorkspaceAsFile: isFile }); - }; - const workspacePicks = recentWorkspaces.map(workspace => toPick(workspace, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined)); const filePicks = recentFiles.map(p => toPick(p, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined)); @@ -394,7 +392,8 @@ export abstract class BaseOpenRecentAction extends Action { } }).then((pick): Promise | void => { if (pick) { - return runPick(pick.resource, pick.fileKind === FileKind.FILE, keyMods); + const forceNewWindow = keyMods.ctrlCmd; + return this.windowService.openWindow([pick.uriToOpen], { forceNewWindow }); } }); } diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index e8cec3e367..f42cb63ee7 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -13,8 +13,8 @@ import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/brow import { domContentLoaded, addDisposableListener, EventType, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { onUnexpectedError } from 'vs/base/common/errors'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { URI as uri } from 'vs/base/common/uri'; -import { WorkspaceService, DefaultConfigurationExportHelper } from 'vs/workbench/services/configuration/node/configurationService'; +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 { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { stat } from 'vs/base/node/pfs'; @@ -24,13 +24,13 @@ import { IWindowConfiguration, IWindowService } from 'vs/platform/windows/common import { WindowService } from 'vs/platform/windows/electron-browser/windowService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { webFrame } from 'electron'; -import { ISingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, IMultiFolderWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createSpdLogService } from 'vs/platform/log/node/spdlogService'; import { ConsoleLogService, MultiplexLogService, ILogService } from 'vs/platform/log/common/log'; import { StorageService } from 'vs/platform/storage/node/storageService'; import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log/node/logIpc'; import { Schemas } from 'vs/base/common/network'; -import { sanitizeFilePath } from 'vs/base/node/extfs'; +import { sanitizeFilePath } from 'vs/base/common/extpath'; import { basename } from 'vs/base/common/path'; import { GlobalStorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -49,6 +49,9 @@ import { DiskFileSystemProvider } from 'vs/workbench/services/files2/electron-br 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'; class CodeRendererMain extends Disposable { @@ -82,8 +85,9 @@ class CodeRendererMain extends Disposable { private reviveUris() { if (this.configuration.folderUri) { - this.configuration.folderUri = uri.revive(this.configuration.folderUri); + this.configuration.folderUri = URI.revive(this.configuration.folderUri); } + if (this.configuration.workspace) { this.configuration.workspace = reviveWorkspaceIdentifier(this.configuration.workspace); } @@ -94,13 +98,14 @@ class CodeRendererMain extends Disposable { if (Array.isArray(paths)) { paths.forEach(path => { if (path.fileUri) { - path.fileUri = uri.revive(path.fileUri); + path.fileUri = URI.revive(path.fileUri); } }); } }); + if (filesToWait) { - filesToWait.waitMarkerFileUri = uri.revive(filesToWait.waitMarkerFileUri); + filesToWait.waitMarkerFileUri = URI.revive(filesToWait.waitMarkerFileUri); } } @@ -182,19 +187,21 @@ class CodeRendererMain extends Disposable { const remoteAuthorityResolverService = new RemoteAuthorityResolverService(); serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); - const remoteAgentService = new RemoteAgentService(this.configuration, environmentService, remoteAuthorityResolverService); + const remoteAgentService = this._register(new RemoteAgentService(this.configuration, environmentService, remoteAuthorityResolverService)); serviceCollection.set(IRemoteAgentService, remoteAgentService); // Files - const fileService = new FileService2(logService); + const fileService = this._register(new FileService2(logService)); serviceCollection.set(IFileService, fileService); - fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(logService)); + const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); const connection = remoteAgentService.getConnection(); if (connection) { const channel = connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); - fileService.registerProvider(REMOTE_HOST_SCHEME, new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); + const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); + fileService.registerProvider(REMOTE_HOST_SCHEME, remoteFileSystemProvider); } return this.resolveWorkspaceInitializationPayload(environmentService).then(payload => Promise.all([ @@ -223,7 +230,7 @@ class CodeRendererMain extends Disposable { // Multi-root workspace if (this.configuration.workspace) { - return Promise.resolve(this.configuration.workspace as IMultiFolderWorkspaceInitializationPayload); + return Promise.resolve(this.configuration.workspace); } // Single-folder workspace @@ -245,7 +252,7 @@ class CodeRendererMain extends Disposable { return Promise.reject(new Error('Unexpected window configuration without backupPath')); } - payload = { id } as IEmptyWorkspaceInitializationPayload; + payload = { id }; } return payload; @@ -259,7 +266,7 @@ class CodeRendererMain extends Disposable { return Promise.resolve({ id: createHash('md5').update(folderUri.toString()).digest('hex'), folder: folderUri }); } - function computeLocalDiskFolderId(folder: uri, stat: fs.Stats): string { + function computeLocalDiskFolderId(folder: URI, stat: fs.Stats): string { let ctime: number | undefined; if (isLinux) { ctime = stat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead! @@ -281,16 +288,16 @@ class CodeRendererMain extends Disposable { // For local: ensure path is absolute and exists const sanitizedFolderPath = sanitizeFilePath(folderUri.fsPath, process.env['VSCODE_CWD'] || process.cwd()); return stat(sanitizedFolderPath).then(stat => { - const sanitizedFolderUri = uri.file(sanitizedFolderPath); + const sanitizedFolderUri = URI.file(sanitizedFolderPath); return { id: computeLocalDiskFolderId(sanitizedFolderUri, stat), folder: sanitizedFolderUri - } as ISingleFolderWorkspaceInitializationPayload; + }; }, error => onUnexpectedError(error)); } private createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IEnvironmentService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { - const workspaceService = new WorkspaceService(this.configuration, environmentService, remoteAgentService); + const workspaceService = new WorkspaceService({ userSettingsResource: URI.file(environmentService.appSettingsPath), remoteAuthority: this.configuration.remoteAuthority, configurationCache: new ConfigurationCache(environmentService) }, new ConfigurationFileService(), remoteAgentService); return workspaceService.initialize(payload).then(() => workspaceService, error => { onUnexpectedError(error); diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index e64542badd..c3c979bcb3 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -115,7 +115,7 @@ export class ElectronWindow extends Disposable { // Support runAction event ipc.on('vscode:runAction', (event: Event, request: IRunActionInWindowRequest) => { - const args: any[] = request.args || []; + const args: unknown[] = request.args || []; // If we run an action from the touchbar, we fill in the currently active resource // as payload because the touch bar items are context aware depending on the editor @@ -504,7 +504,7 @@ export class ElectronWindow extends Disposable { } private openResources(resources: Array, diffMode: boolean): void { - this.lifecycleService.when(LifecyclePhase.Ready).then((): Promise => { + this.lifecycleService.when(LifecyclePhase.Ready).then((): Promise => { // In diffMode we open 2 resources as diff if (diffMode && resources.length === 2) { @@ -526,9 +526,9 @@ export class ElectronWindow extends Disposable { const resource = URI.revive(p.fileUri); let input: IResourceInput | IUntitledResourceInput; if (isNew) { - input = { filePath: resource.fsPath, options: { pinned: true } } as IUntitledResourceInput; + input = { filePath: resource.fsPath, options: { pinned: true } }; } else { - input = { resource, options: { pinned: true } } as IResourceInput; + input = { resource, options: { pinned: true } }; } if (!isNew && typeof p.lineNumber === 'number' && typeof p.columnNumber === 'number') { diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 50896c2c48..8a2ae60583 100644 --- a/src/vs/workbench/services/backup/common/backup.ts +++ b/src/vs/workbench/services/backup/common/backup.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI as Uri } from 'vs/base/common/uri'; +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 { ITextBufferFactory } from 'vs/editor/common/model'; @@ -30,7 +30,7 @@ export interface IBackupFileService { * @param resource The resource that is backed up. * @return The backup resource if any. */ - loadBackupResource(resource: Uri): Promise; + loadBackupResource(resource: URI): Promise; /** * Given a resource, returns the associated backup resource. @@ -38,7 +38,7 @@ export interface IBackupFileService { * @param resource The resource to get the backup resource for. * @return The backup resource. */ - toBackupResource(resource: Uri): Uri; + toBackupResource(resource: URI): URI; /** * Backs up a resource. @@ -47,14 +47,14 @@ export interface IBackupFileService { * @param content The content of the resource as snapshot. * @param versionId The version id of the resource to backup. */ - backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise; + backupResource(resource: URI, content: ITextSnapshot, versionId?: number): Promise; /** * Gets a list of file backups for the current workspace. * * @return The list of backups. */ - getWorkspaceFileBackups(): Promise; + getWorkspaceFileBackups(): Promise; /** * Resolves the backup for the given resource. @@ -62,14 +62,14 @@ export interface IBackupFileService { * @param value The contents from a backup resource as stream. * @return The backup file's backed up content as text buffer factory. */ - resolveBackupContent(backup: Uri): Promise; + resolveBackupContent(backup: URI): Promise; /** * Discards the backup associated with a resource if it exists.. * * @param resource The resource whose backup is being discarded discard to back up. */ - discardResourceBackup(resource: Uri): Promise; + discardResourceBackup(resource: URI): Promise; /** * Discards all backups associated with the current workspace and prevents further backups from diff --git a/src/vs/workbench/services/backup/node/backupFileService.ts b/src/vs/workbench/services/backup/node/backupFileService.ts index 1271565027..35173eb19b 100644 --- a/src/vs/workbench/services/backup/node/backupFileService.ts +++ b/src/vs/workbench/services/backup/node/backupFileService.ts @@ -242,7 +242,7 @@ class BackupFileServiceImpl implements IBackupFileService { const backupResource = this.toBackupResource(resource); return this.ioOperationQueues.queueFor(backupResource).queue(() => { - return pfs.del(backupResource.fsPath).then(() => model.remove(backupResource)); + return pfs.rimraf(backupResource.fsPath, pfs.RimRafMode.MOVE).then(() => model.remove(backupResource)); }); }); } @@ -251,7 +251,7 @@ class BackupFileServiceImpl implements IBackupFileService { this.isShuttingDown = true; return this.ready.then(model => { - return pfs.del(this.backupWorkspacePath).then(() => model.clear()); + return pfs.rimraf(this.backupWorkspacePath, pfs.RimRafMode.MOVE).then(() => model.clear()); }); } 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 8143c0261e..9c9f2cdc4b 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 @@ -12,17 +12,18 @@ import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { URI as Uri } from 'vs/base/common/uri'; import { BackupFileService, BackupFilesModel, hashPath } from 'vs/workbench/services/backup/node/backupFileService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { TestContextService, TestTextResourceConfigurationService, TestLifecycleService, TestEnvironmentService, TestStorageService, TestWindowService } from 'vs/workbench/test/workbenchTestServices'; +import { TestContextService, TestTextResourceConfigurationService, TestEnvironmentService, TestWindowService } from 'vs/workbench/test/workbenchTestServices'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { DefaultEndOfLine } from 'vs/editor/common/model'; import { snapshotToString } from 'vs/platform/files/common/files'; import { Schemas } from 'vs/base/common/network'; 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'; const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice'); const backupHome = path.join(parentDir, 'Backups'); @@ -55,7 +56,14 @@ class TestBackupWindowService extends TestWindowService { class TestBackupFileService extends BackupFileService { constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) { - const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); + const fileService = new FileService2(new NullLogService()); + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); + fileService.setLegacyService(new LegacyFileService( + fileService, + new TestContextService(new Workspace(workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), + TestEnvironmentService, + new TestTextResourceConfigurationService(), + )); const windowService = new TestBackupWindowService(workspaceBackupPath); super(windowService, fileService); @@ -73,7 +81,7 @@ suite('BackupFileService', () => { service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath); // Delete any existing backups completely and then re-create it. - return pfs.del(backupHome, os.tmpdir()).then(() => { + return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE).then(() => { return pfs.mkdirp(backupHome).then(() => { return pfs.writeFile(workspacesJsonPath, ''); }); @@ -81,7 +89,7 @@ suite('BackupFileService', () => { }); teardown(() => { - return pfs.del(backupHome, os.tmpdir()); + return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); }); suite('hashPath', () => { diff --git a/src/vs/workbench/services/broadcast/electron-browser/broadcastService.ts b/src/vs/workbench/services/broadcast/electron-browser/broadcastService.ts index 0d1e0fdad2..d0fb21fa85 100644 --- a/src/vs/workbench/services/broadcast/electron-browser/broadcastService.ts +++ b/src/vs/workbench/services/broadcast/electron-browser/broadcastService.ts @@ -11,7 +11,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { IBroadcastService, IBroadcast } from 'vs/workbench/services/broadcast/common/broadcast'; -export class BroadcastService extends Disposable implements IBroadcastService { +class BroadcastService extends Disposable implements IBroadcastService { _serviceBrand: any; private readonly _onBroadcast: Emitter = this._register(new Emitter()); diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 31c9cc8264..47381fca8e 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -327,7 +327,7 @@ export class BulkEdit { // delete file if (await this._fileService.exists(edit.oldUri)) { let useTrash = this._configurationService.getValue('files.enableTrash'); - if (useTrash && !(await this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) { + if (useTrash && !(this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) { useTrash = false; // not supported by provider } await this._textFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive }); diff --git a/src/vs/workbench/services/commands/common/commandService.ts b/src/vs/workbench/services/commands/common/commandService.ts index 24b04735f7..4cdbea12c2 100644 --- a/src/vs/workbench/services/commands/common/commandService.ts +++ b/src/vs/workbench/services/commands/common/commandService.ts @@ -10,12 +10,14 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { timeout } from 'vs/base/common/async'; export class CommandService extends Disposable implements ICommandService { _serviceBrand: any; private _extensionHostIsReady: boolean = false; + private _starActivation: Promise | null; private readonly _onWillExecuteCommand: Emitter = this._register(new Emitter()); public readonly onWillExecuteCommand: Event = this._onWillExecuteCommand.event; @@ -27,6 +29,18 @@ export class CommandService extends Disposable implements ICommandService { ) { super(); this._extensionService.whenInstalledExtensionsRegistered().then(value => this._extensionHostIsReady = value); + this._starActivation = null; + } + + private _activateStar(): Promise { + if (!this._starActivation) { + // wait for * activation, limited to at most 30s + this._starActivation = Promise.race([ + this._extensionService.activateByEvent(`*`), + timeout(30000) + ]); + } + return this._starActivation; } executeCommand(id: string, ...args: any[]): Promise { @@ -44,10 +58,13 @@ export class CommandService extends Disposable implements ICommandService { } else { let waitFor = activation; if (!commandIsRegistered) { - waitFor = Promise.race([ - // race activation events against command registration - Promise.all([activation, this._extensionService.activateByEvent(`*`)]), - Event.toPromise(Event.filter(CommandsRegistry.onDidRegisterCommand, e => e === id)), + waitFor = Promise.all([ + activation, + Promise.race([ + // race * activation against command registration + this._activateStar(), + Event.toPromise(Event.filter(CommandsRegistry.onDidRegisterCommand, e => e === id)) + ]), ]); } return (waitFor as Promise).then(_ => this._tryExecuteCommand(id, args)); diff --git a/src/vs/workbench/services/commands/test/common/commandService.test.ts b/src/vs/workbench/services/commands/test/common/commandService.test.ts index 78786b0781..e1b4f1b525 100644 --- a/src/vs/workbench/services/commands/test/common/commandService.test.ts +++ b/src/vs/workbench/services/commands/test/common/commandService.test.ts @@ -134,4 +134,43 @@ suite('CommandService', function () { dispose(dispoables); }); }); + + test('issue #71471: wait for onCommand activation even if a command is registered', () => { + let expectedOrder: string[] = ['registering command', 'resolving activation event', 'executing command']; + let actualOrder: string[] = []; + let disposables: IDisposable[] = []; + let service = new CommandService(new InstantiationService(), new class extends NullExtensionService { + + activateByEvent(event: string): Promise { + if (event === '*') { + return new Promise(() => { }); //forever promise... + } + if (event.indexOf('onCommand:') === 0) { + return new Promise(resolve => { + setTimeout(() => { + // Register the command after some time + actualOrder.push('registering command'); + let reg = CommandsRegistry.registerCommand(event.substr('onCommand:'.length), () => { + actualOrder.push('executing command'); + }); + disposables.push(reg); + + setTimeout(() => { + // Resolve the activation event after some more time + actualOrder.push('resolving activation event'); + resolve(); + }, 10); + }, 10); + }); + } + return Promise.resolve(); + } + + }, new NullLogService()); + + return service.executeCommand('farboo2').then(() => { + assert.deepEqual(actualOrder, expectedOrder); + dispose(disposables); + }); + }); }); diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts similarity index 56% rename from src/vs/workbench/services/configuration/node/configuration.ts rename to src/vs/workbench/services/configuration/browser/configuration.ts index 8e9fd0b550..661143523e 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -4,43 +4,41 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { createHash } from 'crypto'; import * as resources from 'vs/base/common/resources'; import { Event, Emitter } from 'vs/base/common/event'; -import * as pfs from 'vs/base/node/pfs'; import * as errors from 'vs/base/common/errors'; -import * as collections from 'vs/base/common/collections'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { RunOnceScheduler, Delayer } from 'vs/base/common/async'; -import { FileChangeType, FileChangesEvent, IContent, IFileService } from 'vs/platform/files/common/files'; -import { ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { FileChangeType, FileChangesEvent, IFileService } 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 } from 'vs/workbench/services/configuration/common/configuration'; -import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; -import * as extfs from 'vs/base/node/extfs'; +import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, IConfigurationFileService } 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'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { extname, join } from 'vs/base/common/path'; import { equals } from 'vs/base/common/objects'; import { Schemas } from 'vs/base/common/network'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationModel, compare } from 'vs/platform/configuration/common/configuration'; -import { FileServiceBasedUserConfiguration, NodeBasedUserConfiguration } from 'vs/platform/configuration/node/configuration'; +import { createSHA1 } from 'vs/base/browser/hash'; export class LocalUserConfiguration extends Disposable { + private readonly userConfigurationResource: URI; private userConfiguration: NodeBasedUserConfiguration | FileServiceBasedUserConfiguration; + private changeDisposable: IDisposable = Disposable.None; private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); public readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; constructor( - environmentService: IEnvironmentService + userConfigurationResource: URI, + configurationFileService: IConfigurationFileService ) { super(); - this.userConfiguration = this._register(new NodeBasedUserConfiguration(environmentService.appSettingsPath)); - this._register(this.userConfiguration.onDidChangeConfiguration(configurationModel => this._onDidChangeConfiguration.fire(configurationModel))); + this.userConfigurationResource = userConfigurationResource; + this.userConfiguration = this._register(new NodeBasedUserConfiguration(this.userConfigurationResource, configurationFileService)); } initialize(): Promise { @@ -52,6 +50,21 @@ export class LocalUserConfiguration extends Disposable { } async adopt(fileService: IFileService): Promise { + if (this.userConfiguration instanceof NodeBasedUserConfiguration) { + const oldConfigurationModel = this.userConfiguration.getConfigurationModel(); + this.userConfiguration.dispose(); + dispose(this.changeDisposable); + + let newConfigurationModel = new ConfigurationModel(); + this.userConfiguration = this._register(new FileServiceBasedUserConfiguration(this.userConfigurationResource, fileService)); + this.changeDisposable = this._register(this.userConfiguration.onDidChangeConfiguration(configurationModel => this._onDidChangeConfiguration.fire(configurationModel))); + newConfigurationModel = await this.userConfiguration.initialize(); + + const { added, updated, removed } = compare(oldConfigurationModel, newConfigurationModel); + if (added.length > 0 || updated.length > 0 || removed.length > 0) { + return newConfigurationModel; + } + } return null; } } @@ -66,10 +79,10 @@ export class RemoteUserConfiguration extends Disposable { constructor( remoteAuthority: string, - environmentService: IEnvironmentService + configurationCache: IConfigurationCache ) { super(); - this._userConfiguration = this._cachedConfiguration = new CachedUserConfiguration(remoteAuthority, environmentService); + this._userConfiguration = this._cachedConfiguration = new CachedUserConfiguration(remoteAuthority, configurationCache); } initialize(): Promise { @@ -108,22 +121,157 @@ export class RemoteUserConfiguration extends Disposable { } } +class NodeBasedUserConfiguration extends Disposable { + + private configuraitonModel: ConfigurationModel = new ConfigurationModel(); + + constructor( + private readonly userConfigurationResource: URI, + private readonly configurationFileService: IConfigurationFileService + ) { + super(); + } + + initialize(): Promise { + return this._load(); + } + + reload(): Promise { + return this._load(); + } + + getConfigurationModel(): ConfigurationModel { + return this.configuraitonModel; + } + + async _load(): Promise { + const exists = await this.configurationFileService.exists(this.userConfigurationResource); + if (exists) { + try { + const content = await this.configurationFileService.resolveContent(this.userConfigurationResource); + const parser = new ConfigurationModelParser(this.userConfigurationResource.toString()); + parser.parse(content); + this.configuraitonModel = parser.configurationModel; + } catch (e) { + // ignore error + errors.onUnexpectedError(e); + this.configuraitonModel = new ConfigurationModel(); + } + } else { + this.configuraitonModel = new ConfigurationModel(); + } + return this.configuraitonModel; + } + +} + +export class FileServiceBasedUserConfiguration extends Disposable { + + private readonly reloadConfigurationScheduler: RunOnceScheduler; + protected readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); + readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; + + private fileWatcherDisposable: IDisposable = Disposable.None; + private directoryWatcherDisposable: IDisposable = Disposable.None; + + constructor( + private readonly configurationResource: URI, + private readonly fileService: IFileService + ) { + super(); + + this._register(fileService.onFileChanges(e => this.handleFileEvents(e))); + this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); + this._register(toDisposable(() => { + this.stopWatchingResource(); + this.stopWatchingDirectory(); + })); + } + + private watchResource(): void { + this.fileWatcherDisposable = this.fileService.watch(this.configurationResource); + } + + private stopWatchingResource(): void { + this.fileWatcherDisposable.dispose(); + this.fileWatcherDisposable = Disposable.None; + } + + private watchDirectory(): void { + const directory = resources.dirname(this.configurationResource); + this.directoryWatcherDisposable = this.fileService.watch(directory); + } + + private stopWatchingDirectory(): void { + this.directoryWatcherDisposable.dispose(); + this.directoryWatcherDisposable = Disposable.None; + } + + async initialize(): Promise { + const exists = await this.fileService.exists(this.configurationResource); + this.onResourceExists(exists); + return this.reload(); + } + + async reload(): Promise { + try { + const content = await this.fileService.resolveContent(this.configurationResource); + const parser = new ConfigurationModelParser(this.configurationResource.toString()); + parser.parse(content.value); + return parser.configurationModel; + } catch (e) { + return new ConfigurationModel(); + } + } + + private async handleFileEvents(event: FileChangesEvent): Promise { + const events = event.changes; + + let affectedByChanges = false; + + // Find changes that affect the resource + for (const event of events) { + affectedByChanges = resources.isEqual(this.configurationResource, event.resource); + if (affectedByChanges) { + if (event.type === FileChangeType.ADDED) { + this.onResourceExists(true); + } else if (event.type === FileChangeType.DELETED) { + this.onResourceExists(false); + } + break; + } + } + + if (affectedByChanges) { + this.reloadConfigurationScheduler.schedule(); + } + } + + private onResourceExists(exists: boolean): void { + if (exists) { + this.stopWatchingDirectory(); + this.watchResource(); + } else { + this.stopWatchingResource(); + this.watchDirectory(); + } + } +} + class CachedUserConfiguration extends Disposable { private readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; - private readonly cachedFolderPath: string; - private readonly cachedConfigurationPath: string; + private readonly key: ConfigurationKey; private configurationModel: ConfigurationModel; constructor( remoteAuthority: string, - private environmentService: IEnvironmentService + private readonly configurationCache: IConfigurationCache ) { super(); - this.cachedFolderPath = join(this.environmentService.userDataPath, 'CachedConfigurations', 'user', remoteAuthority); - this.cachedConfigurationPath = join(this.cachedFolderPath, 'configuration.json'); + this.key = { type: 'user', key: remoteAuthority }; this.configurationModel = new ConfigurationModel(); } @@ -135,44 +283,29 @@ class CachedUserConfiguration extends Disposable { return this.reload(); } - reload(): Promise { - return pfs.readFile(this.cachedConfigurationPath) - .then(content => content.toString(), () => '') - .then(content => { - try { - const parsed: IConfigurationModel = JSON.parse(content); - this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides); - } catch (e) { - } - return this.configurationModel; - }); + async reload(): Promise { + const content = await this.configurationCache.read(this.key); + try { + const parsed: IConfigurationModel = JSON.parse(content); + this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides); + } catch (e) { + } + return this.configurationModel; } updateConfiguration(configurationModel: ConfigurationModel): Promise { - const raw = JSON.stringify(configurationModel.toJSON()); - return this.createCachedFolder().then(created => { - if (created) { - return configurationModel.keys.length ? pfs.writeFile(this.cachedConfigurationPath, raw) : pfs.rimraf(this.cachedFolderPath); - } - return undefined; - }); + if (configurationModel.keys.length) { + return this.configurationCache.write(this.key, JSON.stringify(configurationModel.toJSON())); + } else { + return this.configurationCache.remove(this.key); + } } - - private createCachedFolder(): Promise { - return Promise.resolve(pfs.exists(this.cachedFolderPath)) - .then(undefined, () => false) - .then(exists => exists ? exists : pfs.mkdirp(this.cachedFolderPath).then(() => true, () => false)); - } -} - -export interface IWorkspaceIdentifier { - id: string; - configPath: URI; } export class WorkspaceConfiguration extends Disposable { private readonly _cachedConfiguration: CachedWorkspaceConfiguration; + private readonly _configurationFileService: IConfigurationFileService; private _workspaceConfiguration: IWorkspaceConfiguration; private _workspaceIdentifier: IWorkspaceIdentifier | null = null; private _fileService: IFileService | null = null; @@ -181,10 +314,12 @@ export class WorkspaceConfiguration extends Disposable { public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; constructor( - environmentService: IEnvironmentService + configurationCache: IConfigurationCache, + configurationFileService: IConfigurationFileService ) { super(); - this._cachedConfiguration = new CachedWorkspaceConfiguration(environmentService); + this._cachedConfiguration = new CachedWorkspaceConfiguration(configurationCache); + this._configurationFileService = configurationFileService; this._workspaceConfiguration = this._cachedConfiguration; } @@ -251,7 +386,7 @@ export class WorkspaceConfiguration extends Disposable { if (this._workspaceIdentifier.configPath.scheme === Schemas.file) { if (!(this._workspaceConfiguration instanceof NodeBasedWorkspaceConfiguration)) { dispose(this._workspaceConfiguration); - this._workspaceConfiguration = new NodeBasedWorkspaceConfiguration(); + this._workspaceConfiguration = new NodeBasedWorkspaceConfiguration(this._configurationFileService); return true; } return false; @@ -306,14 +441,17 @@ abstract class AbstractWorkspaceConfiguration extends Disposable implements IWor return this._workspaceIdentifier; } - load(workspaceIdentifier: IWorkspaceIdentifier): Promise { + async load(workspaceIdentifier: IWorkspaceIdentifier): Promise { this._workspaceIdentifier = workspaceIdentifier; - return this.loadWorkspaceConfigurationContents(workspaceIdentifier) - .then(contents => { - this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(workspaceIdentifier.id); - this.workspaceConfigurationModelParser.parse(contents); - this.consolidate(); - }); + this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(workspaceIdentifier.id); + let contents = ''; + try { + contents = (await this.loadWorkspaceConfigurationContents(workspaceIdentifier.configPath)) || ''; + } catch (e) { + errors.onUnexpectedError(e); + } + this.workspaceConfigurationModelParser.parse(contents); + this.consolidate(); } getConfigurationModel(): ConfigurationModel { @@ -338,17 +476,21 @@ abstract class AbstractWorkspaceConfiguration extends Disposable implements IWor this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel); } - protected abstract loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise; + protected abstract loadWorkspaceConfigurationContents(workspaceConfigurationResource: URI): Promise; } class NodeBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration { - protected loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise { - return pfs.readFile(workspaceIdentifier.configPath.fsPath) - .then(contents => contents.toString(), e => { - errors.onUnexpectedError(e); - return ''; - }); + constructor(private readonly configurationFileService: IConfigurationFileService) { + super(); + } + + protected async loadWorkspaceConfigurationContents(workspaceConfigurationResource: URI): Promise { + const exists = await this.configurationFileService.exists(workspaceConfigurationResource); + if (exists) { + return this.configurationFileService.resolveContent(workspaceConfigurationResource); + } + return undefined; } } @@ -356,6 +498,8 @@ class NodeBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration { class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration { private workspaceConfig: URI | null = null; + private workspaceConfigWatcher: IDisposable; + private readonly reloadConfigurationScheduler: RunOnceScheduler; constructor(private fileService: IFileService, from?: IWorkspaceConfiguration) { @@ -363,33 +507,24 @@ class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfigurat this.workspaceConfig = from && from.workspaceIdentifier ? from.workspaceIdentifier.configPath : null; this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e))); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); - this.watchWorkspaceConfigurationFile(); - this._register(toDisposable(() => this.unWatchWorkspaceConfigurtionFile())); + this.workspaceConfigWatcher = this.watchWorkspaceConfigurationFile(); } - private watchWorkspaceConfigurationFile(): void { + private watchWorkspaceConfigurationFile(): IDisposable { if (this.workspaceConfig) { - this.fileService.watch(this.workspaceConfig); + return this.fileService.watch(this.workspaceConfig); } + + return Disposable.None; } - private unWatchWorkspaceConfigurtionFile(): void { - if (this.workspaceConfig) { - this.fileService.unwatch(this.workspaceConfig); + protected loadWorkspaceConfigurationContents(workspaceConfigurationResource: URI): Promise { + if (!(this.workspaceConfig && resources.isEqual(this.workspaceConfig, workspaceConfigurationResource))) { + dispose(this.workspaceConfigWatcher); + this.workspaceConfig = workspaceConfigurationResource; + this.workspaceConfigWatcher = this.watchWorkspaceConfigurationFile(); } - } - - protected loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise { - if (!(this.workspaceConfig && resources.isEqual(this.workspaceConfig, workspaceIdentifier.configPath))) { - this.unWatchWorkspaceConfigurtionFile(); - this.workspaceConfig = workspaceIdentifier.configPath; - this.watchWorkspaceConfigurationFile(); - } - return this.fileService.resolveContent(this.workspaceConfig) - .then(content => content.value, e => { - errors.onUnexpectedError(e); - return ''; - }); + return this.fileService.resolveContent(this.workspaceConfig).then(content => content.value); } private handleWorkspaceFileEvents(event: FileChangesEvent): void { @@ -407,6 +542,12 @@ class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfigurat } } } + + dispose(): void { + super.dispose(); + + dispose(this.workspaceConfigWatcher); + } } class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfiguration { @@ -414,25 +555,24 @@ class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfi private readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; - private cachedWorkspacePath: string; - private cachedConfigurationPath: string; workspaceConfigurationModelParser: WorkspaceConfigurationModelParser; workspaceSettings: ConfigurationModel; - constructor(private environmentService: IEnvironmentService) { + constructor(private readonly configurationCache: IConfigurationCache) { super(); this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(''); this.workspaceSettings = new ConfigurationModel(); } - load(workspaceIdentifier: IWorkspaceIdentifier): Promise { - this.createPaths(workspaceIdentifier); - return pfs.readFile(this.cachedConfigurationPath) - .then(contents => { - this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(this.cachedConfigurationPath); - this.workspaceConfigurationModelParser.parse(contents.toString()); - this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel); - }, () => { }); + async load(workspaceIdentifier: IWorkspaceIdentifier): Promise { + try { + const key = this.getKey(workspaceIdentifier); + const contents = await this.configurationCache.read(key); + this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(key.key); + this.workspaceConfigurationModelParser.parse(contents); + this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel); + } catch (e) { + } } get workspaceIdentifier(): IWorkspaceIdentifier | null { @@ -457,38 +597,24 @@ class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfi async updateWorkspace(workspaceIdentifier: IWorkspaceIdentifier, configurationModel: ConfigurationModel): Promise { try { - this.createPaths(workspaceIdentifier); + const key = this.getKey(workspaceIdentifier); if (configurationModel.keys.length) { - const exists = await pfs.exists(this.cachedWorkspacePath); - if (!exists) { - await pfs.mkdirp(this.cachedWorkspacePath); - } - const raw = JSON.stringify(configurationModel.toJSON().contents); - await pfs.writeFile(this.cachedConfigurationPath, raw); + await this.configurationCache.write(key, JSON.stringify(configurationModel.toJSON().contents)); } else { - pfs.rimraf(this.cachedWorkspacePath); + await this.configurationCache.remove(key); } } catch (error) { - errors.onUnexpectedError(error); } } - private createPaths(workspaceIdentifier: IWorkspaceIdentifier) { - this.cachedWorkspacePath = join(this.environmentService.userDataPath, 'CachedConfigurations', 'workspaces', workspaceIdentifier.id); - this.cachedConfigurationPath = join(this.cachedWorkspacePath, 'workspace.json'); + private getKey(workspaceIdentifier: IWorkspaceIdentifier): ConfigurationKey { + return { + type: 'workspaces', + key: workspaceIdentifier.id + }; } } -function isFolderConfigurationFile(resource: URI): boolean { - const configurationNameResource = URI.from({ scheme: resource.scheme, path: resources.basename(resource) }); - return [`${FOLDER_SETTINGS_NAME}.json`, `${TASKS_CONFIGURATION_KEY}.json`, `${LAUNCH_CONFIGURATION_KEY}.json`].some(configurationFileName => - resources.isEqual(configurationNameResource, URI.from({ scheme: resource.scheme, path: configurationFileName }))); // only workspace config files -} - -function isFolderSettingsConfigurationFile(resource: URI): boolean { - return resources.isEqual(URI.from({ scheme: resource.scheme, path: resources.basename(resource) }), URI.from({ scheme: resource.scheme, path: `${FOLDER_SETTINGS_NAME}.json` })); -} - export interface IFolderConfiguration extends IDisposable { readonly onDidChange: Event; readonly loaded: boolean; @@ -503,12 +629,16 @@ export abstract class AbstractFolderConfiguration extends Disposable implements private _cache: ConfigurationModel; private _loaded: boolean = false; + private readonly configurationNames: string[]; + protected readonly configurationResources: URI[]; protected readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; - constructor(protected readonly folder: URI, workbenchState: WorkbenchState, from?: AbstractFolderConfiguration) { + constructor(protected readonly configurationFolder: URI, workbenchState: WorkbenchState, from?: AbstractFolderConfiguration) { super(); + 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 = from ? from._folderSettingsModelParser : new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? [ConfigurationScope.RESOURCE] : [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]); this._standAloneConfigurations = from ? from._standAloneConfigurations : []; this._cache = from ? from._cache : new ConfigurationModel(); @@ -518,23 +648,37 @@ export abstract class AbstractFolderConfiguration extends Disposable implements return this._loaded; } - loadConfiguration(): Promise { - return this.loadFolderConfigurationContents() - .then((contents) => { + async loadConfiguration(): Promise { + const configurationContents = await Promise.all(this.configurationResources.map(resource => + this.loadConfigurationResourceContents(resource) + .then(undefined, error => { + /* never fail */ + errors.onUnexpectedError(error); + return undefined; + }))); - // reset - this._standAloneConfigurations = []; - this._folderSettingsModelParser.parse(''); + // reset + this._standAloneConfigurations = []; + this._folderSettingsModelParser.parse(''); - // parse - this.parseContents(contents); + // parse + if (configurationContents[0]) { + this._folderSettingsModelParser.parse(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); + this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel); + } + } - // Consolidate (support *.json files in the workspace settings folder) - this.consolidate(); + // Consolidate (support *.json files in the workspace settings folder) + this.consolidate(); - this._loaded = true; - return this._cache; - }); + this._loaded = true; + return this._cache; } reprocess(): ConfigurationModel { @@ -550,109 +694,41 @@ export abstract class AbstractFolderConfiguration extends Disposable implements this._cache = this._folderSettingsModelParser.configurationModel.merge(...this._standAloneConfigurations); } - private parseContents(contents: { resource: URI, value: string }[]): void { - for (const content of contents) { - if (isFolderSettingsConfigurationFile(content.resource)) { - this._folderSettingsModelParser.parse(content.value); - } else { - const name = resources.basename(content.resource); - const matches = /([^\.]*)*\.json/.exec(name); - if (matches && matches[1]) { - const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(content.resource.toString(), matches[1]); - standAloneConfigurationModelParser.parse(content.value); - this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel); - } - } - } - } - - protected abstract loadFolderConfigurationContents(): Promise<{ resource: URI, value: string }[]>; + protected abstract loadConfigurationResourceContents(configurationResource: URI): Promise; } export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration { - private readonly folderConfigurationPath: URI; - - constructor(folder: URI, configFolderRelativePath: string, workbenchState: WorkbenchState) { - super(folder, workbenchState); - this.folderConfigurationPath = resources.joinPath(folder, configFolderRelativePath); + constructor(private readonly configurationFileService: IConfigurationFileService, configurationFolder: URI, workbenchState: WorkbenchState) { + super(configurationFolder, workbenchState); } - protected loadFolderConfigurationContents(): Promise<{ resource: URI, value: string }[]> { - return this.resolveStat(this.folderConfigurationPath).then(stat => { - if (!stat.isDirectory || !stat.children) { - return Promise.resolve([]); - } - return this.resolveContents(stat.children.filter(stat => isFolderConfigurationFile(stat.resource)) - .map(stat => stat.resource)); - }, err => [] /* never fail this call */) - .then(undefined, e => { - errors.onUnexpectedError(e); - return []; - }); - } - - private resolveContents(resources: URI[]): Promise<{ resource: URI, value: string }[]> { - return Promise.all(resources.map(resource => - pfs.readFile(resource.fsPath) - .then(contents => ({ resource, value: contents.toString() })))); - } - - private resolveStat(resource: URI): Promise<{ resource: URI, isDirectory?: boolean, children?: { resource: URI; }[] }> { - return new Promise<{ resource: URI, isDirectory?: boolean, children?: { resource: URI; }[] }>((c, e) => { - extfs.readdir(resource.fsPath, (error, children) => { - if (error) { - if ((error).code === 'ENOTDIR') { - c({ resource }); - } else { - e(error); - } - } else { - c({ - resource, - isDirectory: true, - children: children.map(child => { return { resource: resources.joinPath(resource, child) }; }) - }); - } - }); - }); + protected async loadConfigurationResourceContents(configurationResource: URI): Promise { + const exists = await this.configurationFileService.exists(configurationResource); + if (exists) { + return this.configurationFileService.resolveContent(configurationResource); + } + return undefined; } } export class FileServiceBasedFolderConfiguration extends AbstractFolderConfiguration { - private reloadConfigurationScheduler: RunOnceScheduler; - private readonly folderConfigurationPath: URI; - private readonly loadConfigurationDelayer = new Delayer>(50); + private changeEventTriggerScheduler: RunOnceScheduler; - constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) { - super(folder, workbenchState, from); - this.folderConfigurationPath = resources.joinPath(folder, configFolderRelativePath); - this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); + constructor(configurationFolder: URI, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) { + super(configurationFolder, workbenchState, from); + this.changeEventTriggerScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e))); } - protected loadFolderConfigurationContents(): Promise> { - return Promise.resolve(this.loadConfigurationDelayer.trigger(() => this.doLoadFolderConfigurationContents())); - } - - private doLoadFolderConfigurationContents(): Promise> { - const workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: Promise } = Object.create(null); - const bulkContentFetchromise = Promise.resolve(this.fileService.resolve(this.folderConfigurationPath)) - .then(stat => { - if (stat.isDirectory && stat.children) { - stat.children - .filter(child => isFolderConfigurationFile(child.resource)) - .forEach(child => { - const folderRelativePath = this.toFolderRelativePath(child.resource); - if (folderRelativePath) { - workspaceFilePathToConfiguration[folderRelativePath] = Promise.resolve(this.fileService.resolveContent(child.resource)).then(undefined, errors.onUnexpectedError); - } - }); - } - }).then(undefined, err => [] /* never fail this call */); - - return bulkContentFetchromise.then(() => Promise.all(collections.values(workspaceFilePathToConfiguration))).then(contents => contents.filter(content => content !== undefined)); + protected async loadConfigurationResourceContents(configurationResource: URI): Promise { + const exists = await this.fileService.exists(configurationResource); + if (exists) { + const contents = await this.fileService.resolveContent(configurationResource); + return contents.value; + } + return undefined; } private handleWorkspaceFileEvents(event: FileChangesEvent): void { @@ -664,9 +740,9 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura const resource = events[i].resource; const basename = resources.basename(resource); const isJson = extname(basename) === '.json'; - const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && basename === this.configFolderRelativePath); + const isConfigurationFolderDeleted = (events[i].type === FileChangeType.DELETED && resources.isEqual(resource, this.configurationFolder)); - if (!isJson && !isDeletedSettingsFolder) { + if (!isJson && !isConfigurationFolderDeleted) { continue; // only JSON files or the actual settings folder } @@ -676,72 +752,69 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura } // Handle case where ".vscode" got deleted - if (isDeletedSettingsFolder) { + if (isConfigurationFolderDeleted) { affectedByChanges = true; break; } // only valid workspace config files - if (!isFolderConfigurationFile(resource)) { - continue; + if (this.configurationResources.some(configurationResource => resources.isEqual(configurationResource, resource))) { + affectedByChanges = true; + break; } - - affectedByChanges = true; - break; } if (affectedByChanges) { - this.reloadConfigurationScheduler.schedule(); + this.changeEventTriggerScheduler.schedule(); } } private toFolderRelativePath(resource: URI): string | undefined { - if (resources.isEqualOrParent(resource, this.folderConfigurationPath)) { - return resources.relativePath(this.folderConfigurationPath, resource); + if (resources.isEqualOrParent(resource, this.configurationFolder)) { + return resources.relativePath(this.configurationFolder, resource); } return undefined; } } -export class CachedFolderConfiguration extends Disposable implements IFolderConfiguration { +class CachedFolderConfiguration extends Disposable implements IFolderConfiguration { private readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; - private readonly cachedFolderPath: string; - private readonly cachedConfigurationPath: string; private configurationModel: ConfigurationModel; - + private readonly key: Thenable; loaded: boolean = false; constructor( folder: URI, configFolderRelativePath: string, - environmentService: IEnvironmentService) { + private readonly configurationCache: IConfigurationCache + ) { super(); - this.cachedFolderPath = join(environmentService.userDataPath, 'CachedConfigurations', 'folders', createHash('md5').update(join(folder.path, configFolderRelativePath)).digest('hex')); - this.cachedConfigurationPath = join(this.cachedFolderPath, 'configuration.json'); + this.key = createSHA1(join(folder.path, configFolderRelativePath)).then(key => ({ type: 'folder', key })); this.configurationModel = new ConfigurationModel(); } - loadConfiguration(): Promise { - return pfs.readFile(this.cachedConfigurationPath) - .then(contents => { - const parsed: IConfigurationModel = JSON.parse(contents.toString()); - this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides); - this.loaded = true; - return this.configurationModel; - }, () => this.configurationModel); + async loadConfiguration(): Promise { + try { + const key = await this.key; + const contents = await this.configurationCache.read(key); + const parsed: IConfigurationModel = JSON.parse(contents.toString()); + this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides); + this.loaded = true; + } catch (e) { + } + return this.configurationModel; } - updateConfiguration(configurationModel: ConfigurationModel): Promise { - const raw = JSON.stringify(configurationModel.toJSON()); - return this.createCachedFolder().then(created => { - if (created) { - return configurationModel.keys.length ? pfs.writeFile(this.cachedConfigurationPath, raw) : pfs.rimraf(this.cachedFolderPath); - } - return undefined; - }); + async updateConfiguration(configurationModel: ConfigurationModel): Promise { + const key = await this.key; + if (configurationModel.keys.length) { + await this.configurationCache.write(key, JSON.stringify(configurationModel.toJSON())); + } else { + await this.configurationCache.remove(key); + } } reprocess(): ConfigurationModel { @@ -751,12 +824,6 @@ export class CachedFolderConfiguration extends Disposable implements IFolderConf getUnsupportedKeys(): string[] { return []; } - - private createCachedFolder(): Promise { - return Promise.resolve(pfs.exists(this.cachedFolderPath)) - .then(undefined, () => false) - .then(exists => exists ? exists : pfs.mkdirp(this.cachedFolderPath).then(() => true, () => false)); - } } export class FolderConfiguration extends Disposable implements IFolderConfiguration { @@ -765,24 +832,27 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat readonly onDidChange: Event = this._onDidChange.event; private folderConfiguration: IFolderConfiguration; + private readonly configurationFolder: URI; private cachedFolderConfiguration: CachedFolderConfiguration; private _loaded: boolean = false; constructor( readonly workspaceFolder: IWorkspaceFolder, - private readonly configFolderRelativePath: string, + configFolderRelativePath: string, private readonly workbenchState: WorkbenchState, - private environmentService: IEnvironmentService, + configurationFileService: IConfigurationFileService, + configurationCache: IConfigurationCache, fileService?: IFileService ) { super(); - this.cachedFolderConfiguration = new CachedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.environmentService); + this.configurationFolder = resources.joinPath(workspaceFolder.uri, configFolderRelativePath); + this.cachedFolderConfiguration = new CachedFolderConfiguration(workspaceFolder.uri, configFolderRelativePath, configurationCache); this.folderConfiguration = this.cachedFolderConfiguration; if (fileService) { - this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService); - } else if (this.workspaceFolder.uri.scheme === Schemas.file) { - this.folderConfiguration = new NodeBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState); + this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService); + } else if (workspaceFolder.uri.scheme === Schemas.file) { + this.folderConfiguration = new NodeBasedFolderConfiguration(configurationFileService, this.configurationFolder, this.workbenchState); } this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange())); } @@ -817,7 +887,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat } private adoptFromCachedConfiguration(fileService: IFileService): Promise { - const folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService); + const folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService); return folderConfiguration.loadConfiguration() .then(() => { this.folderConfiguration = folderConfiguration; @@ -829,7 +899,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat private adoptFromNodeBasedConfiguration(fileService: IFileService): Promise { const oldFolderConfiguration = this.folderConfiguration; - this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService, oldFolderConfiguration); + this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService, oldFolderConfiguration); oldFolderConfiguration.dispose(); this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange())); return Promise.resolve(false); @@ -841,7 +911,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat } private updateCache(): Promise { - if (this.workspaceFolder.uri.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedFolderConfiguration) { + if (this.configurationFolder.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedFolderConfiguration) { return this.folderConfiguration.loadConfiguration() .then(configurationModel => this.cachedFolderConfiguration.updateConfiguration(configurationModel)); } diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts similarity index 86% rename from src/vs/workbench/services/configuration/node/configurationService.ts rename to src/vs/workbench/services/configuration/browser/configurationService.ts index 4fad94b840..4bdcd95403 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -10,25 +10,20 @@ import { ResourceMap } from 'vs/base/common/map'; import { equals, deepClone } from 'vs/base/common/objects'; import { Disposable } from 'vs/base/common/lifecycle'; import { Queue, Barrier } from 'vs/base/common/async'; -import { writeFile } from 'vs/base/node/pfs'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { isLinux } from 'vs/base/common/platform'; import { IFileService } from 'vs/platform/files/common/files'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; 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 } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, IConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry'; +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'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import product from 'vs/platform/product/node/product'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ConfigurationEditingService } from 'vs/workbench/services/configuration/common/configurationEditingService'; -import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, LocalUserConfiguration } from 'vs/workbench/services/configuration/node/configuration'; +import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, LocalUserConfiguration } from 'vs/workbench/services/configuration/browser/configuration'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { localize } from 'vs/nls'; @@ -36,7 +31,6 @@ import { isEqual, dirname } from 'vs/base/common/resources'; import { mark } from 'vs/base/common/performance'; import { Schemas } from 'vs/base/common/network'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService { @@ -44,9 +38,10 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private workspace: Workspace; private completeWorkspaceBarrier: Barrier; + private readonly configurationCache: IConfigurationCache; private _configuration: Configuration; private defaultConfiguration: DefaultConfigurationModel; - private localUserConfiguration: LocalUserConfiguration; + private localUserConfiguration: LocalUserConfiguration | null = null; private remoteUserConfiguration: RemoteUserConfiguration | null = null; private workspaceConfiguration: WorkspaceConfiguration; private cachedFolderConfigs: ResourceMap; @@ -69,18 +64,25 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private configurationEditingService: ConfigurationEditingService; private jsonEditingService: JSONEditingService; - constructor(configuration: IWindowConfiguration, private environmentService: IEnvironmentService, private remoteAgentService: IRemoteAgentService, private workspaceSettingsRootFolder: string = FOLDER_CONFIG_FOLDER_NAME) { + constructor( + { userSettingsResource, remoteAuthority, configurationCache }: { userSettingsResource?: URI, remoteAuthority?: string, configurationCache: IConfigurationCache }, + private readonly configurationFileService: IConfigurationFileService, + private readonly remoteAgentService: IRemoteAgentService, + ) { super(); this.completeWorkspaceBarrier = new Barrier(); this.defaultConfiguration = new DefaultConfigurationModel(); - this.localUserConfiguration = this._register(new LocalUserConfiguration(environmentService)); - this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); - if (configuration.remoteAuthority) { - this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(configuration.remoteAuthority, environmentService)); + this.configurationCache = configurationCache; + if (userSettingsResource) { + this.localUserConfiguration = this._register(new LocalUserConfiguration(userSettingsResource, configurationFileService)); + this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); + } + if (remoteAuthority) { + this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache)); this._register(this.remoteUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onRemoteUserConfigurationChanged(userConfiguration))); } - this.workspaceConfiguration = this._register(new WorkspaceConfiguration(environmentService)); + this.workspaceConfiguration = this._register(new WorkspaceConfiguration(configurationCache, this.configurationFileService)); this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged())); this._register(Registry.as(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas())); @@ -284,10 +286,10 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return this._configuration.keys(); } - initialize(arg: IWorkspaceInitializationPayload, postInitialisationTask: () => void = () => null): Promise { + initialize(arg: IWorkspaceInitializationPayload): Promise { mark('willInitWorkspaceService'); return this.createWorkspace(arg) - .then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace, postInitialisationTask)).then(() => { + .then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace)).then(() => { mark('didInitWorkspaceService'); }); } @@ -295,7 +297,14 @@ export class WorkspaceService extends Disposable implements IConfigurationServic acquireFileService(fileService: IFileService): void { this.fileService = fileService; const changedWorkspaceFolders: IWorkspaceFolder[] = []; - this.localUserConfiguration.adopt(fileService); + if (this.localUserConfiguration) { + this.localUserConfiguration.adopt(fileService) + .then(changedModel => { + if (changedModel) { + this.onLocalUserConfigurationChanged(changedModel); + } + }); + } Promise.all([this.workspaceConfiguration.adopt(fileService), ...this.cachedFolderConfigs.values() .map(folderConfiguration => folderConfiguration.adopt(fileService) .then(result => { @@ -382,7 +391,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } } - private updateWorkspaceAndInitializeConfiguration(workspace: Workspace, postInitialisationTask: () => void): Promise { + private updateWorkspaceAndInitializeConfiguration(workspace: Workspace): Promise { const hasWorkspaceBefore = !!this.workspace; let previousState: WorkbenchState; let previousWorkspacePath: string | undefined; @@ -399,8 +408,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return this.initializeConfiguration().then(() => { - postInitialisationTask(); // Post initialisation task should be run before triggering events. - // Trigger changes after configuration initialization so that configuration is up to date. if (hasWorkspaceBefore) { const newState = this.getWorkbenchState(); @@ -446,12 +453,12 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } private initializeUserConfiguration(): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> { - return Promise.all([this.localUserConfiguration.initialize(), this.remoteUserConfiguration ? this.remoteUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel())]) + return Promise.all([this.localUserConfiguration ? this.localUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel()), this.remoteUserConfiguration ? this.remoteUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel())]) .then(([local, remote]) => ({ local, remote })); } private reloadUserConfiguration(key?: string): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> { - return Promise.all([this.localUserConfiguration.reload(), this.remoteUserConfiguration ? this.remoteUserConfiguration.reload() : Promise.resolve(new ConfigurationModel())]) + return Promise.all([this.localUserConfiguration ? this.localUserConfiguration.reload() : Promise.resolve(new ConfigurationModel()), this.remoteUserConfiguration ? this.remoteUserConfiguration.reload() : Promise.resolve(new ConfigurationModel())]) .then(([local, remote]) => ({ local, remote })); } @@ -622,7 +629,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return Promise.all([...folders.map(folder => { let folderConfiguration = this.cachedFolderConfigs.get(folder.uri); if (!folderConfiguration) { - folderConfiguration = new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.getWorkbenchState(), this.environmentService, this.fileService); + folderConfiguration = new FolderConfiguration(folder, FOLDER_CONFIG_FOLDER_NAME, this.getWorkbenchState(), this.configurationFileService, this.configurationCache, this.fileService); this._register(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder))); this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration)); } @@ -709,99 +716,4 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } return {}; } -} - -interface IExportedConfigurationNode { - name: string; - description: string; - default: any; - type?: string | string[]; - enum?: any[]; - enumDescriptions?: string[]; -} - -interface IConfigurationExport { - settings: IExportedConfigurationNode[]; - buildTime: number; - commit?: string; - buildNumber?: number; -} - -export class DefaultConfigurationExportHelper { - - constructor( - @IEnvironmentService environmentService: IEnvironmentService, - @IExtensionService private readonly extensionService: IExtensionService, - @ICommandService private readonly commandService: ICommandService) { - if (environmentService.args['export-default-configuration']) { - this.writeConfigModelAndQuit(environmentService.args['export-default-configuration']); - } - } - - private writeConfigModelAndQuit(targetPath: string): Promise { - return Promise.resolve(this.extensionService.whenInstalledExtensionsRegistered()) - .then(() => this.writeConfigModel(targetPath)) - .then(() => this.commandService.executeCommand('workbench.action.quit')) - .then(() => { }); - } - - private writeConfigModel(targetPath: string): Promise { - const config = this.getConfigModel(); - - const resultString = JSON.stringify(config, undefined, ' '); - return writeFile(targetPath, resultString); - } - - private getConfigModel(): IConfigurationExport { - const configRegistry = Registry.as(Extensions.Configuration); - const configurations = configRegistry.getConfigurations().slice(); - const settings: IExportedConfigurationNode[] = []; - - const processProperty = (name: string, prop: IConfigurationPropertySchema) => { - const propDetails: IExportedConfigurationNode = { - name, - description: prop.description || prop.markdownDescription || '', - default: prop.default, - type: prop.type - }; - - if (prop.enum) { - propDetails.enum = prop.enum; - } - - if (prop.enumDescriptions || prop.markdownEnumDescriptions) { - propDetails.enumDescriptions = prop.enumDescriptions || prop.markdownEnumDescriptions; - } - - settings.push(propDetails); - }; - - const processConfig = (config: IConfigurationNode) => { - if (config.properties) { - for (let name in config.properties) { - processProperty(name, config.properties[name]); - } - } - - if (config.allOf) { - config.allOf.forEach(processConfig); - } - }; - - configurations.forEach(processConfig); - - const excludedProps = configRegistry.getExcludedConfigurationProperties(); - for (let name in excludedProps) { - processProperty(name, excludedProps[name]); - } - - const result: IConfigurationExport = { - settings: settings.sort((a, b) => a.name.localeCompare(b.name)), - buildTime: Date.now(), - commit: product.commit, - buildNumber: product.settingsSearchBuildId - }; - - return result; - } -} +} \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index b03ffaab81..37e24ab32f 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; + export const FOLDER_CONFIG_FOLDER_NAME = '.azuredatastudio'; export const FOLDER_SETTINGS_NAME = 'settings'; export const FOLDER_SETTINGS_PATH = `${FOLDER_CONFIG_FOLDER_NAME}/${FOLDER_SETTINGS_NAME}.json`; @@ -19,3 +21,19 @@ export const LAUNCH_CONFIGURATION_KEY = 'launch'; 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 { + + read(key: ConfigurationKey): Promise; + write(key: ConfigurationKey, content: string): Promise; + remove(key: ConfigurationKey): Promise; + +} + +export interface IConfigurationFileService { + exists(resource: URI): Promise; + resolveContent(resource: URI): Promise; +} diff --git a/src/vs/workbench/services/configuration/node/configurationCache.ts b/src/vs/workbench/services/configuration/node/configurationCache.ts new file mode 100644 index 0000000000..67b46a14cb --- /dev/null +++ b/src/vs/workbench/services/configuration/node/configurationCache.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as pfs from 'vs/base/node/pfs'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { join } from 'vs/base/common/path'; +import { IConfigurationCache, ConfigurationKey } from 'vs/workbench/services/configuration/common/configuration'; + +export class ConfigurationCache implements IConfigurationCache { + + private readonly cachedConfigurations: Map = new Map(); + + constructor(private readonly environmentService: IEnvironmentService) { + } + + read(key: ConfigurationKey): Promise { + return this.getCachedConfiguration(key).read(); + } + + write(key: ConfigurationKey, content: string): Promise { + return this.getCachedConfiguration(key).save(content); + } + + remove(key: ConfigurationKey): Promise { + return this.getCachedConfiguration(key).remove(); + } + + private getCachedConfiguration({ type, key }: ConfigurationKey): CachedConfiguration { + const k = `${type}:${key}`; + let cachedConfiguration = this.cachedConfigurations.get(k); + if (!cachedConfiguration) { + cachedConfiguration = new CachedConfiguration({ type, key }, this.environmentService); + this.cachedConfigurations.set(k, cachedConfiguration); + } + return cachedConfiguration; + } + +} + + +class CachedConfiguration { + + private cachedConfigurationFolderPath: string; + private cachedConfigurationFilePath: string; + + constructor( + { type, key }: ConfigurationKey, + environmentService: IEnvironmentService + ) { + this.cachedConfigurationFolderPath = join(environmentService.userDataPath, 'CachedConfigurations', type, key); + this.cachedConfigurationFilePath = join(this.cachedConfigurationFolderPath, type === 'workspaces' ? 'workspace.json' : 'configuration.json'); + } + + async read(): Promise { + try { + const content = await pfs.readFile(this.cachedConfigurationFilePath); + return content.toString(); + } catch (e) { + return ''; + } + } + + async save(content: string): Promise { + const created = await this.createCachedFolder(); + if (created) { + await pfs.writeFile(this.cachedConfigurationFilePath, content); + } + } + + remove(): Promise { + return pfs.rimraf(this.cachedConfigurationFolderPath); + } + + private createCachedFolder(): Promise { + return Promise.resolve(pfs.exists(this.cachedConfigurationFolderPath)) + .then(undefined, () => false) + .then(exists => exists ? exists : pfs.mkdirp(this.cachedConfigurationFolderPath).then(() => true, () => false)); + } +} + diff --git a/src/vs/workbench/services/configuration/node/configurationExportHelper.ts b/src/vs/workbench/services/configuration/node/configurationExportHelper.ts new file mode 100644 index 0000000000..0b33f235de --- /dev/null +++ b/src/vs/workbench/services/configuration/node/configurationExportHelper.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { writeFile } from 'vs/base/node/pfs'; +import product from 'vs/platform/product/node/product'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; + +interface IExportedConfigurationNode { + name: string; + description: string; + default: any; + type?: string | string[]; + enum?: any[]; + enumDescriptions?: string[]; +} + +interface IConfigurationExport { + settings: IExportedConfigurationNode[]; + buildTime: number; + commit?: string; + buildNumber?: number; +} + +export class DefaultConfigurationExportHelper { + + constructor( + @IEnvironmentService environmentService: IEnvironmentService, + @IExtensionService private readonly extensionService: IExtensionService, + @ICommandService private readonly commandService: ICommandService) { + if (environmentService.args['export-default-configuration']) { + this.writeConfigModelAndQuit(environmentService.args['export-default-configuration']); + } + } + + private writeConfigModelAndQuit(targetPath: string): Promise { + return Promise.resolve(this.extensionService.whenInstalledExtensionsRegistered()) + .then(() => this.writeConfigModel(targetPath)) + .then(() => this.commandService.executeCommand('workbench.action.quit')) + .then(() => { }); + } + + private writeConfigModel(targetPath: string): Promise { + const config = this.getConfigModel(); + + const resultString = JSON.stringify(config, undefined, ' '); + return writeFile(targetPath, resultString); + } + + private getConfigModel(): IConfigurationExport { + const configRegistry = Registry.as(Extensions.Configuration); + const configurations = configRegistry.getConfigurations().slice(); + const settings: IExportedConfigurationNode[] = []; + + const processProperty = (name: string, prop: IConfigurationPropertySchema) => { + const propDetails: IExportedConfigurationNode = { + name, + description: prop.description || prop.markdownDescription || '', + default: prop.default, + type: prop.type + }; + + if (prop.enum) { + propDetails.enum = prop.enum; + } + + if (prop.enumDescriptions || prop.markdownEnumDescriptions) { + propDetails.enumDescriptions = prop.enumDescriptions || prop.markdownEnumDescriptions; + } + + settings.push(propDetails); + }; + + const processConfig = (config: IConfigurationNode) => { + if (config.properties) { + for (let name in config.properties) { + processProperty(name, config.properties[name]); + } + } + + if (config.allOf) { + config.allOf.forEach(processConfig); + } + }; + + configurations.forEach(processConfig); + + const excludedProps = configRegistry.getExcludedConfigurationProperties(); + for (let name in excludedProps) { + processProperty(name, excludedProps[name]); + } + + const result: IConfigurationExport = { + settings: settings.sort((a, b) => a.name.localeCompare(b.name)), + buildTime: Date.now(), + commit: product.commit, + buildNumber: product.settingsSearchBuildId + }; + + return result; + } +} diff --git a/src/vs/workbench/services/configuration/node/configurationFileService.ts b/src/vs/workbench/services/configuration/node/configurationFileService.ts new file mode 100644 index 0000000000..ef6d1b6ea3 --- /dev/null +++ b/src/vs/workbench/services/configuration/node/configurationFileService.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 * as pfs from 'vs/base/node/pfs'; +import { IConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration'; +import { URI } from 'vs/base/common/uri'; + +export class ConfigurationFileService implements IConfigurationFileService { + + exists(resource: URI): Promise { + return pfs.exists(resource.fsPath); + } + + async resolveContent(resource: URI): Promise { + const contents = await pfs.readFile(resource.fsPath); + return contents.toString(); + } + +} diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index b7ea165c07..15027e035e 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -14,31 +14,33 @@ import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/ import { parseArgs } from 'vs/platform/environment/node/argv'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; -import * as extfs from 'vs/base/node/extfs'; -import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; +import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/common/configurationEditingService'; -import { IFileService } from 'vs/platform/files/common/files'; import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { mkdirp } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { CommandService } from 'vs/workbench/services/commands/common/commandService'; import { URI } from 'vs/base/common/uri'; import { createHash } from 'crypto'; -import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { Schemas } from 'vs/base/common/network'; +import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache'; +import { ConfigurationFileService } from 'vs/workbench/services/configuration/node/configurationFileService'; class SettingsTestEnvironmentService extends EnvironmentService { @@ -85,7 +87,7 @@ suite('ConfigurationEditingService', () => { .then(() => setUpServices()); }); - async function setUpWorkspace(): Promise { + async function setUpWorkspace(): Promise { const id = uuid.generateUuid(); parentDir = path.join(os.tmpdir(), 'vsctests', id); workspaceDir = path.join(parentDir, 'workspaceconfig', id); @@ -105,11 +107,19 @@ suite('ConfigurationEditingService', () => { instantiationService.stub(IEnvironmentService, environmentService); const remoteAgentService = instantiationService.createInstance(RemoteAgentService, {}); instantiationService.stub(IRemoteAgentService, remoteAgentService); - const workspaceService = new WorkspaceService({}, environmentService, remoteAgentService); + const workspaceService = new WorkspaceService({ userSettingsResource: URI.file(environmentService.appSettingsPath), configurationCache: new ConfigurationCache(environmentService) }, new ConfigurationFileService(), remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); return workspaceService.initialize(noWorkspace ? { id: '' } : { folder: URI.file(workspaceDir), id: createHash('md5').update(URI.file(workspaceDir).toString()).digest('hex') }).then(() => { instantiationService.stub(IConfigurationService, workspaceService); - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); + const fileService = new FileService2(new NullLogService()); + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); + fileService.setLegacyService(new LegacyFileService( + fileService, + workspaceService, + TestEnvironmentService, + new TestTextResourceConfigurationService(), + )); + instantiationService.stub(IFileService, fileService); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); instantiationService.stub(ICommandService, CommandService); @@ -135,7 +145,7 @@ suite('ConfigurationEditingService', () => { function clearWorkspace(): Promise { return new Promise((c, e) => { if (parentDir) { - extfs.del(parentDir, os.tmpdir(), () => c(undefined), () => c(undefined)); + rimraf(parentDir, RimRafMode.MOVE).then(c, c); } else { c(undefined); } diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index 7bceb37165..6712b5d83c 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -16,15 +16,14 @@ import { parseArgs } from 'vs/platform/environment/node/argv'; import * as pfs from 'vs/base/node/pfs'; import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; -import { ISingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; +import { ISingleFolderWorkspaceInitializationPayload, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/common/configurationEditingService'; -import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ConfigurationTarget, IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -32,15 +31,18 @@ import { TextModelResolverService } from 'vs/workbench/services/textmodelResolve import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; import { createHash } from 'crypto'; -import { Emitter, Event } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; import { originalFSPath } from 'vs/base/common/resources'; import { isLinux } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier } from 'vs/workbench/services/configuration/node/configuration'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-browser/remoteAuthorityResolverService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +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 { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache'; +import { ConfigurationFileService } from 'vs/workbench/services/configuration/node/configurationFileService'; class SettingsTestEnvironmentService extends EnvironmentService { @@ -91,7 +93,7 @@ function setUpWorkspace(folders: string[]): Promise<{ parentDir: string, configP suite('WorkspaceContextService - Folder', () => { test('getWorkspace()', () => { - // {{SQL CARBON EDIT}} - Remove test + // {{SQL CARBON EDIT}} - Remove tests assert.equal(0, 0); }); }); 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 e74c16d340..ab396a8ce9 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 @@ -18,19 +18,35 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput, IPickOptions, Omit, 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'; + +const mockLineNumber = 10; +class TestEditorServiceWithActiveEditor extends TestEditorService { + get activeTextEditorWidget(): any { + return { + getEditorType() { + return EditorType.ICodeEditor; + }, + getSelection() { + return new Selection(mockLineNumber, 1, mockLineNumber, 10); + } + }; + } +} 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 mockCommandService: MockCommandService; - let editorService: TestEditorService; + let editorService: TestEditorServiceWithActiveEditor; let workspace: IWorkspaceFolder; let quickInputService: MockQuickInputService; setup(() => { mockCommandService = new MockCommandService(); - editorService = new TestEditorService(); + editorService = new TestEditorServiceWithActiveEditor(); quickInputService = new MockQuickInputService(); windowService = new MockWindowService(envVariables); workspace = { @@ -58,14 +74,9 @@ suite('Configuration Resolver Service', () => { assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceRootFolderName} xyz'), 'abc workspaceLocation xyz'); }); - // TODO@isidor mock the editor service properly - // test('current selected line number', () => { - // assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${lineNumber} xyz'), `abc ${editorService.mockLineNumber} xyz`); - // }); - - // test('current selected text', () => { - // assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${selectedText} xyz'), `abc ${editorService.mockSelectedText} xyz`); - // }); + test('current selected line number', () => { + assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${lineNumber} xyz'), `abc ${mockLineNumber} xyz`); + }); test('substitute many', () => { if (platform.isWindows) { @@ -123,7 +134,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -140,7 +151,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz'); }); @@ -157,7 +168,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, 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 { @@ -178,7 +189,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, 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 { @@ -212,7 +223,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, 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'); }); @@ -222,7 +233,7 @@ suite('Configuration Resolver Service', () => { editor: {} }); - let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, 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'); }); @@ -235,7 +246,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.throws(() => service.resolve(workspace, 'abc ${env} xyz')); assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz')); diff --git a/src/vs/workbench/services/contextmenu/electron-browser/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-browser/contextmenuService.ts index 12b978f019..b7ecb4f989 100644 --- a/src/vs/workbench/services/contextmenu/electron-browser/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-browser/contextmenuService.ts @@ -125,7 +125,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService // Separator if (entry instanceof Separator) { - return { type: 'separator' } as IContextMenuItem; + return { type: 'separator' }; } // Submenu @@ -133,7 +133,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService return { label: unmnemonicLabel(entry.label), submenu: this.createMenu(delegate, entry.entries, onHide) - } as IContextMenuItem; + }; } // Normal Menu Item diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index 625e38c1d2..b663d0bb66 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -18,6 +18,7 @@ import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; export class FileDialogService implements IFileDialogService { @@ -29,7 +30,8 @@ export class FileDialogService implements IFileDialogService { @IHistoryService private readonly historyService: IHistoryService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IFileService private readonly fileService: IFileService ) { } defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined { @@ -78,9 +80,7 @@ export class FileDialogService implements IFileDialogService { return { forceNewWindow: options.forceNewWindow, telemetryExtraData: options.telemetryExtraData, - dialogOptions: { - defaultPath: options.defaultUri && options.defaultUri.fsPath - } + defaultPath: options.defaultUri && options.defaultUri.fsPath }; } @@ -103,7 +103,15 @@ export class FileDialogService implements IFileDialogService { if (this.shouldUseSimplified(schema)) { const title = nls.localize('openFileOrFolder.title', 'Open File Or Folder'); const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well - return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, !!options.forceNewWindow, true); + return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => { + if (uri) { + return (this.fileService.resolve(uri)).then(stat => { + const toOpen: IURIToOpen = stat.isDirectory ? { fileUri: uri } : { folderUri: uri }; + return this.windowService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow }); + }); + } + return undefined; + }); } return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options)); @@ -119,7 +127,12 @@ export class FileDialogService implements IFileDialogService { if (this.shouldUseSimplified(schema)) { const title = nls.localize('openFile.title', 'Open File'); const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well - return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, !!options.forceNewWindow, true); + return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => { + if (uri) { + return this.windowService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow }); + } + return undefined; + }); } return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options)); @@ -135,7 +148,12 @@ export class FileDialogService implements IFileDialogService { if (this.shouldUseSimplified(schema)) { const title = nls.localize('openFolder.title', 'Open Folder'); const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well - return this.pickRemoteResourceAndOpen({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, !!options.forceNewWindow, false); + return this.pickRemoteResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => { + if (uri) { + return this.windowService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow }); + } + return undefined; + }); } return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options)); @@ -152,7 +170,12 @@ export class FileDialogService implements IFileDialogService { const title = nls.localize('openWorkspace.title', 'Open Workspace'); const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }]; const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well - return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }, !!options.forceNewWindow, false); + return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }).then(uri => { + if (uri) { + return this.windowService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow }); + } + return undefined; + }); } return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options)); @@ -191,8 +214,8 @@ export class FileDialogService implements IFileDialogService { if (!options.availableFileSystems) { options.availableFileSystems = [schema]; // by default only allow loading in the own file system } - return this.pickRemoteResource(options).then(urisToOpen => { - return urisToOpen && urisToOpen.map(uto => uto.uri); + return this.pickRemoteResource(options).then(uri => { + return uri ? [uri] : undefined; }); } @@ -223,16 +246,7 @@ export class FileDialogService implements IFileDialogService { return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined); } - private pickRemoteResourceAndOpen(options: IOpenDialogOptions, forceNewWindow: boolean, forceOpenWorkspaceAsFile: boolean) { - return this.pickRemoteResource(options).then(urisToOpen => { - if (urisToOpen) { - return this.windowService.openWindow(urisToOpen, { forceNewWindow, forceOpenWorkspaceAsFile }); - } - return undefined; - }); - } - - private pickRemoteResource(options: IOpenDialogOptions): Promise { + private pickRemoteResource(options: IOpenDialogOptions): Promise { const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog); return remoteFileDialog.showOpenDialog(options); } diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index 790685b66b..5a6b96ff62 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -12,7 +12,7 @@ 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, IURIToOpen, FileFilter } from 'vs/platform/windows/common/windows'; +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'; @@ -40,7 +40,6 @@ export class RemoteFileDialog { private options: IOpenDialogOptions; private currentFolder: URI; private filePickBox: IQuickPick; - private filters: FileFilter[] | undefined; private hidden: boolean; private allowFileSelection: boolean; private allowFolderSelection: boolean; @@ -71,7 +70,7 @@ export class RemoteFileDialog { this.contextKey = RemoteFileDialogContext.bindTo(contextKeyService); } - public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { + public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { this.scheme = this.getScheme(options.defaultUri, options.availableFileSystems); const newOptions = await this.getOptions(options); if (!newOptions) { @@ -85,14 +84,7 @@ export class RemoteFileDialog { let fallbackLabel = options.canSelectFiles ? (options.canSelectFolders ? openFileFolderString : openFileString) : openFolderString; this.fallbackListItem = this.getFallbackFileSystem(fallbackLabel); - return this.pickResource().then(async fileFolderUri => { - if (fileFolderUri) { - const stat = await this.fileService.resolve(fileFolderUri); - return [{ uri: fileFolderUri, typeHint: stat.isDirectory ? 'folder' : 'file' }]; - - } - return Promise.resolve(undefined); - }); + return this.pickResource(); } public async showSaveDialog(options: ISaveDialogOptions): Promise { @@ -195,7 +187,7 @@ export class RemoteFileDialog { this.filePickBox.buttons = [this.acceptButton]; this.filePickBox.onDidTriggerButton(_ => { // accept button - const resolveValue = this.remoteUriFrom(this.filePickBox.value); + const resolveValue = this.addPostfix(this.remoteUriFrom(this.filePickBox.value)); this.validate(resolveValue).then(validated => { if (validated) { isResolving = true; @@ -323,6 +315,7 @@ export class RemoteFileDialog { } if (resolveValue) { + resolveValue = this.addPostfix(resolveValue); if (await this.validate(resolveValue)) { return Promise.resolve(resolveValue); } @@ -387,6 +380,32 @@ export class RemoteFileDialog { } } + private addPostfix(uri: URI): URI { + let result = uri; + if (this.requiresTrailing && this.options.filters && this.options.filters.length > 0) { + // Make sure that the suffix is added. If the user deleted it, we automatically add it here + let hasExt: boolean = false; + const currentExt = resources.extname(uri).substr(1); + if (currentExt !== '') { + for (let i = 0; i < this.options.filters.length; i++) { + for (let j = 0; j < this.options.filters[i].extensions.length; j++) { + if ((this.options.filters[i].extensions[j] === '*') || (this.options.filters[i].extensions[j] === currentExt)) { + hasExt = true; + break; + } + } + if (hasExt) { + break; + } + } + } + if (!hasExt) { + result = resources.joinPath(resources.dirname(uri), resources.basename(uri) + '.' + this.options.filters[0].extensions[0]); + } + } + return result; + } + private async validate(uri: URI): Promise { let stat: IFileStat | undefined; let statDirname: IFileStat | undefined; @@ -545,11 +564,11 @@ export class RemoteFileDialog { } private filterFile(file: URI): boolean { - if (this.filters) { + if (this.options.filters) { const ext = resources.extname(file); - for (let i = 0; i < this.filters.length; i++) { - for (let j = 0; j < this.filters[i].extensions.length; j++) { - if (ext === ('.' + this.filters[i].extensions[j])) { + for (let i = 0; i < this.options.filters.length; i++) { + for (let j = 0; j < this.options.filters[i].extensions.length; j++) { + if (ext === ('.' + this.options.filters[i].extensions[j])) { return true; } } diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index 9dbcda3781..83f368630c 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -87,7 +87,7 @@ class NativeDialogService implements IDialogService { return { confirmed: buttonIndexMap[result.button] === 0 ? true : false, checkboxChecked: result.checkboxChecked - } as IConfirmationResult; + }; }); } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 11d0acf4b0..a7e8483c02 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput, ITextEditorOptions, IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -37,7 +37,7 @@ type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditor export class EditorService extends Disposable implements EditorServiceImpl { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; private static CACHE: ResourceMap = new ResourceMap(); diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index ef36603368..0824b4ed29 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -45,6 +45,7 @@ export interface IVisibleEditor extends IEditor { } export interface IEditorService { + _serviceBrand: ServiceIdentifier; /** diff --git a/src/vs/workbench/services/extensions/common/extensionHostDebug.ts b/src/vs/workbench/services/extensions/common/extensionHostDebug.ts new file mode 100644 index 0000000000..60d48ac5b8 --- /dev/null +++ b/src/vs/workbench/services/extensions/common/extensionHostDebug.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; +import { IRemoteConsoleLog } from 'vs/base/common/console'; + +export const IExtensionHostDebugService = createDecorator('extensionHostDebugService'); + +export interface IExtensionHostDebugService { + _serviceBrand: any; + + reload(resource: URI): void; + onReload: Event; + + close(resource: URI): void; + onClose: Event; + + attachSession(id: string, port: number): void; + onAttachSession: Event<{ id: string, port: number }>; + + logToSession(id: string, log: IRemoteConsoleLog): void; + onLogToSession: Event<{ id: string, log: IRemoteConsoleLog }>; + + terminateSession(id: string): void; + onTerminateSession: Event; +} \ No newline at end of file diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index f1a641782f..c3d9261569 100644 --- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -134,7 +134,7 @@ export class CachedExtensionScanner { } try { - await pfs.del(cacheFile); + await pfs.rimraf(cacheFile, pfs.RimRafMode.MOVE); } catch (err) { errors.onUnexpectedError(err); console.error(err); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index 780263fc42..797f2bb27f 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -21,9 +21,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 { IBroadcast, IBroadcastService } from 'vs/workbench/services/broadcast/common/broadcast'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; 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'; @@ -38,6 +36,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { parseExtensionDevOptions } from '../common/extensionDevOptions'; import { VSBuffer } from 'vs/base/common/buffer'; +import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug'; export interface IExtensionHostStarter { readonly onCrashed: Event<[number, string | null]>; @@ -77,12 +76,12 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { @INotificationService private readonly _notificationService: INotificationService, @IWindowsService private readonly _windowsService: IWindowsService, @IWindowService private readonly _windowService: IWindowService, - @IBroadcastService private readonly _broadcastService: IBroadcastService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILogService private readonly _logService: ILogService, - @ILabelService private readonly _labelService: ILabelService + @ILabelService private readonly _labelService: ILabelService, + @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService ) { const devOpts = parseExtensionDevOptions(this._environmentService); this._isExtensionDevHost = devOpts.isExtensionDevHost; @@ -102,7 +101,16 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { this._toDispose.push(this._onCrashed); this._toDispose.push(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); this._toDispose.push(this._lifecycleService.onShutdown(reason => this.terminate())); - this._toDispose.push(this._broadcastService.onBroadcast(b => this._onBroadcast(b))); + this._toDispose.push(this._extensionHostDebugService.onClose(resource => { + if (this._isExtensionDevHost && isEqual(resource, this._environmentService.extensionDevelopmentLocationURI)) { + this._windowService.closeWindow(); + } + })); + this._toDispose.push(this._extensionHostDebugService.onReload(resource => { + if (this._isExtensionDevHost && isEqual(resource, this._environmentService.extensionDevelopmentLocationURI)) { + this._windowService.reloadWindow(); + } + })); const globalExitListener = () => this.terminate(); process.once('exit', globalExitListener); @@ -115,24 +123,6 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { this.terminate(); } - private _onBroadcast(broadcast: IBroadcast): void { - - // Close Ext Host Window Request - if (broadcast.channel === EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL && this._isExtensionDevHost) { - const extensionLocations = broadcast.payload as string[]; - if (Array.isArray(extensionLocations) && extensionLocations.some(uriString => isEqual(this._environmentService.extensionDevelopmentLocationURI, URI.parse(uriString)))) { - this._windowService.closeWindow(); - } - } - - if (broadcast.channel === EXTENSION_RELOAD_BROADCAST_CHANNEL && this._isExtensionDevHost) { - const extensionPaths = broadcast.payload as string[]; - if (Array.isArray(extensionPaths) && extensionPaths.some(uriString => isEqual(this._environmentService.extensionDevelopmentLocationURI, URI.parse(uriString)))) { - this._windowService.reloadWindow(); - } - } - } - public start(): Promise | null { if (this._terminating) { // .terminate() was called @@ -230,14 +220,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { this._extensionHostProcess.on('exit', (code: number, signal: string) => this._onExtHostProcessExit(code, signal)); // Notify debugger that we are ready to attach to the process if we run a development extension - if (this._isExtensionDevHost && portData.actual && this._isExtensionDevDebug) { - this._broadcastService.broadcast({ - channel: EXTENSION_ATTACH_BROADCAST_CHANNEL, - payload: { - debugId: this._environmentService.debugExtensionHost.debugId, - port: portData.actual - } - }); + if (this._isExtensionDevHost && portData.actual && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { + this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portData.actual); } this._inspectPort = portData.actual; @@ -445,14 +429,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { } // Broadcast to other windows if we are in development mode - else if (!this._environmentService.isBuilt || this._isExtensionDevHost) { - this._broadcastService.broadcast({ - channel: EXTENSION_LOG_BROADCAST_CHANNEL, - payload: { - logEntry: entry, - debugId: this._environmentService.debugExtensionHost.debugId - } - }); + else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) { + this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry); } } @@ -557,14 +535,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { // If the extension development host was started without debugger attached we need // to communicate this back to the main side to terminate the debug session - if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug) { - this._broadcastService.broadcast({ - channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL, - payload: { - debugId: this._environmentService.debugExtensionHost.debugId - } - }); - + if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { + this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId); event.join(timeout(100 /* wait a bit for IPC to get delivered */)); } } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts new file mode 100644 index 0000000000..c92bd39062 --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostDebugService.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 { URI } from 'vs/base/common/uri'; +import { IRemoteConsoleLog } from 'vs/base/common/console'; +import { ipcRenderer as ipc } from 'electron'; + +interface IReloadBroadcast { + type: 'vscode:extensionReload'; + resource: string; +} + +interface IAttachSessionBroadcast { + type: 'vscode:extensionAttach'; + id: string; + port: number; +} + +interface ICloseBroadcast { + type: 'vscode:extensionCloseExtensionHost'; + resource: string; +} + +interface ILogToSessionBroadcast { + type: 'vscode:extensionLog'; + id: string; + log: IRemoteConsoleLog; +} + +interface ITerminateSessionBroadcast { + type: 'vscode:extensionTerminate'; + id: string; +} + +const CHANNEL = 'vscode:extensionHostDebug'; + +class ExtensionHostDebugService implements IExtensionHostDebugService { + _serviceBrand: any; + + 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(); + + constructor( + @IWindowService readonly windowService: IWindowService, + ) { + this.windowId = windowService.getCurrentWindowId(); + + 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); + } + }); + } + + reload(resource: URI): void { + ipc.send(CHANNEL, this.windowId, { + type: 'vscode:extensionReload', + resource: resource.toString() + }); + } + + get onReload(): Event { + return this._onReload.event; + } + + close(resource: URI): void { + ipc.send(CHANNEL, this.windowId, { + type: 'vscode:extensionCloseExtensionHost', + resource: resource.toString() + }); + } + + get onClose(): Event { + return this._onClose.event; + } + + attachSession(id: string, port: number): void { + ipc.send(CHANNEL, this.windowId, { + type: 'vscode:extensionAttach', + id, + port + }); + } + + get onAttachSession(): Event<{ id: string, port: number }> { + return this._onAttachSession.event; + } + + logToSession(id: string, log: IRemoteConsoleLog): void { + ipc.send(CHANNEL, this.windowId, { + type: 'vscode:extensionLog', + id, + log + }); + } + + get onLogToSession(): Event<{ id: string, log: IRemoteConsoleLog }> { + return this._onLogToSession.event; + } + + terminateSession(id: string): void { + ipc.send(CHANNEL, this.windowId, { + type: 'vscode:extensionTerminate', + id + }); + } + + get onTerminateSession(): Event { + return this._onTerminateSession.event; + } +} + +registerSingleton(IExtensionHostDebugService, ExtensionHostDebugService, true); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts index c9bcedb1d1..184721489a 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts @@ -5,7 +5,7 @@ import { Profile, ProfileNode } from 'v8-inspect-profiler'; import { TernarySearchTree } from 'vs/base/common/map'; -import { realpathSync } from 'vs/base/node/extfs'; +import { realpathSync } from 'vs/base/node/extpath'; import { IExtensionHostProfile, IExtensionService, ProfileSegmentId, ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; diff --git a/src/vs/workbench/services/extensionManagement/node/multiExtensionManagement.ts b/src/vs/workbench/services/extensions/node/multiExtensionManagement.ts similarity index 100% rename from src/vs/workbench/services/extensionManagement/node/multiExtensionManagement.ts rename to src/vs/workbench/services/extensions/node/multiExtensionManagement.ts diff --git a/src/vs/workbench/services/files/node/encoding.ts b/src/vs/workbench/services/files/node/encoding.ts index 1c12c3b80b..7b887d8a16 100644 --- a/src/vs/workbench/services/files/node/encoding.ts +++ b/src/vs/workbench/services/files/node/encoding.ts @@ -5,7 +5,7 @@ import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import * as encoding from 'vs/base/node/encoding'; -import { URI as uri } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IResolveContentOptions, isParent, IResourceEncodings, IResourceEncoding } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; import { extname } from 'vs/base/common/path'; @@ -16,7 +16,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; export interface IEncodingOverride { - parent?: uri; + parent?: URI; extension?: string; encoding: string; } @@ -49,7 +49,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings })); } - getReadEncoding(resource: uri, options: IResolveContentOptions | undefined, detected: encoding.IDetectedEncodingResult): string { + getReadEncoding(resource: URI, options: IResolveContentOptions | undefined, detected: encoding.IDetectedEncodingResult): string { let preferredEncoding: string | undefined; // Encoding passed in as option @@ -78,7 +78,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings return this.getEncodingForResource(resource, preferredEncoding); } - getWriteEncoding(resource: uri, preferredEncoding?: string): IResourceEncoding { + getWriteEncoding(resource: URI, preferredEncoding?: string): IResourceEncoding { const resourceEncoding = this.getEncodingForResource(resource, preferredEncoding); return { @@ -87,7 +87,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings }; } - private getEncodingForResource(resource: uri, preferredEncoding?: string): string { + private getEncodingForResource(resource: URI, preferredEncoding?: string): string { let fileEncoding: string; const override = this.getEncodingOverride(resource); @@ -110,7 +110,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings const encodingOverride: IEncodingOverride[] = []; // Global settings - encodingOverride.push({ parent: uri.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 }); + encodingOverride.push({ parent: URI.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 }); // Workspace files encodingOverride.push({ extension: WORKSPACE_EXTENSION, encoding: encoding.UTF8 }); @@ -124,7 +124,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings return encodingOverride; } - private getEncodingOverride(resource: uri): string | null { + private getEncodingOverride(resource: URI): string | null { if (resource && this.encodingOverride && this.encodingOverride.length) { for (const override of this.encodingOverride) { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 3809e9b213..7ffee8c1c4 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -6,38 +6,23 @@ import * as paths from 'vs/base/common/path'; import * as fs from 'fs'; import * as os from 'os'; -import * as crypto from 'crypto'; import * as assert from 'assert'; -import { isParent, FileOperation, FileOperationEvent, IContent, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot, IFilesConfiguration, IFileSystemProviderRegistrationEvent, IFileSystemProvider, ILegacyFileService, IFileStatWithMetadata, IFileService, IResolveMetadataFileOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +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 { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/fileConstants'; -import { isEqualOrParent } from 'vs/base/common/extpath'; -import { ResourceMap } from 'vs/base/common/map'; -import * as arrays from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; -import * as extfs from 'vs/base/node/extfs'; -import { nfcall, ThrottledDelayer, timeout } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; import { URI as uri } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; -import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; -import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { isWindows, isMacintosh } from 'vs/base/common/platform'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import * as pfs from 'vs/base/node/pfs'; import { detectEncodingFromBuffer, decodeStream, detectEncodingByBOM, UTF8 } from 'vs/base/node/encoding'; -import * as flow from 'vs/base/node/flow'; -import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService'; -import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService'; -import { toFileChangesEvent, normalize, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; import { Event, Emitter } from 'vs/base/common/event'; -import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { getBaseLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { onUnexpectedError } from 'vs/base/common/errors'; import product from 'vs/platform/product/node/product'; import { IEncodingOverride, ResourceEncodings } from 'vs/workbench/services/files/node/encoding'; @@ -45,182 +30,37 @@ import { createReadableOfSnapshot } from 'vs/workbench/services/files/node/strea import { withUndefinedAsNull } from 'vs/base/common/types'; export interface IFileServiceTestOptions { - disableWatcher?: boolean; encodingOverride?: IEncodingOverride[]; } -export class FileService extends Disposable implements ILegacyFileService, IFileService { +export class LegacyFileService extends Disposable implements ILegacyFileService { _serviceBrand: any; - private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) - private static readonly FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms) - - private static readonly NET_VERSION_ERROR = 'System.MissingMethodException'; - private static readonly NET_VERSION_ERROR_IGNORE_KEY = 'ignoreNetVersionError'; - - private static readonly ENOSPC_ERROR = 'ENOSPC'; - private static readonly ENOSPC_ERROR_IGNORE_KEY = 'ignoreEnospcError'; - - protected readonly _onFileChanges: Emitter = this._register(new Emitter()); - get onFileChanges(): Event { return this._onFileChanges.event; } + registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { return Disposable.None; } protected readonly _onAfterOperation: Emitter = this._register(new Emitter()); get onAfterOperation(): Event { return this._onAfterOperation.event; } - protected readonly _onDidChangeFileSystemProviderRegistrations = this._register(new Emitter()); - get onDidChangeFileSystemProviderRegistrations(): Event { return this._onDidChangeFileSystemProviderRegistrations.event; } - - readonly onWillActivateFileSystemProvider = Event.None; - - private activeWorkspaceFileChangeWatcher: IDisposable | null; - private activeFileChangesWatchers: ResourceMap<{ unwatch: Function, count: number }>; - private fileChangesWatchDelayer: ThrottledDelayer; - private undeliveredRawFileChangesEvents: IRawFileChange[]; - private _encoding: ResourceEncodings; constructor( - private contextService: IWorkspaceContextService, + protected fileService: IFileService, + contextService: IWorkspaceContextService, private environmentService: IEnvironmentService, private textResourceConfigurationService: ITextResourceConfigurationService, - private configurationService: IConfigurationService, - private lifecycleService: ILifecycleService, - private storageService: IStorageService, - private notificationService: INotificationService, private options: IFileServiceTestOptions = Object.create(null) ) { super(); - this.activeFileChangesWatchers = new ResourceMap<{ unwatch: Function, count: number }>(); - this.fileChangesWatchDelayer = new ThrottledDelayer(FileService.FS_EVENT_DELAY); - this.undeliveredRawFileChangesEvents = []; - this._encoding = new ResourceEncodings(textResourceConfigurationService, environmentService, contextService, this.options.encodingOverride); - - this.registerListeners(); } get encoding(): ResourceEncodings { return this._encoding; } - private registerListeners(): void { - - // Wait until we are fully running before starting file watchers - this.lifecycleService.when(LifecyclePhase.Restored).then(() => { - this.setupFileWatching(); - }); - - // Workbench State Change - this._register(this.contextService.onDidChangeWorkbenchState(() => { - if (this.lifecycleService.phase >= LifecyclePhase.Restored) { - this.setupFileWatching(); - } - })); - - // Lifecycle - this.lifecycleService.onShutdown(this.dispose, this); - } - - private handleError(error: string | Error): void { - const msg = error ? error.toString() : undefined; - if (!msg) { - return; - } - - // Forward to unexpected error handler - onUnexpectedError(msg); - - // Detect if we run < .NET Framework 4.5 (TODO@ben remove with new watcher impl) - if (msg.indexOf(FileService.NET_VERSION_ERROR) >= 0 && !this.storageService.getBoolean(FileService.NET_VERSION_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) { - this.notificationService.prompt( - Severity.Warning, - nls.localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."), - [{ - label: nls.localize('installNet', "Download .NET Framework 4.5"), - run: () => window.open('https://go.microsoft.com/fwlink/?LinkId=786533') - }, - { - label: nls.localize('neverShowAgain', "Don't Show Again"), - isSecondary: true, - run: () => this.storageService.store(FileService.NET_VERSION_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE) - }], - { sticky: true } - ); - } - - // Detect if we run into ENOSPC issues - if (msg.indexOf(FileService.ENOSPC_ERROR) >= 0 && !this.storageService.getBoolean(FileService.ENOSPC_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) { - this.notificationService.prompt( - Severity.Warning, - nls.localize('enospcError', "{0} is unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue.", product.nameLong), - [{ - label: nls.localize('learnMore', "Instructions"), - run: () => window.open('https://go.microsoft.com/fwlink/?linkid=867693') - }, - { - label: nls.localize('neverShowAgain', "Don't Show Again"), - isSecondary: true, - run: () => this.storageService.store(FileService.ENOSPC_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE) - }], - { sticky: true } - ); - } - } - - private setupFileWatching(): void { - - // dispose old if any - if (this.activeWorkspaceFileChangeWatcher) { - this.activeWorkspaceFileChangeWatcher.dispose(); - } - - // Return if not aplicable - const workbenchState = this.contextService.getWorkbenchState(); - if (workbenchState === WorkbenchState.EMPTY || this.options.disableWatcher) { - return; - } - - // new watcher: use it if setting tells us so or we run in multi-root environment - const configuration = this.configurationService.getValue(); - if ((configuration.files && configuration.files.useExperimentalFileWatcher) || workbenchState === WorkbenchState.WORKSPACE) { - const multiRootWatcher = new NsfwWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); - this.activeWorkspaceFileChangeWatcher = toDisposable(multiRootWatcher.startWatching()); - } - - // legacy watcher - else { - let watcherIgnoredPatterns: string[] = []; - if (configuration.files && configuration.files.watcherExclude) { - watcherIgnoredPatterns = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); - } - - if (isWindows) { - const legacyWindowsWatcher = new WindowsWatcherService(this.contextService, watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); - this.activeWorkspaceFileChangeWatcher = toDisposable(legacyWindowsWatcher.startWatching()); - } else { - const legacyUnixWatcher = new UnixWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); - this.activeWorkspaceFileChangeWatcher = toDisposable(legacyUnixWatcher.startWatching()); - } - } - } - - registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { - return Disposable.None; - } - - activateProvider(scheme: string): Promise { - return Promise.reject(new Error('not implemented')); - } - - canHandleResource(resource: uri): boolean { - return resource.scheme === Schemas.file; - } - - hasCapability(resource: uri, capability: FileSystemProviderCapabilities): Promise { - return Promise.resolve(false); - } + //#region Read File resolveContent(resource: uri, options?: IResolveContentOptions): Promise { return this.resolveStreamContent(resource, options).then(streamContent => { @@ -280,7 +120,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile return Promise.reject(error); }; - const statsPromise = this.resolve(resource).then(stat => { + const statsPromise = this.fileService.resolve(resource).then(stat => { result.resource = stat.resource; result.name = stat.name; result.mtime = stat.mtime; @@ -447,7 +287,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile if (fd) { fs.close(fd, err => { if (err) { - this.handleError(`resolveFileData#close(): ${err.toString()}`); + onUnexpectedError(`resolveFileData#close(): ${err.toString()}`); } }); } @@ -538,6 +378,10 @@ export class FileService extends Disposable implements ILegacyFileService, IFile }); } + //#endregion + + //#region File Writing + updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Promise { if (options.writeElevated) { return this.doUpdateContentElevated(resource, value, options); @@ -634,7 +478,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): Promise { // Configure encoding related options as needed - const writeFileOptions: extfs.IWriteFileOptions = options ? options : Object.create(null); + const writeFileOptions: pfs.IWriteFileOptions = options ? options : Object.create(null); if (addBOM || encodingToWrite !== UTF8) { writeFileOptions.encoding = { charset: encodingToWrite, @@ -653,7 +497,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile return writeFilePromise.then(() => { // resolve - return this.resolve(resource); + return this.fileService.resolve(resource); }); } @@ -695,16 +539,16 @@ export class FileService extends Disposable implements ILegacyFileService, IFile }).then(() => { // 3.) delete temp file - return pfs.del(tmpPath, os.tmpdir()).then(() => { + return pfs.rimraf(tmpPath, pfs.RimRafMode.MOVE).then(() => { // 4.) resolve again - return this.resolve(resource); + return this.fileService.resolve(resource); }); }); }); }).then(undefined, error => { if (this.environmentService.verbose) { - this.handleError(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`); + onUnexpectedError(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`); } if (!FileOperationError.isFileOperationError(error)) { @@ -719,6 +563,10 @@ export class FileService extends Disposable implements ILegacyFileService, IFile }); } + //#endregion + + //#region Create File + createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): Promise { const absolutePath = this.toAbsolutePath(resource); @@ -750,6 +598,10 @@ export class FileService extends Disposable implements ILegacyFileService, IFile }); } + //#endregion + + //#region Helpers + private checkFileBeforeWriting(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): Promise { return pfs.exists(absolutePath).then(exists => { if (exists) { @@ -812,133 +664,6 @@ export class FileService extends Disposable implements ILegacyFileService, IFile )); } - move(source: uri, target: uri, overwrite?: boolean): Promise { - return this.moveOrCopyFile(source, target, false, !!overwrite); - } - - copy(source: uri, target: uri, overwrite?: boolean): Promise { - return this.moveOrCopyFile(source, target, true, !!overwrite); - } - - private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): Promise { - const sourcePath = this.toAbsolutePath(source); - const targetPath = this.toAbsolutePath(target); - - // 1.) move / copy - return this.doMoveOrCopyFile(sourcePath, targetPath, keepCopy, overwrite).then(() => { - - // 2.) resolve - return this.doResolve(target, { resolveMetadata: true }).then(result => { - - // Events (unless it was a no-op because paths are identical) - if (sourcePath !== targetPath) { - this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result)); - } - - return result; - }); - }); - } - - private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): Promise { - - // 1.) validate operation - if (isParent(targetPath, sourcePath, !isLinux)) { - return Promise.reject(new Error('Unable to move/copy when source path is parent of target path')); - } else if (sourcePath === targetPath) { - return Promise.resolve(); // no-op but not an error - } - - // 2.) check if target exists - return pfs.exists(targetPath).then(exists => { - const isCaseRename = sourcePath.toLowerCase() === targetPath.toLowerCase(); - - // Return early with conflict if target exists and we are not told to overwrite - if (exists && !isCaseRename && !overwrite) { - return Promise.reject(new FileOperationError(nls.localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT)); - } - - // 3.) make sure target is deleted before we move/copy unless this is a case rename of the same file - let deleteTargetPromise: Promise = Promise.resolve(); - if (exists && !isCaseRename) { - if (isEqualOrParent(sourcePath, targetPath, !isLinux /* ignorecase */)) { - return Promise.reject(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case! - } - - deleteTargetPromise = this.del(uri.file(targetPath), { recursive: true }); - } - - return deleteTargetPromise.then(() => { - - // 4.) make sure parents exists - return pfs.mkdirp(paths.dirname(targetPath)).then(() => { - - // 4.) copy/move - if (keepCopy) { - return nfcall(extfs.copy, sourcePath, targetPath); - } else { - return nfcall(extfs.mv, sourcePath, targetPath); - } - }); - }); - }); - } - - del(resource: uri, options?: { useTrash?: boolean, recursive?: boolean }): Promise { - if (options && options.useTrash) { - return this.doMoveItemToTrash(resource); - } - - return this.doDelete(resource, !!(options && options.recursive)); - } - - private doMoveItemToTrash(resource: uri): Promise { - const absolutePath = resource.fsPath; - - const shell = (require('electron') as any as Electron.RendererInterface).shell; // workaround for being able to run tests out of VSCode debugger - const result = shell.moveItemToTrash(absolutePath); - if (!result) { - return Promise.reject(new Error(isWindows ? nls.localize('binFailed', "Failed to move '{0}' to the recycle bin", paths.basename(absolutePath)) : nls.localize('trashFailed', "Failed to move '{0}' to the trash", paths.basename(absolutePath)))); - } - - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); - - return Promise.resolve(); - } - - private doDelete(resource: uri, recursive: boolean): Promise { - const absolutePath = this.toAbsolutePath(resource); - - let assertNonRecursiveDelete: Promise; - if (!recursive) { - assertNonRecursiveDelete = pfs.stat(absolutePath).then(stat => { - if (!stat.isDirectory()) { - return undefined; - } - - return pfs.readdir(absolutePath).then(children => { - if (children.length === 0) { - return undefined; - } - - return Promise.reject(new Error(nls.localize('deleteFailed', "Failed to delete non-empty folder '{0}'.", paths.basename(absolutePath)))); - }); - }, error => Promise.resolve() /* ignore errors */); - } else { - assertNonRecursiveDelete = Promise.resolve(); - } - - return assertNonRecursiveDelete.then(() => { - return pfs.del(absolutePath, os.tmpdir()).then(() => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); - }); - }); - } - - // Helpers - private toAbsolutePath(arg1: uri | IFileStat): string { let resource: uri; if (arg1 instanceof uri) { @@ -952,347 +677,5 @@ export class FileService extends Disposable implements ILegacyFileService, IFile return paths.normalize(resource.fsPath); } - private doResolve(resource: uri, options: IResolveMetadataFileOptions): Promise; - private doResolve(resource: uri, options?: IResolveFileOptions): Promise; - private doResolve(resource: uri, options: IResolveFileOptions = Object.create(null)): Promise { - return this.toStatResolver(resource).then(model => model.resolve(options)); - } - - private toStatResolver(resource: uri): Promise { - const absolutePath = this.toAbsolutePath(resource); - - return pfs.statLink(absolutePath).then(({ isSymbolicLink, stat }) => { - return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.environmentService.verbose ? err => this.handleError(err) : undefined); - }); - } - - watch(resource: uri): void { - assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`); - - // Check for existing watcher first - const entry = this.activeFileChangesWatchers.get(resource); - if (entry) { - entry.count += 1; - - return; - } - - // Create or get watcher for provided path - const fsPath = resource.fsPath; - const fsName = paths.basename(resource.fsPath); - - const watcherDisposable = extfs.watch(fsPath, (eventType: string, filename: string) => { - const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename'); - - // The file was either deleted or renamed. Many tools apply changes to files in an - // atomic way ("Atomic Save") by first renaming the file to a temporary name and then - // renaming it back to the original name. Our watcher will detect this as a rename - // and then stops to work on Mac and Linux because the watcher is applied to the - // inode and not the name. The fix is to detect this case and trying to watch the file - // again after a certain delay. - // In addition, we send out a delete event if after a timeout we detect that the file - // does indeed not exist anymore. - if (renamedOrDeleted) { - - // Very important to dispose the watcher which now points to a stale inode - watcherDisposable.dispose(); - this.activeFileChangesWatchers.delete(resource); - - // Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save") - setTimeout(() => { - this.exists(resource).then(exists => { - - // File still exists, so reapply the watcher - if (exists) { - this.watch(resource); - } - - // File seems to be really gone, so emit a deleted event - else { - this.onRawFileChange({ - type: FileChangeType.DELETED, - path: fsPath - }); - } - }); - }, FileService.FS_REWATCH_DELAY); - } - - // Handle raw file change - this.onRawFileChange({ - type: FileChangeType.UPDATED, - path: fsPath - }); - }, (error: string) => this.handleError(error)); - - // Remember in map - this.activeFileChangesWatchers.set(resource, { - count: 1, - unwatch: () => watcherDisposable.dispose() - }); - } - - private onRawFileChange(event: IRawFileChange): void { - - // add to bucket of undelivered events - this.undeliveredRawFileChangesEvents.push(event); - - if (this.environmentService.verbose) { - console.log('%c[File Watcher (node.js)]%c', 'color: blue', 'color: black', `${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); - } - - // handle emit through delayer to accommodate for bulk changes - this.fileChangesWatchDelayer.trigger(() => { - const buffer = this.undeliveredRawFileChangesEvents; - this.undeliveredRawFileChangesEvents = []; - - // Normalize - const normalizedEvents = normalize(buffer); - - // Logging - if (this.environmentService.verbose) { - normalizedEvents.forEach(r => { - console.log('%c[File Watcher (node.js)]%c >> normalized', 'color: blue', 'color: black', `${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`); - }); - } - - // Emit - this._onFileChanges.fire(toFileChangesEvent(normalizedEvents)); - - return Promise.resolve(); - }); - } - - unwatch(resource: uri): void { - const watcher = this.activeFileChangesWatchers.get(resource); - if (watcher && --watcher.count === 0) { - watcher.unwatch(); - this.activeFileChangesWatchers.delete(resource); - } - } - - dispose(): void { - super.dispose(); - - if (this.activeWorkspaceFileChangeWatcher) { - this.activeWorkspaceFileChangeWatcher.dispose(); - this.activeWorkspaceFileChangeWatcher = null; - } - - this.activeFileChangesWatchers.forEach(watcher => watcher.unwatch()); - this.activeFileChangesWatchers.clear(); - } - - - - - - - - - // Tests only - - resolve(resource: uri, options?: IResolveFileOptions): Promise; - resolve(resource: uri, options: IResolveMetadataFileOptions): Promise; - resolve(resource: uri, options?: IResolveFileOptions): Promise { - return this.doResolve(resource, options); - } - - resolveAll(toResolve: { resource: uri, options?: IResolveFileOptions }[]): Promise { - return Promise.all(toResolve.map(resourceAndOptions => this.doResolve(resourceAndOptions.resource, resourceAndOptions.options) - .then(stat => ({ stat, success: true }), error => ({ stat: undefined, success: false })))); - } - - createFolder(resource: uri): Promise { - - // 1.) Create folder - const absolutePath = this.toAbsolutePath(resource); - return pfs.mkdirp(absolutePath).then(() => { - - // 2.) Resolve - return this.doResolve(resource, { resolveMetadata: true }).then(result => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); - - return result; - }); - }); - } - - exists(resource: uri): Promise { - return this.resolve(resource).then(() => true, () => false); - } -} - -function etag(stat: fs.Stats): string; -function etag(size: number, mtime: number): string; -function etag(arg1: any, arg2?: any): string { - let size: number; - let mtime: number; - if (typeof arg2 === 'number') { - size = arg1; - mtime = arg2; - } else { - size = (arg1).size; - mtime = (arg1).mtime.getTime(); - } - - return `"${crypto.createHash('sha1').update(String(size) + String(mtime)).digest('hex')}"`; -} - -export class StatResolver { - private name: string; - private etag: string; - - constructor( - private resource: uri, - private isSymbolicLink: boolean, - private isDirectory: boolean, - private mtime: number, - private size: number, - private errorLogger?: (error: Error | string) => void - ) { - assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); - - this.name = getBaseLabel(resource); - this.etag = etag(size, mtime); - } - - resolve(options: IResolveFileOptions | undefined): Promise { - - // General Data - const fileStat: IFileStat = { - resource: this.resource, - isDirectory: this.isDirectory, - isSymbolicLink: this.isSymbolicLink, - isReadonly: false, - name: this.name, - etag: this.etag, - size: this.size, - mtime: this.mtime - }; - - // File Specific Data - if (!this.isDirectory) { - return Promise.resolve(fileStat); - } - - // Directory Specific Data - else { - - // Convert the paths from options.resolveTo to absolute paths - let absoluteTargetPaths: string[] | null = null; - if (options && options.resolveTo) { - absoluteTargetPaths = []; - for (const resource of options.resolveTo) { - absoluteTargetPaths.push(resource.fsPath); - } - } - - return new Promise(resolve => { - - // Load children - this.resolveChildren(this.resource.fsPath, absoluteTargetPaths, !!(options && options.resolveSingleChildDescendants), children => { - if (children) { - children = arrays.coalesce(children); // we don't want those null children (could be permission denied when reading a child) - } - fileStat.children = children || []; - - resolve(fileStat); - }); - }); - } - } - - private resolveChildren(absolutePath: string, absoluteTargetPaths: string[] | null, resolveSingleChildDescendants: boolean, callback: (children: IFileStat[] | null) => void): void { - extfs.readdir(absolutePath, (error: Error, files: string[]) => { - if (error) { - if (this.errorLogger) { - this.errorLogger(error); - } - - return callback(null); // return - we might not have permissions to read the folder - } - - // for each file in the folder - flow.parallel(files, (file: string, clb: (error: Error | null, children: IFileStat | null) => void) => { - const fileResource = uri.file(paths.resolve(absolutePath, file)); - let fileStat: fs.Stats; - let isSymbolicLink = false; - const $this = this; - - flow.sequence( - function onError(error: Error): void { - if ($this.errorLogger) { - $this.errorLogger(error); - } - - clb(null, null); // return - we might not have permissions to read the folder or stat the file - }, - - function stat(this: any): void { - extfs.statLink(fileResource.fsPath, this); - }, - - function countChildren(this: any, statAndLink: extfs.IStatAndLink): void { - fileStat = statAndLink.stat; - isSymbolicLink = statAndLink.isSymbolicLink; - - if (fileStat.isDirectory()) { - extfs.readdir(fileResource.fsPath, (error, result) => { - this(null, result ? result.length : 0); - }); - } else { - this(null, 0); - } - }, - - function resolve(childCount: number): void { - const childStat: IFileStat = { - resource: fileResource, - isDirectory: fileStat.isDirectory(), - isSymbolicLink, - isReadonly: false, - name: file, - mtime: fileStat.mtime.getTime(), - etag: etag(fileStat), - size: fileStat.size - }; - - // Return early for files - if (!fileStat.isDirectory()) { - return clb(null, childStat); - } - - // Handle Folder - let resolveFolderChildren = false; - if (files.length === 1 && resolveSingleChildDescendants) { - resolveFolderChildren = true; - } else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => isEqualOrParent(targetPath, fileResource.fsPath, !isLinux /* ignorecase */))) { - resolveFolderChildren = true; - } - - // Continue resolving children based on condition - if (resolveFolderChildren) { - $this.resolveChildren(fileResource.fsPath, absoluteTargetPaths, resolveSingleChildDescendants, children => { - if (children) { - children = arrays.coalesce(children); // we don't want those null children - } - childStat.children = children || []; - - clb(null, childStat); - }); - } - - // Otherwise return result - else { - clb(null, childStat); - } - }); - }, (errors, result) => { - callback(result); - }); - }); - } + //#endregion } diff --git a/src/vs/workbench/services/files/node/remoteFileService.ts b/src/vs/workbench/services/files/node/remoteFileService.ts index d86eed8e1a..8ca55746ae 100644 --- a/src/vs/workbench/services/files/node/remoteFileService.ts +++ b/src/vs/workbench/services/files/node/remoteFileService.ts @@ -3,185 +3,38 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; -import { TernarySearchTree } from 'vs/base/common/map'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IDecodeStreamOptions, toDecodeStream, encodeStream } from 'vs/base/node/encoding'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileStat, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IResolveFileOptions, IResolveFileResult, IStat, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, IWatchOptions, FileType, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata, IResolveMetadataFileOptions, etag } from 'vs/platform/files/common/files'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +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 TypeOnlyStat implements IStat { - - constructor(readonly type: FileType) { - // - } - - // todo@remote -> make a getter and warn when - // being used in development. - mtime: number = 0; - ctime: number = 0; - size: number = 0; -} - -function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): Promise { - const [resource, stat] = tuple; - const fileStat: IFileStat = { - resource, - name: resources.basename(resource), - isDirectory: (stat.type & FileType.Directory) !== 0, - isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0, - isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly), - mtime: stat.mtime, - size: stat.size, - etag: etag(stat.mtime, stat.size), - }; - - if (fileStat.isDirectory) { - if (recurse && recurse([resource, stat])) { - // dir -> resolve - return provider.readdir(resource).then(entries => { - // resolve children if requested - return Promise.all(entries.map(tuple => { - const [name, type] = tuple; - const childResource = resources.joinPath(resource, name); - return toIFileStat(provider, [childResource, new TypeOnlyStat(type)], recurse); - })).then(children => { - fileStat.children = children; - return fileStat; - }); - }); - } - } - - // file or (un-resolved) dir - return Promise.resolve(fileStat); -} - -export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], to?: URI[]): Promise { - - const trie = TernarySearchTree.forPaths(); - trie.set(tuple[0].toString(), true); - - if (isNonEmptyArray(to)) { - to.forEach(uri => trie.set(uri.toString(), true)); - } - - return toIFileStat(provider, tuple, candidate => { - return Boolean(trie.findSuperstr(candidate[0].toString()) || trie.get(candidate[0].toString())); - }); -} - -class WorkspaceWatchLogic extends Disposable { - - private _watches = new Map(); - - constructor( - private _fileService: RemoteFileService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - ) { - super(); - - this._refresh(); - - this._register(this._contextService.onDidChangeWorkspaceFolders(e => { - for (const removed of e.removed) { - this._unwatchWorkspace(removed.uri); - } - for (const added of e.added) { - this._watchWorkspace(added.uri); - } - })); - this._register(this._contextService.onDidChangeWorkbenchState(e => { - this._refresh(); - })); - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('files.watcherExclude')) { - this._refresh(); - } - })); - } - - dispose(): void { - this._unwatchWorkspaces(); - super.dispose(); - } - - private _refresh(): void { - this._unwatchWorkspaces(); - for (const folder of this._contextService.getWorkspace().folders) { - if (folder.uri.scheme !== Schemas.file) { - this._watchWorkspace(folder.uri); - } - } - } - - private _watchWorkspace(resource: URI) { - let excludes: string[] = []; - let config = this._configurationService.getValue({ resource }); - if (config.files && config.files.watcherExclude) { - for (const key in config.files.watcherExclude) { - if (config.files.watcherExclude[key] === true) { - excludes.push(key); - } - } - } - this._watches.set(resource.toString(), resource); - this._fileService.watch(resource, { recursive: true, excludes }); - } - - private _unwatchWorkspace(resource: URI) { - if (this._watches.has(resource.toString())) { - this._fileService.unwatch(resource); - this._watches.delete(resource.toString()); - } - } - - private _unwatchWorkspaces() { - this._watches.forEach(uri => this._fileService.unwatch(uri)); - this._watches.clear(); - } -} - -export class RemoteFileService extends FileService { +export class LegacyRemoteFileService extends LegacyFileService { private readonly _provider: Map; constructor( - @IFileService private readonly _fileService: IFileService, - @IStorageService storageService: IStorageService, + @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, - @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService contextService: IWorkspaceContextService, - @ILifecycleService lifecycleService: ILifecycleService, - @INotificationService notificationService: INotificationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, ) { super( + fileService, contextService, environmentService, - textResourceConfigurationService, - configurationService, - lifecycleService, - storageService, - notificationService + textResourceConfigurationService ); this._provider = new Map(); - this._register(new WorkspaceWatchLogic(this, configurationService, contextService)); } registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { @@ -201,7 +54,6 @@ export class RemoteFileService extends FileService { // --- stat private _withProvider(resource: URI): Promise { - if (!resources.isAbsolutePath(resource)) { throw new FileOperationError( localize('invalidPath', "The path of resource '{0}' must be absolute", resource.toString(true)), @@ -210,7 +62,7 @@ export class RemoteFileService extends FileService { } return Promise.all([ - this._fileService.activateProvider(resource.scheme) + this.fileService.activateProvider(resource.scheme) ]).then(() => { const provider = this._provider.get(resource.scheme); if (!provider) { @@ -223,48 +75,13 @@ export class RemoteFileService extends FileService { }); } - resolve(resource: URI, options: IResolveMetadataFileOptions): Promise; - resolve(resource: URI, options?: IResolveFileOptions): Promise; - resolve(resource: URI, options?: IResolveFileOptions): Promise { - if (resource.scheme === Schemas.file) { - return super.resolve(resource, options); - } else { - return this._doResolveFiles([{ resource, options }]).then(data => { - if (data.length !== 1 || !data[0].success) { - throw new FileOperationError( - localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), - FileOperationResult.FILE_NOT_FOUND - ); - } else { - return data[0].stat!; - } - }); - } - } - - private _doResolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise { - return this._withProvider(toResolve[0].resource).then(provider => { - let result: IResolveFileResult[] = []; - let promises = toResolve.map((item, idx) => { - return provider.stat(item.resource).then(stat => { - return toDeepIFileStat(provider, [item.resource, stat], item.options && item.options.resolveTo).then(fileStat => { - result[idx] = { stat: fileStat, success: true }; - }); - }, _err => { - result[idx] = { stat: undefined, success: false }; - }); - }); - return Promise.all(promises).then(() => result); - }); - } - // --- resolve resolveContent(resource: URI, options?: IResolveContentOptions): Promise { if (resource.scheme === Schemas.file) { return super.resolveContent(resource, options); } else { - return this._readFile(resource, options).then(RemoteFileService._asContent); + return this._readFile(resource, options).then(LegacyRemoteFileService._asContent); } } @@ -279,7 +96,7 @@ export class RemoteFileService extends FileService { private _readFile(resource: URI, options: IResolveContentOptions = Object.create(null)): Promise { return this._withProvider(resource).then(provider => { - return this.resolve(resource).then(fileStat => { + return this.fileService.resolve(resource).then(fileStat => { if (fileStat.isDirectory) { // todo@joh cannot copy a folder @@ -334,28 +151,6 @@ export class RemoteFileService extends FileService { // --- saving - private static async _mkdirp(provider: IFileSystemProvider, directory: URI): Promise { - - let basenames: string[] = []; - while (directory.path !== '/') { - try { - let stat = await provider.stat(directory); - if ((stat.type & FileType.Directory) === 0) { - throw new Error(`${directory.toString()} is not a directory`); - } - break; // we have hit a directory -> good - } catch (e) { - // ENOENT - basenames.push(resources.basename(directory)); - directory = resources.dirname(directory); - } - } - for (let i = basenames.length - 1; i >= 0; i--) { - directory = resources.joinPath(directory, basenames[i]); - await provider.mkdir(directory); - } - } - private static _throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED); @@ -368,9 +163,9 @@ export class RemoteFileService extends FileService { return super.createFile(resource, content, options); } else { - return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { + return this._withProvider(resource).then(LegacyRemoteFileService._throwIfFileSystemIsReadonly).then(provider => { - return RemoteFileService._mkdirp(provider, resources.dirname(resource)).then(() => { + return this.fileService.createFolder(resources.dirname(resource)).then(() => { const { encoding } = this.encoding.getWriteEncoding(resource); return this._writeFile(provider, resource, new StringSnapshot(content || ''), encoding, { create: true, overwrite: Boolean(options && options.overwrite) }); }); @@ -390,8 +185,8 @@ export class RemoteFileService extends FileService { if (resource.scheme === Schemas.file) { return super.updateContent(resource, value, options); } else { - return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { - return RemoteFileService._mkdirp(provider, resources.dirname(resource)).then(() => { + return this._withProvider(resource).then(LegacyRemoteFileService._throwIfFileSystemIsReadonly).then(provider => { + return this.fileService.createFolder(resources.dirname(resource)).then(() => { const snapshot = typeof value === 'string' ? new StringSnapshot(value) : value; return this._writeFile(provider, resource, snapshot, options && options.encoding, { create: true, overwrite: true }); }); @@ -409,7 +204,7 @@ export class RemoteFileService extends FileService { target.once('error', err => reject(err)); target.once('finish', (_: unknown) => resolve(undefined)); }).then(_ => { - return this.resolve(resource, { resolveMetadata: true }) as Promise; + return this.fileService.resolve(resource, { resolveMetadata: true }) as Promise; }); } @@ -430,41 +225,6 @@ export class RemoteFileService extends FileService { content.value.on('end', () => resolve(result)); }); } - - private _activeWatches = new Map, count: number }>(); - - watch(resource: URI, opts: IWatchOptions = { recursive: false, excludes: [] }): void { - if (resource.scheme === Schemas.file) { - return super.watch(resource); - } - - const key = resource.toString(); - const entry = this._activeWatches.get(key); - if (entry) { - entry.count += 1; - return; - } - - this._activeWatches.set(key, { - count: 1, - unwatch: this._withProvider(resource).then(provider => { - return provider.watch(resource, opts); - }, _err => { - return { dispose() { } }; - }) - }); - } - - unwatch(resource: URI): void { - if (resource.scheme === Schemas.file) { - return super.unwatch(resource); - } - let entry = this._activeWatches.get(resource.toString()); - if (entry && --entry.count === 0) { - entry.unwatch.then(dispose); - this._activeWatches.delete(resource.toString()); - } - } } -registerSingleton(ILegacyFileService, RemoteFileService); +registerSingleton(ILegacyFileService, LegacyRemoteFileService); \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts deleted file mode 100644 index 7caadc367b..0000000000 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts +++ /dev/null @@ -1,120 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; -import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; -import { WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc'; -import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { Event } from 'vs/base/common/event'; -import { IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; - -export class FileWatcher { - private static readonly MAX_RESTARTS = 5; - - private service: WatcherChannelClient; - private isDisposed: boolean; - private restartCounter: number; - private toDispose: IDisposable[] = []; - - constructor( - private contextService: IWorkspaceContextService, - private configurationService: IConfigurationService, - private onFileChanges: (changes: FileChangesEvent) => void, - private errorLogger: (msg: string) => void, - private verboseLogging: boolean, - ) { - this.isDisposed = false; - this.restartCounter = 0; - } - - public startWatching(): () => void { - const client = new Client( - getPathFromAmdModule(require, 'bootstrap-fork'), - { - serverName: 'File Watcher (nsfw)', - args: ['--type=watcherService'], - env: { - AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/nsfw/watcherApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: this.verboseLogging - } - } - ); - this.toDispose.push(client); - - client.onDidProcessExit(() => { - // our watcher app should never be completed because it keeps on watching. being in here indicates - // that the watcher process died and we want to restart it here. we only do it a max number of times - if (!this.isDisposed) { - if (this.restartCounter <= FileWatcher.MAX_RESTARTS) { - this.errorLogger('[FileWatcher] terminated unexpectedly and is restarted again...'); - this.restartCounter++; - this.startWatching(); - } else { - this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!'); - } - } - }, null, this.toDispose); - - // Initialize watcher - const channel = getNextTickChannel(client.getChannel('watcher')); - this.service = new WatcherChannelClient(channel); - - const options = { verboseLogging: this.verboseLogging }; - const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed); - - const onError = Event.filter(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string'); - onError(err => this.errorLogger(err.message), null, this.toDispose); - - const onFileChanges = Event.filter(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0); - onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose); - - // Start watching - this.updateFolders(); - this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateFolders())); - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('files.watcherExclude')) { - this.updateFolders(); - } - })); - - return () => this.dispose(); - } - - private updateFolders() { - if (this.isDisposed) { - return; - } - - this.service.setRoots(this.contextService.getWorkspace().folders.filter(folder => { - // Only workspace folders on disk - return folder.uri.scheme === Schemas.file; - }).map(folder => { - // Fetch the root's watcherExclude setting and return it - const configuration = this.configurationService.getValue({ - resource: folder.uri - }); - let ignored: string[] = []; - if (configuration.files && configuration.files.watcherExclude) { - ignored = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); - } - return { - basePath: folder.uri.fsPath, - ignored - }; - })); - } - - private dispose(): void { - this.isDisposed = true; - this.toDispose = dispose(this.toDispose); - } -} diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts deleted file mode 100644 index c3cc209ae1..0000000000 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts +++ /dev/null @@ -1,123 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; -import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; -import { WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc'; -import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Schemas } from 'vs/base/common/network'; -import { Event } from 'vs/base/common/event'; -import { IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; - -export class FileWatcher { - private static readonly MAX_RESTARTS = 5; - - private isDisposed: boolean; - private restartCounter: number; - private service: WatcherChannelClient; - private toDispose: IDisposable[]; - - constructor( - private contextService: IWorkspaceContextService, - private configurationService: IConfigurationService, - private onFileChanges: (changes: FileChangesEvent) => void, - private errorLogger: (msg: string) => void, - private verboseLogging: boolean - ) { - this.isDisposed = false; - this.restartCounter = 0; - this.toDispose = []; - } - - public startWatching(): () => void { - const args = ['--type=watcherService']; - - const client = new Client( - getPathFromAmdModule(require, 'bootstrap-fork'), - { - serverName: 'File Watcher (chokidar)', - args, - env: { - AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/unix/watcherApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: this.verboseLogging - } - } - ); - this.toDispose.push(client); - - client.onDidProcessExit(() => { - // our watcher app should never be completed because it keeps on watching. being in here indicates - // that the watcher process died and we want to restart it here. we only do it a max number of times - if (!this.isDisposed) { - if (this.restartCounter <= FileWatcher.MAX_RESTARTS) { - this.errorLogger('[FileWatcher] terminated unexpectedly and is restarted again...'); - this.restartCounter++; - this.startWatching(); - } else { - this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!'); - } - } - }, null, this.toDispose); - - const channel = getNextTickChannel(client.getChannel('watcher')); - this.service = new WatcherChannelClient(channel); - - const options = { verboseLogging: this.verboseLogging }; - const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed); - - const onError = Event.filter(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string'); - onError(err => this.errorLogger(err.message), null, this.toDispose); - - const onFileChanges = Event.filter(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0); - onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose); - - // Start watching - this.updateFolders(); - this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateFolders())); - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('files.watcherExclude')) { - this.updateFolders(); - } - })); - - return () => this.dispose(); - } - - private updateFolders() { - if (this.isDisposed) { - return; - } - - this.service.setRoots(this.contextService.getWorkspace().folders.filter(folder => { - // Only workspace folders on disk - return folder.uri.scheme === Schemas.file; - }).map(folder => { - // Fetch the root's watcherExclude setting and return it - const configuration = this.configurationService.getValue({ - resource: folder.uri - }); - let ignored: string[] = []; - if (configuration.files && configuration.files.watcherExclude) { - ignored = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); - } - return { - basePath: folder.uri.fsPath, - ignored, - recursive: false - }; - })); - } - - private dispose(): void { - this.isDisposed = true; - this.toDispose = dispose(this.toDispose); - } -} diff --git a/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts b/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts deleted file mode 100644 index dd28b94387..0000000000 --- a/src/vs/workbench/services/files/node/watcher/win32/watcherService.ts +++ /dev/null @@ -1,70 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IRawFileChange, toFileChangesEvent } from 'vs/workbench/services/files/node/watcher/common'; -import { OutOfProcessWin32FolderWatcher } from 'vs/workbench/services/files/node/watcher/win32/csharpWatcherService'; -import { FileChangesEvent } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { normalize, posix } from 'vs/base/common/path'; -import { rtrim, endsWith } from 'vs/base/common/strings'; -import { Schemas } from 'vs/base/common/network'; - -export class FileWatcher { - private isDisposed: boolean; - - constructor( - private contextService: IWorkspaceContextService, - private ignored: string[], - private onFileChanges: (changes: FileChangesEvent) => void, - private errorLogger: (msg: string) => void, - private verboseLogging: boolean - ) { - } - - public startWatching(): () => void { - if (this.contextService.getWorkspace().folders[0].uri.scheme !== Schemas.file) { - return () => { }; - } - let basePath: string = normalize(this.contextService.getWorkspace().folders[0].uri.fsPath); - - if (basePath && basePath.indexOf('\\\\') === 0 && endsWith(basePath, posix.sep)) { - // for some weird reason, node adds a trailing slash to UNC paths - // we never ever want trailing slashes as our base path unless - // someone opens root ("/"). - // See also https://github.com/nodejs/io.js/issues/1765 - basePath = rtrim(basePath, posix.sep); - } - - const watcher = new OutOfProcessWin32FolderWatcher( - basePath, - this.ignored, - events => this.onRawFileEvents(events), - error => this.onError(error), - this.verboseLogging - ); - - return () => { - this.isDisposed = true; - watcher.dispose(); - }; - } - - private onRawFileEvents(events: IRawFileChange[]): void { - if (this.isDisposed) { - return; - } - - // Emit through event emitter - if (events.length > 0) { - this.onFileChanges(toFileChangesEvent(events)); - } - } - - private onError(error: string): void { - if (!this.isDisposed) { - this.errorLogger(error); - } - } -} \ No newline at end of file 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 53aa54dce1..ed368102c5 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 @@ -7,23 +7,26 @@ import * as fs from 'fs'; import * as path from 'vs/base/common/path'; import * as os from 'os'; import * as assert from 'assert'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; -import { FileOperation, FileOperationEvent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; +import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; +import { FileOperation, FileOperationEvent, 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'; import * as encodingLib from 'vs/base/node/encoding'; -import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService, TestLifecycleService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService } from 'vs/workbench/test/workbenchTestServices'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IEncodingOverride } from 'vs/workbench/services/files/node/encoding'; import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { Schemas } from 'vs/base/common/network'; +import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider'; -suite('FileService', () => { - let service: FileService; +suite('LegacyFileService', () => { + let service: LegacyFileService; const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'fileservice'); let testDir: string; @@ -32,14 +35,22 @@ suite('FileService', () => { testDir = path.join(parentDir, id); const sourceDir = getPathFromAmdModule(require, './fixtures/service'); + const fileService = new FileService2(new NullLogService()); + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); + return pfs.copy(sourceDir, testDir).then(() => { - service = new FileService(new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); + service = new LegacyFileService( + fileService, + new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))), + TestEnvironmentService, + new TestTextResourceConfigurationService(), + ); }); }); teardown(() => { service.dispose(); - return pfs.del(parentDir, os.tmpdir()); + return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); test('createFile', () => { @@ -348,45 +359,6 @@ suite('FileService', () => { }); }); - test('watch', function (done) { - const toWatch = uri.file(path.join(testDir, 'index.html')); - - service.watch(toWatch); - - service.onFileChanges((e: FileChangesEvent) => { - assert.ok(e); - - service.unwatch(toWatch); - done(); - }); - - setTimeout(() => { - fs.writeFileSync(toWatch.fsPath, 'Changes'); - }, 100); - }); - - // test('watch - support atomic save', function (done) { - // const toWatch = uri.file(path.join(testDir, 'index.html')); - - // service.watch(toWatch); - - // service.onFileChanges((e: FileChangesEvent) => { - // assert.ok(e); - - // service.unwatch(toWatch); - // done(); - // }); - - // setTimeout(() => { - // // Simulate atomic save by deleting the file, creating it under different name - // // and then replacing the previously deleted file with those contents - // const renamed = `${toWatch.fsPath}.bak`; - // fs.unlinkSync(toWatch.fsPath); - // fs.writeFileSync(renamed, 'Changes'); - // fs.renameSync(renamed, toWatch.fsPath); - // }, 100); - // }); - test('options - encoding override (parent)', function () { // setup @@ -406,18 +378,16 @@ suite('FileService', () => { const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService); - const _service = new FileService( + 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, textResourceConfigurationService, - configurationService, - new TestLifecycleService(), - new TestStorageService(), - new TestNotificationService(), - { - encodingOverride, - disableWatcher: true - }); + { encodingOverride }); return _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).then(c => { assert.equal(c.encoding, 'windows1252'); @@ -451,18 +421,15 @@ suite('FileService', () => { const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService); - const _service = new FileService( + 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, textResourceConfigurationService, - configurationService, - new TestLifecycleService(), - new TestStorageService(), - new TestNotificationService(), - { - encodingOverride, - disableWatcher: true - }); + { encodingOverride }); return _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).then(c => { assert.equal(c.encoding, 'windows1252'); @@ -485,17 +452,15 @@ suite('FileService', () => { const _sourceDir = getPathFromAmdModule(require, './fixtures/service'); const resource = uri.file(path.join(testDir, 'index.html')); - const _service = new FileService( + 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(), - new TestConfigurationService(), - new TestLifecycleService(), - new TestStorageService(), - new TestNotificationService(), - { - disableWatcher: true - }); + new TestTextResourceConfigurationService() + ); return pfs.copy(_sourceDir, _testDir).then(() => { return pfs.readFile(resource.fsPath).then(data => { diff --git a/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts b/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts deleted file mode 100644 index 568352593b..0000000000 --- a/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as fs from 'fs'; -import * as path from 'vs/base/common/path'; -import * as assert from 'assert'; - -import { StatResolver } from 'vs/workbench/services/files/node/fileService'; -import { URI as uri } from 'vs/base/common/uri'; -import { isLinux } from 'vs/base/common/platform'; -import * as utils from 'vs/workbench/services/files/test/electron-browser/utils'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; - -function create(relativePath: string): StatResolver { - let basePath = getPathFromAmdModule(require, './fixtures/resolver'); - let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath; - let fsStat = fs.statSync(absolutePath); - - return new StatResolver(uri.file(absolutePath), fsStat.isSymbolicLink(), fsStat.isDirectory(), fsStat.mtime.getTime(), fsStat.size, undefined); -} - -function toResource(relativePath: string): uri { - let basePath = getPathFromAmdModule(require, './fixtures/resolver'); - let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath; - - return uri.file(absolutePath); -} - -suite('Stat Resolver', () => { - - test('resolve file', function () { - let resolver = create('/index.html'); - return resolver.resolve(undefined).then(result => { - assert.ok(!result.isDirectory); - assert.equal(result.name, 'index.html'); - assert.ok(!!result.etag); - - resolver = create('examples'); - return resolver.resolve(undefined).then(result => { - assert.ok(result.isDirectory); - }); - }); - }); - - test('resolve directory', function () { - let testsElements = ['examples', 'other', 'index.html', 'site.css']; - - let resolver = create('/'); - - return resolver.resolve(undefined).then(result => { - assert.ok(result); - assert.ok(result.children); - assert.ok(result.children!.length > 0); - assert.ok(result!.isDirectory); - assert.equal(result.children!.length, testsElements.length); - - assert.ok(result.children!.every((entry) => { - return testsElements.some((name) => { - return path.basename(entry.resource.fsPath) === name; - }); - })); - - result.children!.forEach((value) => { - assert.ok(path.basename(value.resource.fsPath)); - if (['examples', 'other'].indexOf(path.basename(value.resource.fsPath)) >= 0) { - assert.ok(value.isDirectory); - } else if (path.basename(value.resource.fsPath) === 'index.html') { - assert.ok(!value.isDirectory); - assert.ok(!value.children); - } else if (path.basename(value.resource.fsPath) === 'site.css') { - assert.ok(!value.isDirectory); - assert.ok(!value.children); - } else { - assert.ok(!'Unexpected value ' + path.basename(value.resource.fsPath)); - } - }); - }); - }); - - test('resolve directory - resolveTo single directory', function () { - let resolver = create('/'); - - return resolver.resolve({ resolveTo: [toResource('other/deep')] }).then(result => { - assert.ok(result); - assert.ok(result.children); - assert.ok(result.children!.length > 0); - assert.ok(result.isDirectory); - - const children = result.children!; - assert.equal(children.length, 4); - - const other = utils.getByName(result, 'other'); - assert.ok(other); - assert.ok(other!.children!.length > 0); - - const deep = utils.getByName(other!, 'deep'); - assert.ok(deep); - assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); - }); - }); - - test('resolve directory - resolveTo single directory - mixed casing', function () { - let resolver = create('/'); - - return resolver.resolve({ resolveTo: [toResource('other/Deep')] }).then(result => { - assert.ok(result); - assert.ok(result.children); - assert.ok(result.children!.length > 0); - assert.ok(result.isDirectory); - - const children = result.children; - assert.equal(children!.length, 4); - - const other = utils.getByName(result, 'other'); - assert.ok(other); - assert.ok(other!.children!.length > 0); - - const deep = utils.getByName(other!, 'deep'); - if (isLinux) { // Linux has case sensitive file system - assert.ok(deep); - assert.ok(!deep!.children); // not resolved because we got instructed to resolve other/Deep with capital D - } else { - assert.ok(deep); - assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); - } - }); - }); - - test('resolve directory - resolveTo multiple directories', function () { - let resolver = create('/'); - - return resolver.resolve({ resolveTo: [toResource('other/deep'), toResource('examples')] }).then(result => { - assert.ok(result); - assert.ok(result.children); - assert.ok(result.children!.length > 0); - assert.ok(result.isDirectory); - - const children = result.children!; - assert.equal(children.length, 4); - - const other = utils.getByName(result, 'other'); - assert.ok(other); - assert.ok(other!.children!.length > 0); - - const deep = utils.getByName(other!, 'deep'); - assert.ok(deep); - assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); - - const examples = utils.getByName(result, 'examples'); - assert.ok(examples); - assert.ok(examples!.children!.length > 0); - assert.equal(examples!.children!.length, 4); - }); - }); - - test('resolve directory - resolveSingleChildFolders', function () { - let resolver = create('/other'); - - return resolver.resolve({ resolveSingleChildDescendants: true }).then(result => { - assert.ok(result); - assert.ok(result.children); - assert.ok(result.children!.length > 0); - assert.ok(result.isDirectory); - - const children = result.children!; - assert.equal(children.length, 1); - - let deep = utils.getByName(result, 'deep'); - assert.ok(deep); - assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); - }); - }); -}); diff --git a/src/vs/workbench/services/files/test/electron-browser/utils.ts b/src/vs/workbench/services/files/test/electron-browser/utils.ts deleted file mode 100644 index f7f19ad6d9..0000000000 --- a/src/vs/workbench/services/files/test/electron-browser/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IFileStat } from 'vs/platform/files/common/files'; - -export function getByName(root: IFileStat, name: string): IFileStat | null { - if (root.children === undefined) { - return null; - } - - for (const child of root.children) { - if (child.name === name) { - return child; - } - } - - return null; -} \ 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 6c5f163b36..4d0d54465e 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable, toDisposable, combinedDisposable } 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 } from 'vs/platform/files/common/files'; +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 { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -14,17 +14,19 @@ 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'; export class FileService2 extends Disposable implements IFileService { //#region TODO@Ben HACKS - private _legacy: IFileService | null; + private _legacy: ILegacyFileService | null; + private joinOnLegacy: Promise; + private joinOnImplResolve: (service: ILegacyFileService) => void; - setLegacyService(legacy: IFileService): void { + setLegacyService(legacy: ILegacyFileService): void { this._legacy = this._register(legacy); - this._register(legacy.onFileChanges(e => this._onFileChanges.fire(e))); this._register(legacy.onAfterOperation(e => this._onAfterOperation.fire(e))); this.provider.forEach((provider, scheme) => { @@ -38,8 +40,7 @@ export class FileService2 extends Disposable implements IFileService { _serviceBrand: ServiceIdentifier; - private joinOnLegacy: Promise; - private joinOnImplResolve: (service: IFileService) => void; + private readonly BUFFER_SIZE = 16 * 1024; constructor(@ILogService private logService: ILogService) { super(); @@ -75,15 +76,19 @@ export class FileService2 extends Disposable implements IFileService { this.provider.set(scheme, provider); this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider }); - // Forward change events from provider - const providerFileListener = provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes))); + // Forward events from provider + const providerDisposables: IDisposable[] = []; + providerDisposables.push(provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes)))); + if (typeof provider.onDidErrorOccur === 'function') { + providerDisposables.push(provider.onDidErrorOccur(error => this._onError.fire(error))); + } return combinedDisposable([ toDisposable(() => { this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider }); this.provider.delete(scheme); - providerFileListener.dispose(); + dispose(providerDisposables); }), legacyDisposal ]); @@ -116,10 +121,10 @@ export class FileService2 extends Disposable implements IFileService { return this.provider.has(resource.scheme); } - async hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise { - const provider = await this.withProvider(resource); + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean { + const provider = this.provider.get(resource.scheme); - return !!(provider.capabilities & capability); + return !!(provider && (provider.capabilities & capability)); } private async withProvider(resource: URI): Promise { @@ -150,6 +155,9 @@ export class FileService2 extends Disposable implements IFileService { private _onAfterOperation: Emitter = this._register(new Emitter()); get onAfterOperation(): Event { return this._onAfterOperation.event; } + private _onError: Emitter = this._register(new Emitter()); + get onError(): Event { return this._onError.event; } + //#region File Metadata Resolving async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise; @@ -311,12 +319,12 @@ export class FileService2 extends Disposable implements IFileService { // create file: buffered if (hasOpenReadWriteCloseCapability(provider)) { - await this.doWriteBuffered(provider, resource, new TextEncoder().encode(content)); + await this.doWriteBuffered(provider, resource, VSBuffer.fromString(content || '')); } // create file: unbuffered else if (hasReadWriteCapability(provider)) { - await this.doWriteUnbuffered(provider, resource, new TextEncoder().encode(content), overwrite); + await this.doWriteUnbuffered(provider, resource, VSBuffer.fromString(content || ''), overwrite); } // give up if provider has insufficient capabilities @@ -586,19 +594,73 @@ export class FileService2 extends Disposable implements IFileService { private _onFileChanges: Emitter = this._register(new Emitter()); get onFileChanges(): Event { return this._onFileChanges.event; } - watch(resource: URI): void { - this.joinOnLegacy.then(legacy => legacy.watch(resource)); + private activeWatchers = new Map(); + + watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable { + let watchDisposed = false; + let watchDisposable = toDisposable(() => watchDisposed = true); + + // Watch and wire in disposable which is async but + // check if we got disposed meanwhile and forward + this.doWatch(resource, options).then(disposable => { + if (watchDisposed) { + dispose(disposable); + } else { + watchDisposable = disposable; + } + }, error => this.logService.error(error)); + + return toDisposable(() => dispose(watchDisposable)); } - unwatch(resource: URI): void { - this.joinOnLegacy.then(legacy => legacy.unwatch(resource)); + async doWatch(resource: URI, options: IWatchOptions): Promise { + const provider = await this.withProvider(resource); + const key = this.toWatchKey(provider, resource, options); + + // Only start watching if we are the first for the given key + const watcher = this.activeWatchers.get(key) || { count: 0, disposable: provider.watch(resource, options) }; + if (!this.activeWatchers.has(key)) { + this.activeWatchers.set(key, watcher); + } + + // Increment usage counter + watcher.count += 1; + + return toDisposable(() => { + + // Unref + watcher.count--; + + // Dispose only when last user is reached + if (watcher.count === 0) { + dispose(watcher.disposable); + this.activeWatchers.delete(key); + } + }); + } + + private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string { + const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + + return [ + isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase(), // lowercase path is the provider is case insensitive + String(options.recursive), // use recursive: true | false as part of the key + options.excludes.join() // use excludes as part of the key + ].join(); + } + + dispose(): void { + super.dispose(); + + this.activeWatchers.forEach(watcher => dispose(watcher.disposable)); + this.activeWatchers.clear(); } //#endregion //#region Helpers - private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, buffer: Uint8Array): Promise { + private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, buffer: VSBuffer): Promise { // open handle const handle = await provider.open(resource, { create: true }); @@ -613,16 +675,16 @@ export class FileService2 extends Disposable implements IFileService { } } - private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: Uint8Array, length: number, posInFile: number, posInBuffer: number): Promise { + private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise { let totalBytesWritten = 0; while (totalBytesWritten < length) { - const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten); + const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten); totalBytesWritten += bytesWritten; } } - private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, buffer: Uint8Array, overwrite: boolean): Promise { - return provider.writeFile(resource, buffer, { create: true, overwrite }); + private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, buffer: VSBuffer, overwrite: boolean): Promise { + return provider.writeFile(resource, buffer.buffer, { create: true, overwrite }); } private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { @@ -635,7 +697,7 @@ export class FileService2 extends Disposable implements IFileService { sourceHandle = await sourceProvider.open(source, { create: false }); targetHandle = await targetProvider.open(target, { create: true }); - const buffer = new Uint8Array(16 * 1024); + const buffer = VSBuffer.alloc(this.BUFFER_SIZE); let posInFile = 0; let posInBuffer = 0; @@ -643,7 +705,7 @@ export class FileService2 extends Disposable implements IFileService { do { // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength). - bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer, posInBuffer, buffer.byteLength - posInBuffer); + bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer); // write into target (targetHandle) at current position (posInFile) from buffer (buffer) at // buffer position (posInBuffer) all bytes we read (bytesRead). @@ -653,7 +715,7 @@ export class FileService2 extends Disposable implements IFileService { posInBuffer += bytesRead; // when buffer full, fill it again from the beginning - if (posInBuffer === buffer.length) { + if (posInBuffer === buffer.byteLength) { posInBuffer = 0; } } while (bytesRead > 0); @@ -679,7 +741,7 @@ export class FileService2 extends Disposable implements IFileService { // Read entire buffer from source and write buffered try { const buffer = await sourceProvider.readFile(source); - await this.doWriteBuffer(targetProvider, targetHandle, buffer, buffer.byteLength, 0, 0); + await this.doWriteBuffer(targetProvider, targetHandle, VSBuffer.wrap(buffer), buffer.byteLength, 0, 0); } catch (error) { throw error; } finally { @@ -689,27 +751,38 @@ export class FileService2 extends Disposable implements IFileService { private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI, overwrite: boolean): Promise { - // Determine file size - const size = (await this.resolve(source, { resolveMetadata: true })).size; - // Open handle const sourceHandle = await sourceProvider.open(source, { create: false }); try { - const buffer = new Uint8Array(size); + const buffers: VSBuffer[] = []; - let pos = 0; + let buffer = VSBuffer.alloc(this.BUFFER_SIZE); + + let posInFile = 0; + let totalBytesRead = 0; let bytesRead = 0; + let posInBuffer = 0; do { - // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at + // read from source (sourceHandle) at current position (pos) into buffer (buffer) at // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength). - bytesRead = await sourceProvider.read(sourceHandle, pos, buffer, pos, buffer.byteLength - pos); + bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer); - pos += bytesRead; - } while (bytesRead > 0 && pos < size); + posInFile += bytesRead; + posInBuffer += bytesRead; + totalBytesRead += bytesRead; + + // when buffer full, create a new one + if (posInBuffer === buffer.byteLength) { + buffers.push(buffer); + buffer = VSBuffer.alloc(this.BUFFER_SIZE); + + posInBuffer = 0; + } + } while (bytesRead > 0); // Write buffer into target at once - await this.doWriteUnbuffered(targetProvider, target, buffer, overwrite); + await this.doWriteUnbuffered(targetProvider, target, VSBuffer.concat([...buffers, buffer.slice(0, posInBuffer)], totalBytesRead), overwrite); } catch (error) { throw error; } finally { diff --git a/src/vs/workbench/services/files2/common/workspaceWatcher.ts b/src/vs/workbench/services/files2/common/workspaceWatcher.ts new file mode 100644 index 0000000000..28f9e25d90 --- /dev/null +++ b/src/vs/workbench/services/files2/common/workspaceWatcher.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { IFilesConfiguration, IFileService } from 'vs/platform/files/common/files'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { ResourceMap } from 'vs/base/common/map'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { localize } from 'vs/nls'; +import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; + +export class WorkspaceWatcher extends Disposable { + + private watches = new ResourceMap(); + + constructor( + @IFileService private readonly fileService: FileService2, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @INotificationService private readonly notificationService: INotificationService, + @IStorageService private readonly storageService: IStorageService + ) { + super(); + + this.registerListeners(); + + this.refresh(); + } + + private registerListeners(): void { + this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onDidChangeWorkspaceFolders(e))); + this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState())); + this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidChangeConfiguration(e))); + this._register(this.fileService.onError(error => this.onError(error))); + } + + private onDidChangeWorkspaceFolders(e: IWorkspaceFoldersChangeEvent): void { + + // Removed workspace: Unwatch + for (const removed of e.removed) { + this.unwatchWorkspace(removed.uri); + } + + // Added workspace: Watch + for (const added of e.added) { + this.watchWorkspace(added.uri); + } + } + + private onDidChangeWorkbenchState(): void { + this.refresh(); + } + + private onDidChangeConfiguration(e: IConfigurationChangeEvent): void { + if (e.affectsConfiguration('files.watcherExclude')) { + this.refresh(); + } + } + + private onError(error: Error): void { + const msg = error.toString(); + + // Forward to unexpected error handler + onUnexpectedError(msg); + + // Detect if we run < .NET Framework 4.5 + if (msg.indexOf('System.MissingMethodException') >= 0 && !this.storageService.getBoolean('ignoreNetVersionError', StorageScope.WORKSPACE)) { + this.notificationService.prompt( + Severity.Warning, + localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."), + [{ + label: localize('installNet', "Download .NET Framework 4.5"), + run: () => window.open('https://go.microsoft.com/fwlink/?LinkId=786533') + }, + { + label: localize('neverShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => this.storageService.store('ignoreNetVersionError', true, StorageScope.WORKSPACE) + }], + { sticky: true } + ); + } + + // Detect if we run into ENOSPC issues + if (msg.indexOf('ENOSPC') >= 0 && !this.storageService.getBoolean('ignoreEnospcError', StorageScope.WORKSPACE)) { + this.notificationService.prompt( + Severity.Warning, + localize('enospcError', "Unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue."), + [{ + label: localize('learnMore', "Instructions"), + run: () => window.open('https://go.microsoft.com/fwlink/?linkid=867693') + }, + { + label: localize('neverShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => this.storageService.store('ignoreEnospcError', true, StorageScope.WORKSPACE) + }], + { sticky: true } + ); + } + } + + private watchWorkspace(resource: URI) { + + // Compute the watcher exclude rules from configuration + const excludes: string[] = []; + const config = this.configurationService.getValue({ resource }); + if (config.files && config.files.watcherExclude) { + for (const key in config.files.watcherExclude) { + if (config.files.watcherExclude[key] === true) { + excludes.push(key); + } + } + } + + // Watch workspace + const disposable = this.fileService.watch(resource, { recursive: true, excludes }); + this.watches.set(resource, disposable); + } + + private unwatchWorkspace(resource: URI) { + if (this.watches.has(resource)) { + dispose(this.watches.get(resource)); + this.watches.delete(resource); + } + } + + private refresh(): void { + + // Unwatch all first + this.unwatchWorkspaces(); + + // Watch each workspace folder + for (const folder of this.contextService.getWorkspace().folders) { + this.watchWorkspace(folder.uri); + } + } + + private unwatchWorkspaces() { + this.watches.forEach(disposable => dispose(disposable)); + this.watches.clear(); + } + + dispose(): void { + super.dispose(); + + this.unwatchWorkspaces(); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceWatcher, LifecyclePhase.Restored); \ No newline at end of file diff --git a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts index 1bc40210e5..0784880efa 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -4,20 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { mkdir, open, close, read, write } from 'fs'; -import { tmpdir } from 'os'; import { promisify } from 'util'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import { statLink, readdir, unlink, del, move, copy, readFile, writeFile, fileExists, truncate } from 'vs/base/node/pfs'; +import { statLink, readdir, unlink, move, copy, readFile, writeFile, fileExists, truncate, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { normalize, basename, dirname } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; -import { retry } from 'vs/base/common/async'; -import { ILogService } from 'vs/platform/log/common/log'; +import { retry, ThrottledDelayer } from 'vs/base/common/async'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { localize } from 'vs/nls'; +import { IDiskFileChange, toFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher'; +import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files2/node/watcher/unix/watcherService'; +import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files2/node/watcher/win32/watcherService'; +import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files2/node/watcher/nsfw/watcherService'; +import { FileWatcher as NodeJSWatcherService } from 'vs/workbench/services/files2/node/watcher/nodejs/watcherService'; export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider { @@ -65,7 +69,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro ctime: stat.ctime.getTime(), mtime: stat.mtime.getTime(), size: stat.size - } as IStat; + }; } catch (error) { throw this.toFileSystemProviderError(error); } @@ -231,7 +235,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise { if (opts.recursive) { - await del(filePath, tmpdir()); + await rimraf(filePath, RimRafMode.MOVE); } else { await unlink(filePath); } @@ -304,11 +308,108 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro //#region File Watching + private _onDidWatchErrorOccur: Emitter = this._register(new Emitter()); + get onDidErrorOccur(): Event { return this._onDidWatchErrorOccur.event; } + private _onDidChangeFile: Emitter = this._register(new Emitter()); get onDidChangeFile(): Event { return this._onDidChangeFile.event; } + private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined; + private recursiveFoldersToWatch: { path: string, excludes: string[] }[] = []; + private recursiveWatchRequestDelayer: ThrottledDelayer = this._register(new ThrottledDelayer(0)); + watch(resource: URI, opts: IWatchOptions): IDisposable { - throw new Error('Method not implemented.'); + if (opts.recursive) { + return this.watchRecursive(resource, opts.excludes); + } + + return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases + } + + private watchRecursive(resource: URI, excludes: string[]): IDisposable { + + // Add to list of folders to watch recursively + const folderToWatch = { path: this.toFilePath(resource), excludes }; + this.recursiveFoldersToWatch.push(folderToWatch); + + // Trigger update + this.refreshRecursiveWatchers(); + + return toDisposable(() => { + + // Remove from list of folders to watch recursively + this.recursiveFoldersToWatch.splice(this.recursiveFoldersToWatch.indexOf(folderToWatch), 1); + + // Trigger update + this.refreshRecursiveWatchers(); + }); + } + + private refreshRecursiveWatchers(): void { + + // Buffer requests for recursive watching to decide on right watcher + // that supports potentially watching more than one folder at once + this.recursiveWatchRequestDelayer.trigger(() => { + this.doRefreshRecursiveWatchers(); + + return Promise.resolve(); + }); + } + + private doRefreshRecursiveWatchers(): void { + + // Reuse existing + if (this.recursiveWatcher instanceof NsfwWatcherService) { + this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch); + } + + // Create new + else { + + // 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 + }; + + // Single Folder Watcher + if (this.recursiveFoldersToWatch.length === 1) { + if (isWindows) { + watcherImpl = WindowsWatcherService; + } else { + watcherImpl = UnixWatcherService; + } + } + + // Multi Folder Watcher + else { + watcherImpl = NsfwWatcherService; + } + + // Create and start watching + this.recursiveWatcher = new watcherImpl( + this.recursiveFoldersToWatch, + event => this._onDidChangeFile.fire(toFileChanges(event)), + error => this._onDidWatchErrorOccur.fire(new Error(error)), + this.logService.getLevel() === LogLevel.Trace + ); + } + } + + private watchNonRecursive(resource: URI): IDisposable { + return new NodeJSWatcherService( + this.toFilePath(resource), + changes => this._onDidChangeFile.fire(toFileChanges(changes)), + error => this._onDidWatchErrorOccur.fire(new Error(error)), + info => this.logService.trace(info), + this.logService.getLevel() === LogLevel.Trace + ); } //#endregion @@ -347,4 +448,11 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro } //#endregion + + dispose(): void { + super.dispose(); + + dispose(this.recursiveWatcher); + this.recursiveWatcher = undefined; + } } diff --git a/src/vs/workbench/services/files2/node/watcher/nodejs/watcherService.ts b/src/vs/workbench/services/files2/node/watcher/nodejs/watcherService.ts new file mode 100644 index 0000000000..0afbd0f8d8 --- /dev/null +++ b/src/vs/workbench/services/files2/node/watcher/nodejs/watcherService.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { statLink, readlink } from 'vs/base/node/pfs'; +import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher'; +import { FileChangeType } from 'vs/platform/files/common/files'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { join, basename } from 'vs/base/common/path'; + +export class FileWatcher extends Disposable { + private isDisposed: boolean; + + private fileChangesDelayer: ThrottledDelayer = this._register(new ThrottledDelayer(CHANGE_BUFFER_DELAY * 2 /* sync on delay from underlying library */)); + private fileChangesBuffer: IDiskFileChange[] = []; + + constructor( + private path: string, + private onFileChanges: (changes: IDiskFileChange[]) => void, + private errorLogger: (msg: string) => void, + private verboseLogger: (msg: string) => void, + private verboseLogging: boolean + ) { + super(); + + this.startWatching(); + } + + private async startWatching(): Promise { + try { + const { stat, isSymbolicLink } = await statLink(this.path); + + if (this.isDisposed) { + return; + } + + let pathToWatch = this.path; + if (isSymbolicLink) { + try { + pathToWatch = await readlink(pathToWatch); + } catch (error) { + this.onError(error); + } + } + + // Watch Folder + if (stat.isDirectory()) { + this._register(watchFolder(pathToWatch, (eventType, path) => { + this.onFileChange({ + type: eventType === 'changed' ? FileChangeType.UPDATED : eventType === 'added' ? FileChangeType.ADDED : FileChangeType.DELETED, + path: join(this.path, basename(path)) // ensure path is identical with what was passed in + }); + }, error => this.onError(error))); + } + + // Watch File + else { + this._register(watchFile(pathToWatch, eventType => { + this.onFileChange({ + type: eventType === 'changed' ? FileChangeType.UPDATED : FileChangeType.DELETED, + path: this.path // ensure path is identical with what was passed in + }); + }, error => this.onError(error))); + } + } catch (error) { + this.onError(error); + } + } + + private onFileChange(event: IDiskFileChange): void { + + // Add to buffer + this.fileChangesBuffer.push(event); + + // Logging + if (this.verboseLogging) { + this.onVerbose(`[File Watcher (node.js)] ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); + } + + // Handle emit through delayer to accommodate for bulk changes and thus reduce spam + this.fileChangesDelayer.trigger(() => { + const fileChanges = this.fileChangesBuffer; + this.fileChangesBuffer = []; + + // Event normalization + const normalizedFileChanges = normalizeFileChanges(fileChanges); + + // Logging + if (this.verboseLogging) { + normalizedFileChanges.forEach(event => { + this.onVerbose(`[File Watcher (node.js)] >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); + }); + } + + // Fire + if (normalizedFileChanges.length > 0) { + this.onFileChanges(normalizedFileChanges); + } + + return Promise.resolve(); + }); + } + + private onError(error: string): void { + if (!this.isDisposed) { + this.errorLogger(error); + } + } + + private onVerbose(msg: string): void { + if (!this.isDisposed) { + this.verboseLogger(msg); + } + } + + dispose(): void { + this.isDisposed = true; + + super.dispose(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/workbench/services/files2/node/watcher/nsfw/nsfwWatcherService.ts similarity index 82% rename from src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts rename to src/vs/workbench/services/files2/node/watcher/nsfw/nsfwWatcherService.ts index 51e019d3f5..7c28ffdc13 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/workbench/services/files2/node/watcher/nsfw/nsfwWatcherService.ts @@ -7,14 +7,14 @@ import * as glob from 'vs/base/common/glob'; import * as extpath from 'vs/base/common/extpath'; import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; -import * as watcher from 'vs/workbench/services/files/node/watcher/common'; +import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher'; import * as nsfw from 'vscode-nsfw'; -import { IWatcherService, IWatcherRequest, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; +import { IWatcherService, IWatcherRequest, IWatcherOptions, IWatchError } from 'vs/workbench/services/files2/node/watcher/nsfw/watcher'; import { ThrottledDelayer } from 'vs/base/common/async'; import { FileChangeType } from 'vs/platform/files/common/files'; import { normalizeNFC } from 'vs/base/common/normalization'; import { Event, Emitter } from 'vs/base/common/event'; -import { realcaseSync, realpathSync } from 'vs/base/node/extfs'; +import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; const nsfwActionToRawChangeType: { [key: number]: number } = []; nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED; @@ -39,22 +39,22 @@ export class NsfwWatcherService implements IWatcherService { private _verboseLogging: boolean; private enospcErrorLogged: boolean; - private _onWatchEvent = new Emitter(); + private _onWatchEvent = new Emitter(); readonly onWatchEvent = this._onWatchEvent.event; - watch(options: IWatcherOptions): Event { + watch(options: IWatcherOptions): Event { this._verboseLogging = options.verboseLogging; return this.onWatchEvent; } private _watch(request: IWatcherRequest): void { - let undeliveredFileEvents: watcher.IRawFileChange[] = []; + let undeliveredFileEvents: IDiskFileChange[] = []; const fileEventDelayer = new ThrottledDelayer(NsfwWatcherService.FS_EVENT_DELAY); let readyPromiseResolve: (watcher: IWatcherObjet) => void; - this._pathWatchers[request.basePath] = { + this._pathWatchers[request.path] = { ready: new Promise(resolve => readyPromiseResolve = resolve), - ignored: Array.isArray(request.ignored) ? request.ignored.map(ignored => glob.parse(ignored)) : [] + ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => glob.parse(ignored)) : [] }; process.on('uncaughtException', (e: Error | string) => { @@ -75,30 +75,30 @@ export class NsfwWatcherService implements IWatcherService { // - the path is a symbolic link // We have to detect this case and massage the events to correct this. let realBasePathDiffers = false; - let realBasePathLength = request.basePath.length; + let realBasePathLength = request.path.length; if (platform.isMacintosh) { try { // First check for symbolic link - let realBasePath = realpathSync(request.basePath); + let realBasePath = realpathSync(request.path); // Second check for casing difference - if (request.basePath === realBasePath) { - realBasePath = (realcaseSync(request.basePath) || request.basePath); + if (request.path === realBasePath) { + realBasePath = (realcaseSync(request.path) || request.path); } - if (request.basePath !== realBasePath) { + if (request.path !== realBasePath) { realBasePathLength = realBasePath.length; realBasePathDiffers = true; - console.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.basePath}, real: ${realBasePath})`); + console.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`); } } catch (error) { // ignore } } - nsfw(request.basePath, events => { + nsfw(request.path, events => { for (const e of events) { // Logging if (this._verboseLogging) { @@ -111,20 +111,20 @@ export class NsfwWatcherService implements IWatcherService { if (e.action === nsfw.actions.RENAMED) { // Rename fires when a file's name changes within a single directory absolutePath = path.join(e.directory, e.oldFile || ''); - if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) { + if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); } else if (this._verboseLogging) { console.log(' >> ignored', absolutePath); } absolutePath = path.join(e.directory, e.newFile || ''); - if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) { + if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); } else if (this._verboseLogging) { console.log(' >> ignored', absolutePath); } } else { absolutePath = path.join(e.directory, e.file || ''); - if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) { + if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: nsfwActionToRawChangeType[e.action], path: absolutePath @@ -148,13 +148,13 @@ export class NsfwWatcherService implements IWatcherService { // Convert paths back to original form in case it differs if (realBasePathDiffers) { - e.path = request.basePath + e.path.substr(realBasePathLength); + e.path = request.path + e.path.substr(realBasePathLength); } }); } // Broadcast to clients normalized - const res = watcher.normalize(events); + const res = normalizeFileChanges(events); this._onWatchEvent.fire(res); // Logging @@ -167,7 +167,7 @@ export class NsfwWatcherService implements IWatcherService { return Promise.resolve(undefined); }); }).then(watcher => { - this._pathWatchers[request.basePath].watcher = watcher; + this._pathWatchers[request.path].watcher = watcher; const startPromise = watcher.start(); startPromise.then(() => readyPromiseResolve(watcher)); return startPromise; @@ -180,17 +180,17 @@ export class NsfwWatcherService implements IWatcherService { // Gather roots that are not currently being watched const rootsToStartWatching = normalizedRoots.filter(r => { - return !(r.basePath in this._pathWatchers); + return !(r.path in this._pathWatchers); }); // Gather current roots that don't exist in the new roots array const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => { - return normalizedRoots.every(normalizedRoot => normalizedRoot.basePath !== r); + return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r); }); // Logging if (this._verboseLogging) { - console.log(`Start watching: [${rootsToStartWatching.map(r => r.basePath).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`); + console.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`); } // Stop watching some roots @@ -204,8 +204,8 @@ export class NsfwWatcherService implements IWatcherService { // Refresh ignored arrays in case they changed roots.forEach(root => { - if (root.basePath in this._pathWatchers) { - this._pathWatchers[root.basePath].ignored = Array.isArray(root.ignored) ? root.ignored.map(ignored => glob.parse(ignored)) : []; + if (root.path in this._pathWatchers) { + this._pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : []; } }); @@ -233,7 +233,7 @@ export class NsfwWatcherService implements IWatcherService { */ protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] { return roots.filter(r => roots.every(other => { - return !(r.basePath.length > other.basePath.length && extpath.isEqualOrParent(r.basePath, other.basePath)); + return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path)); })); } diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts b/src/vs/workbench/services/files2/node/watcher/nsfw/test/nsfwWatcherService.test.ts similarity index 88% rename from src/vs/workbench/services/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts rename to src/vs/workbench/services/files2/node/watcher/nsfw/test/nsfwWatcherService.test.ts index 1e3535e965..ad9b5b76cf 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts +++ b/src/vs/workbench/services/files2/node/watcher/nsfw/test/nsfwWatcherService.test.ts @@ -6,16 +6,16 @@ import * as assert from 'assert'; import * as platform from 'vs/base/common/platform'; -import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService'; -import { IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher'; +import { NsfwWatcherService } from 'vs/workbench/services/files2/node/watcher/nsfw/nsfwWatcherService'; +import { IWatcherRequest } from 'vs/workbench/services/files2/node/watcher/nsfw/watcher'; class TestNsfwWatcherService extends NsfwWatcherService { public normalizeRoots(roots: string[]): string[] { // Work with strings as paths to simplify testing const requests: IWatcherRequest[] = roots.map(r => { - return { basePath: r, ignored: [] }; + return { path: r, excludes: [] }; }); - return this._normalizeRoots(requests).map(r => r.basePath); + return this._normalizeRoots(requests).map(r => r.path); } } diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcher.ts b/src/vs/workbench/services/files2/node/watcher/nsfw/watcher.ts similarity index 78% rename from src/vs/workbench/services/files/node/watcher/unix/watcher.ts rename to src/vs/workbench/services/files2/node/watcher/nsfw/watcher.ts index 1816bf5c84..338de0c7ba 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcher.ts +++ b/src/vs/workbench/services/files2/node/watcher/nsfw/watcher.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; export interface IWatcherRequest { - basePath: string; - ignored: string[]; + path: string; + excludes: string[]; } export interface IWatcherOptions { @@ -20,7 +20,7 @@ export interface IWatchError { } export interface IWatcherService { - watch(options: IWatcherOptions): Event; + watch(options: IWatcherOptions): Event; setRoots(roots: IWatcherRequest[]): Promise; setVerboseLogging(enabled: boolean): Promise; stop(): Promise; diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherApp.ts b/src/vs/workbench/services/files2/node/watcher/nsfw/watcherApp.ts similarity index 69% rename from src/vs/workbench/services/files/node/watcher/nsfw/watcherApp.ts rename to src/vs/workbench/services/files2/node/watcher/nsfw/watcherApp.ts index 1b2dee5797..f2a81059ad 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherApp.ts +++ b/src/vs/workbench/services/files2/node/watcher/nsfw/watcherApp.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; -import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc'; -import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService'; +import { WatcherChannel } from 'vs/workbench/services/files2/node/watcher/nsfw/watcherIpc'; +import { NsfwWatcherService } from 'vs/workbench/services/files2/node/watcher/nsfw/nsfwWatcherService'; const server = new Server('watcher'); const service = new NsfwWatcherService(); diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts b/src/vs/workbench/services/files2/node/watcher/nsfw/watcherIpc.ts similarity index 90% rename from src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts rename to src/vs/workbench/services/files2/node/watcher/nsfw/watcherIpc.ts index d88f64255c..588acd82d9 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts +++ b/src/vs/workbench/services/files2/node/watcher/nsfw/watcherIpc.ts @@ -6,7 +6,7 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher'; import { Event } from 'vs/base/common/event'; -import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; export class WatcherChannel implements IServerChannel { @@ -35,7 +35,7 @@ export class WatcherChannelClient implements IWatcherService { constructor(private channel: IChannel) { } - watch(options: IWatcherOptions): Event { + watch(options: IWatcherOptions): Event { return this.channel.listen('watch', options); } diff --git a/src/vs/workbench/services/files2/node/watcher/nsfw/watcherService.ts b/src/vs/workbench/services/files2/node/watcher/nsfw/watcherService.ts new file mode 100644 index 0000000000..c713fcfcd9 --- /dev/null +++ b/src/vs/workbench/services/files2/node/watcher/nsfw/watcherService.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; +import { WatcherChannelClient } from 'vs/workbench/services/files2/node/watcher/nsfw/watcherIpc'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { IWatchError, IWatcherRequest } from 'vs/workbench/services/files2/node/watcher/nsfw/watcher'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; + +export class FileWatcher extends Disposable { + private static readonly MAX_RESTARTS = 5; + + private service: WatcherChannelClient; + private isDisposed: boolean; + private restartCounter: number; + + constructor( + private folders: IWatcherRequest[], + private onFileChanges: (changes: IDiskFileChange[]) => void, + private errorLogger: (msg: string) => void, + private verboseLogging: boolean, + ) { + super(); + + this.isDisposed = false; + this.restartCounter = 0; + + this.startWatching(); + } + + private startWatching(): void { + const client = this._register(new Client( + getPathFromAmdModule(require, 'bootstrap-fork'), + { + serverName: 'File Watcher (nsfw)', + args: ['--type=watcherService'], + env: { + AMD_ENTRYPOINT: 'vs/workbench/services/files2/node/watcher/nsfw/watcherApp', + PIPE_LOGGING: 'true', + VERBOSE_LOGGING: this.verboseLogging + } + } + )); + + this._register(client.onDidProcessExit(() => { + // our watcher app should never be completed because it keeps on watching. being in here indicates + // that the watcher process died and we want to restart it here. we only do it a max number of times + if (!this.isDisposed) { + if (this.restartCounter <= FileWatcher.MAX_RESTARTS) { + this.errorLogger('[File Watcher (nsfw)] terminated unexpectedly and is restarted again...'); + this.restartCounter++; + this.startWatching(); + } else { + this.errorLogger('[File Watcher (nsfw)] failed to start after retrying for some time, giving up. Please report this as a bug report!'); + } + } + })); + + // Initialize watcher + const channel = getNextTickChannel(client.getChannel('watcher')); + this.service = new WatcherChannelClient(channel); + + const options = { verboseLogging: this.verboseLogging }; + const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed); + + const onError = Event.filter(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string'); + this._register(onError(err => this.errorLogger(`[File Watcher (nsfw)] ${err.message}`))); + + const onFileChanges = Event.filter(onWatchEvent, (e): e is IDiskFileChange[] => Array.isArray(e) && e.length > 0); + this._register(onFileChanges(e => this.onFileChanges(e))); + + // Start watching + this.setFolders(this.folders); + } + + setFolders(folders: IWatcherRequest[]): void { + this.folders = folders; + + this.service.setRoots(folders); + } + + dispose(): void { + this.isDisposed = true; + + super.dispose(); + } +} diff --git a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/workbench/services/files2/node/watcher/unix/chokidarWatcherService.ts similarity index 88% rename from src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts rename to src/vs/workbench/services/files2/node/watcher/unix/chokidarWatcherService.ts index 1f99ef66f1..c5e0f97a15 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/workbench/services/files2/node/watcher/unix/chokidarWatcherService.ts @@ -12,10 +12,10 @@ import * as glob from 'vs/base/common/glob'; import { FileChangeType } from 'vs/platform/files/common/files'; import { ThrottledDelayer } from 'vs/base/common/async'; import { normalizeNFC } from 'vs/base/common/normalization'; -import { realcaseSync } from 'vs/base/node/extfs'; +import { realcaseSync } from 'vs/base/node/extpath'; import { isMacintosh } from 'vs/base/common/platform'; -import * as watcherCommon from 'vs/workbench/services/files/node/watcher/common'; -import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher'; +import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from 'vs/workbench/services/files2/node/watcher/unix/watcher'; import { Emitter, Event } from 'vs/base/common/event'; interface IWatcher { @@ -46,10 +46,10 @@ export class ChokidarWatcherService implements IWatcherService { private spamWarningLogged: boolean; private enospcErrorLogged: boolean; - private _onWatchEvent = new Emitter(); + private _onWatchEvent = new Emitter(); readonly onWatchEvent = this._onWatchEvent.event; - public watch(options: IWatcherOptions & IChockidarWatcherOptions): Event { + public watch(options: IWatcherOptions & IChockidarWatcherOptions): Event { this._verboseLogging = options.verboseLogging; this._pollingInterval = options.pollingInterval; this._watchers = Object.create(null); @@ -117,7 +117,7 @@ export class ChokidarWatcherService implements IWatcherService { // if there's only one request, use the built-in ignore-filterering const isSingleFolder = requests.length === 1; if (isSingleFolder) { - watcherOpts.ignored = requests[0].ignored; + watcherOpts.ignored = requests[0].excludes; } // Chokidar fails when the basePath does not match case-identical to the path on disk @@ -139,7 +139,7 @@ export class ChokidarWatcherService implements IWatcherService { console.error('Watcher is not using native fsevents library and is falling back to unefficient polling.'); } - let undeliveredFileEvents: watcherCommon.IRawFileChange[] = []; + let undeliveredFileEvents: IDiskFileChange[] = []; let fileEventDelayer: ThrottledDelayer | null = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY); const watcher: IWatcher = { @@ -232,7 +232,7 @@ export class ChokidarWatcherService implements IWatcherService { undeliveredFileEvents = []; // Broadcast to clients normalized - const res = watcherCommon.normalize(events); + const res = normalizeFileChanges(events); this._onWatchEvent.fire(res); // Logging @@ -247,7 +247,7 @@ export class ChokidarWatcherService implements IWatcherService { } }); - chokidarWatcher.on('error', (error: Error) => { + chokidarWatcher.on('error', (error: NodeJS.ErrnoException) => { if (error) { // Specially handle ENOSPC errors that can happen when @@ -255,7 +255,7 @@ export class ChokidarWatcherService implements IWatcherService { // we are running into a limit. We only want to warn // once in this case to avoid log spam. // See https://github.com/Microsoft/vscode/issues/7950 - if ((error).code === 'ENOSPC') { + if (error.code === 'ENOSPC') { if (!this.enospcErrorLogged) { this.enospcErrorLogged = true; this.stop(); @@ -281,19 +281,19 @@ export class ChokidarWatcherService implements IWatcherService { function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { for (let request of requests) { - if (request.basePath === path) { + if (request.path === path) { return false; } - if (extpath.isEqualOrParent(path, request.basePath)) { + if (extpath.isEqualOrParent(path, request.path)) { if (!request.parsedPattern) { - if (request.ignored && request.ignored.length > 0) { - let pattern = `{${request.ignored.join(',')}}`; + if (request.excludes && request.excludes.length > 0) { + let pattern = `{${request.excludes.join(',')}}`; request.parsedPattern = glob.parse(pattern); } else { request.parsedPattern = () => false; } } - const relPath = path.substr(request.basePath.length + 1); + const relPath = path.substr(request.path.length + 1); if (!request.parsedPattern(relPath)) { return false; } @@ -307,18 +307,18 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { * equests with Sub paths are skipped if they have the same ignored set as the parent. */ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string]: IWatcherRequest[] } { - requests = requests.sort((r1, r2) => r1.basePath.localeCompare(r2.basePath)); + requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path)); let prevRequest: IWatcherRequest | null = null; let result: { [basePath: string]: IWatcherRequest[] } = Object.create(null); for (let request of requests) { - let basePath = request.basePath; - let ignored = (request.ignored || []).sort(); - if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.basePath))) { - if (!isEqualIgnore(ignored, prevRequest.ignored)) { - result[prevRequest.basePath].push({ basePath, ignored }); + let basePath = request.path; + let ignored = (request.excludes || []).sort(); + if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) { + if (!isEqualIgnore(ignored, prevRequest.excludes)) { + result[prevRequest.path].push({ path: basePath, excludes: ignored }); } } else { - prevRequest = { basePath, ignored }; + prevRequest = { path: basePath, excludes: ignored }; result[basePath] = [prevRequest]; } } @@ -330,7 +330,7 @@ function isEqualRequests(r1: IWatcherRequest[], r2: IWatcherRequest[]) { return false; } for (let k = 0; k < r1.length; k++) { - if (r1[k].basePath !== r2[k].basePath || !isEqualIgnore(r1[k].ignored, r2[k].ignored)) { + if (r1[k].path !== r2[k].path || !isEqualIgnore(r1[k].excludes, r2[k].excludes)) { return false; } } diff --git a/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts b/src/vs/workbench/services/files2/node/watcher/unix/test/chockidarWatcherService.test.ts similarity index 85% rename from src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts rename to src/vs/workbench/services/files2/node/watcher/unix/test/chockidarWatcherService.test.ts index 13baa2de03..47ddb81499 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts +++ b/src/vs/workbench/services/files2/node/watcher/unix/test/chockidarWatcherService.test.ts @@ -7,17 +7,15 @@ import * as assert from 'assert'; import * as os from 'os'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; - import { normalizeRoots, ChokidarWatcherService } from '../chokidarWatcherService'; import { IWatcherRequest } from '../watcher'; - import * as platform from 'vs/base/common/platform'; import { Delayer } from 'vs/base/common/async'; -import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; import { FileChangeType } from 'vs/platform/files/common/files'; function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest { - return { basePath, ignored }; + return { path: basePath, excludes: ignored }; } function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) { @@ -32,13 +30,13 @@ function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequ const expectedPaths = Object.keys(expectedRequests).sort(); assert.deepEqual(actualPath, expectedPaths); for (let path of actualPath) { - let a = expectedRequests[path].sort((r1, r2) => r1.basePath.localeCompare(r2.basePath)); - let e = expectedRequests[path].sort((r1, r2) => r1.basePath.localeCompare(r2.basePath)); + let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); + let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); assert.deepEqual(a, e); } } -function sort(changes: IRawFileChange[]) { +function sort(changes: IDiskFileChange[]) { return changes.sort((c1, c2) => { return c1.path.localeCompare(c2.path); }); @@ -48,7 +46,7 @@ function wait(time: number) { return new Delayer(time).trigger(() => { }); } -async function assertFileEvents(actuals: IRawFileChange[], expected: IRawFileChange[]) { +async function assertFileEvents(actuals: IDiskFileChange[], expected: IDiskFileChange[]) { let repeats = 40; while ((actuals.length < expected.length) && repeats-- > 0) { await wait(50); @@ -126,7 +124,7 @@ suite.skip('Chockidar watching', () => { const b2Folder = path.join(bFolder, 'b2'); const service = new ChokidarWatcherService(); - const result: IRawFileChange[] = []; + const result: IDiskFileChange[] = []; let error: string | null = null; suiteSetup(async () => { @@ -147,7 +145,7 @@ suite.skip('Chockidar watching', () => { }); suiteTeardown(async () => { - await pfs.del(testDir); + await pfs.rimraf(testDir, pfs.RimRafMode.MOVE); await service.stop(); }); @@ -161,7 +159,7 @@ suite.skip('Chockidar watching', () => { }); test('simple file operations, single root, no ignore', async () => { - let request: IWatcherRequest = { basePath: testDir, ignored: [] }; + let request: IWatcherRequest = { path: testDir, excludes: [] }; service.setRoots([request]); await wait(300); @@ -185,19 +183,19 @@ suite.skip('Chockidar watching', () => { await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.ADDED }, { path: testFolderPath, type: FileChangeType.ADDED }]); // delete a file - await pfs.del(copiedFilePath); + await pfs.rimraf(copiedFilePath, pfs.RimRafMode.MOVE); let renamedFilePath = path.join(testFolderPath, 'file3.txt'); // move a file await pfs.rename(testFilePath, renamedFilePath); await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.DELETED }, { path: testFilePath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.ADDED }]); // delete a folder - await pfs.del(testFolderPath); + await pfs.rimraf(testFolderPath, pfs.RimRafMode.MOVE); await assertFileEvents(result, [{ path: testFolderPath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.DELETED }]); }); test('simple file operations, ignore', async () => { - let request: IWatcherRequest = { basePath: testDir, ignored: ['**/b/**', '**/*.js', '.git/**'] }; + let request: IWatcherRequest = { path: testDir, excludes: ['**/b/**', '**/*.js', '.git/**'] }; service.setRoots([request]); await wait(300); @@ -234,21 +232,21 @@ suite.skip('Chockidar watching', () => { await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.ADDED }, { path: movedFile3, type: FileChangeType.ADDED }]); // delete all files - await pfs.del(movedFile1); // hidden - await pfs.del(movedFile2); - await pfs.del(movedFile3); - await pfs.del(folder1); // hidden - await pfs.del(folder2); // hidden - await pfs.del(folder3); // hidden - await pfs.del(folder4); - await pfs.del(folder5); - await pfs.del(file4); + await pfs.rimraf(movedFile1); // hidden + await pfs.rimraf(movedFile2, pfs.RimRafMode.MOVE); + await pfs.rimraf(movedFile3, pfs.RimRafMode.MOVE); + await pfs.rimraf(folder1); // hidden + await pfs.rimraf(folder2); // hidden + await pfs.rimraf(folder3); // hidden + await pfs.rimraf(folder4, pfs.RimRafMode.MOVE); + await pfs.rimraf(folder5, pfs.RimRafMode.MOVE); + await pfs.rimraf(file4, pfs.RimRafMode.MOVE); await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.DELETED }, { path: movedFile3, type: FileChangeType.DELETED }, { path: file4, type: FileChangeType.DELETED }, { path: folder4, type: FileChangeType.DELETED }, { path: folder5, type: FileChangeType.DELETED }]); }); test('simple file operations, multiple roots', async () => { - let request1: IWatcherRequest = { basePath: aFolder, ignored: ['**/*.js'] }; - let request2: IWatcherRequest = { basePath: b2Folder, ignored: ['**/*.ts'] }; + let request1: IWatcherRequest = { path: aFolder, excludes: ['**/*.js'] }; + let request2: IWatcherRequest = { path: b2Folder, excludes: ['**/*.ts'] }; service.setRoots([request1, request2]); await wait(300); @@ -271,23 +269,23 @@ suite.skip('Chockidar watching', () => { await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.ADDED }, { path: filePath1, type: FileChangeType.ADDED }, { path: folderPath2, type: FileChangeType.ADDED }]); // change roots - let request3: IWatcherRequest = { basePath: aFolder, ignored: ['**/*.json'] }; + let request3: IWatcherRequest = { path: aFolder, excludes: ['**/*.json'] }; service.setRoots([request3]); await wait(300); assert.equal(service.wacherCount, 1); // delete all - await pfs.del(folderPath1); - await pfs.del(folderPath2); - await pfs.del(filePath4); + await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE); + await pfs.rimraf(folderPath2, pfs.RimRafMode.MOVE); + await pfs.rimraf(filePath4, pfs.RimRafMode.MOVE); await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.DELETED }, { path: filePath2, type: FileChangeType.DELETED }]); }); test('simple file operations, nested roots', async () => { - let request1: IWatcherRequest = { basePath: testDir, ignored: ['**/b2/**'] }; - let request2: IWatcherRequest = { basePath: bFolder, ignored: ['**/b3/**'] }; + let request1: IWatcherRequest = { path: testDir, excludes: ['**/b2/**'] }; + let request2: IWatcherRequest = { path: bFolder, excludes: ['**/b3/**'] }; service.setRoots([request1, request2]); await wait(300); @@ -311,8 +309,8 @@ suite.skip('Chockidar watching', () => { await assertFileEvents(result, [{ path: filePath2, type: FileChangeType.DELETED }]); // delete all - await pfs.del(folderPath1); - await pfs.del(filePath1); + await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE); + await pfs.rimraf(filePath1, pfs.RimRafMode.MOVE); await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.DELETED }]); }); diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts b/src/vs/workbench/services/files2/node/watcher/unix/watcher.ts similarity index 78% rename from src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts rename to src/vs/workbench/services/files2/node/watcher/unix/watcher.ts index 1816bf5c84..338de0c7ba 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts +++ b/src/vs/workbench/services/files2/node/watcher/unix/watcher.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; export interface IWatcherRequest { - basePath: string; - ignored: string[]; + path: string; + excludes: string[]; } export interface IWatcherOptions { @@ -20,7 +20,7 @@ export interface IWatchError { } export interface IWatcherService { - watch(options: IWatcherOptions): Event; + watch(options: IWatcherOptions): Event; setRoots(roots: IWatcherRequest[]): Promise; setVerboseLogging(enabled: boolean): Promise; stop(): Promise; diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherApp.ts b/src/vs/workbench/services/files2/node/watcher/unix/watcherApp.ts similarity index 76% rename from src/vs/workbench/services/files/node/watcher/unix/watcherApp.ts rename to src/vs/workbench/services/files2/node/watcher/unix/watcherApp.ts index 8855e48509..f456d37c2f 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherApp.ts +++ b/src/vs/workbench/services/files2/node/watcher/unix/watcherApp.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; -import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc'; -import { ChokidarWatcherService } from 'vs/workbench/services/files/node/watcher/unix/chokidarWatcherService'; +import { WatcherChannel } from 'vs/workbench/services/files2/node/watcher/unix/watcherIpc'; +import { ChokidarWatcherService } from 'vs/workbench/services/files2/node/watcher/unix/chokidarWatcherService'; const server = new Server('watcher'); const service = new ChokidarWatcherService(); diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts b/src/vs/workbench/services/files2/node/watcher/unix/watcherIpc.ts similarity index 90% rename from src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts rename to src/vs/workbench/services/files2/node/watcher/unix/watcherIpc.ts index d88f64255c..588acd82d9 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherIpc.ts +++ b/src/vs/workbench/services/files2/node/watcher/unix/watcherIpc.ts @@ -6,7 +6,7 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher'; import { Event } from 'vs/base/common/event'; -import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; export class WatcherChannel implements IServerChannel { @@ -35,7 +35,7 @@ export class WatcherChannelClient implements IWatcherService { constructor(private channel: IChannel) { } - watch(options: IWatcherOptions): Event { + watch(options: IWatcherOptions): Event { return this.channel.listen('watch', options); } diff --git a/src/vs/workbench/services/files2/node/watcher/unix/watcherService.ts b/src/vs/workbench/services/files2/node/watcher/unix/watcherService.ts new file mode 100644 index 0000000000..5056e59cea --- /dev/null +++ b/src/vs/workbench/services/files2/node/watcher/unix/watcherService.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; +import { WatcherChannelClient } from 'vs/workbench/services/files2/node/watcher/unix/watcherIpc'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { IWatchError, IWatcherRequest } from 'vs/workbench/services/files2/node/watcher/unix/watcher'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; + +export class FileWatcher extends Disposable { + private static readonly MAX_RESTARTS = 5; + + private isDisposed: boolean; + private restartCounter: number; + private service: WatcherChannelClient; + + constructor( + private folders: IWatcherRequest[], + private onFileChanges: (changes: IDiskFileChange[]) => void, + private errorLogger: (msg: string) => void, + private verboseLogging: boolean + ) { + super(); + + this.isDisposed = false; + this.restartCounter = 0; + + this.startWatching(); + } + + private startWatching(): void { + const client = this._register(new Client( + getPathFromAmdModule(require, 'bootstrap-fork'), + { + serverName: 'File Watcher (chokidar)', + args: ['--type=watcherService'], + env: { + AMD_ENTRYPOINT: 'vs/workbench/services/files2/node/watcher/unix/watcherApp', + PIPE_LOGGING: 'true', + VERBOSE_LOGGING: this.verboseLogging + } + } + )); + + this._register(client.onDidProcessExit(() => { + // our watcher app should never be completed because it keeps on watching. being in here indicates + // that the watcher process died and we want to restart it here. we only do it a max number of times + if (!this.isDisposed) { + if (this.restartCounter <= FileWatcher.MAX_RESTARTS) { + this.errorLogger('[File Watcher (chokidar)] terminated unexpectedly and is restarted again...'); + this.restartCounter++; + this.startWatching(); + } else { + this.errorLogger('[File Watcher (chokidar)] failed to start after retrying for some time, giving up. Please report this as a bug report!'); + } + } + })); + + // Initialize watcher + const channel = getNextTickChannel(client.getChannel('watcher')); + this.service = new WatcherChannelClient(channel); + + const options = { verboseLogging: this.verboseLogging }; + const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed); + + const onError = Event.filter(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string'); + this._register(onError(err => this.errorLogger(`[File Watcher (chokidar)] ${err.message}`))); + + const onFileChanges = Event.filter(onWatchEvent, (e): e is IDiskFileChange[] => Array.isArray(e) && e.length > 0); + this._register(onFileChanges(e => this.onFileChanges(e))); + + // Start watching + this.service.setRoots(this.folders); + } + + setFolders(folders: IWatcherRequest[]): void { + this.folders = folders; + + this.service.setRoots(folders); + } + + dispose(): void { + this.isDisposed = true; + + super.dispose(); + } +} diff --git a/src/vs/workbench/services/files/node/watcher/common.ts b/src/vs/workbench/services/files2/node/watcher/watcher.ts similarity index 65% rename from src/vs/workbench/services/files/node/watcher/common.ts rename to src/vs/workbench/services/files2/node/watcher/watcher.ts index def6319870..43cfca38d8 100644 --- a/src/vs/workbench/services/files/node/watcher/common.ts +++ b/src/vs/workbench/services/files2/node/watcher/watcher.ts @@ -4,32 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import { URI as uri } from 'vs/base/common/uri'; -import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files'; +import { FileChangeType, isParent, IFileChange } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; -export interface IRawFileChange { +export interface IDiskFileChange { type: FileChangeType; path: string; } -export function toFileChangesEvent(changes: IRawFileChange[]): FileChangesEvent { - - // map to file changes event that talks about URIs - return new FileChangesEvent(changes.map((c) => { - return { - type: c.type, - resource: uri.file(c.path) - }; +export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] { + return changes.map(change => ({ + type: change.type, + resource: uri.file(change.path) })); } -/** - * Given events that occurred, applies some rules to normalize the events - */ -export function normalize(changes: IRawFileChange[]): IRawFileChange[] { +export function normalizeFileChanges(changes: IDiskFileChange[]): IDiskFileChange[] { // Build deltas - let normalizer = new EventNormalizer(); + const normalizer = new EventNormalizer(); for (const event of changes) { normalizer.processEvent(event); } @@ -38,25 +31,20 @@ export function normalize(changes: IRawFileChange[]): IRawFileChange[] { } class EventNormalizer { - private normalized: IRawFileChange[]; - private mapPathToChange: { [path: string]: IRawFileChange }; + private normalized: IDiskFileChange[] = []; + private mapPathToChange: Map = new Map(); - constructor() { - this.normalized = []; - this.mapPathToChange = Object.create(null); - } - - public processEvent(event: IRawFileChange): void { + processEvent(event: IDiskFileChange): void { + const existingEvent = this.mapPathToChange.get(event.path); // Event path already exists - let existingEvent = this.mapPathToChange[event.path]; if (existingEvent) { - let currentChangeType = existingEvent.type; - let newChangeType = event.type; + const currentChangeType = existingEvent.type; + const newChangeType = event.type; // ignore CREATE followed by DELETE in one go if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.DELETED) { - delete this.mapPathToChange[event.path]; + this.mapPathToChange.delete(event.path); this.normalized.splice(this.normalized.indexOf(existingEvent), 1); } @@ -66,8 +54,7 @@ class EventNormalizer { } // Do nothing. Keep the created event - else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) { - } + else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) { } // Otherwise apply change type else { @@ -75,16 +62,16 @@ class EventNormalizer { } } - // Otherwise Store + // Otherwise store new else { this.normalized.push(event); - this.mapPathToChange[event.path] = event; + this.mapPathToChange.set(event.path, event); } } - public normalize(): IRawFileChange[] { - let addedChangeEvents: IRawFileChange[] = []; - let deletedPaths: string[] = []; + normalize(): IDiskFileChange[] { + const addedChangeEvents: IDiskFileChange[] = []; + const deletedPaths: string[] = []; // This algorithm will remove all DELETE events up to the root folder // that got deleted if any. This ensures that we are not producing @@ -96,6 +83,7 @@ class EventNormalizer { return this.normalized.filter(e => { if (e.type !== FileChangeType.DELETED) { addedChangeEvents.push(e); + return false; // remove ADD / CHANGE } diff --git a/src/vs/workbench/services/files/node/watcher/win32/CodeHelper.exe b/src/vs/workbench/services/files2/node/watcher/win32/CodeHelper.exe similarity index 100% rename from src/vs/workbench/services/files/node/watcher/win32/CodeHelper.exe rename to src/vs/workbench/services/files2/node/watcher/win32/CodeHelper.exe diff --git a/src/vs/workbench/services/files/node/watcher/win32/CodeHelper.md b/src/vs/workbench/services/files2/node/watcher/win32/CodeHelper.md similarity index 100% rename from src/vs/workbench/services/files/node/watcher/win32/CodeHelper.md rename to src/vs/workbench/services/files2/node/watcher/win32/CodeHelper.md diff --git a/src/vs/workbench/services/files/node/watcher/win32/csharpWatcherService.ts b/src/vs/workbench/services/files2/node/watcher/win32/csharpWatcherService.ts similarity index 90% rename from src/vs/workbench/services/files/node/watcher/win32/csharpWatcherService.ts rename to src/vs/workbench/services/files2/node/watcher/win32/csharpWatcherService.ts index 5d40b0f8c2..b0dbacd39c 100644 --- a/src/vs/workbench/services/files/node/watcher/win32/csharpWatcherService.ts +++ b/src/vs/workbench/services/files2/node/watcher/win32/csharpWatcherService.ts @@ -4,12 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as cp from 'child_process'; - import { FileChangeType } from 'vs/platform/files/common/files'; import * as decoder from 'vs/base/node/decoder'; import * as glob from 'vs/base/common/glob'; - -import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; import { getPathFromAmdModule } from 'vs/base/common/amd'; export class OutOfProcessWin32FolderWatcher { @@ -26,7 +24,7 @@ export class OutOfProcessWin32FolderWatcher { constructor( private watchedFolder: string, ignored: string[], - private eventCallback: (events: IRawFileChange[]) => void, + private eventCallback: (events: IDiskFileChange[]) => void, private errorCallback: (error: string) => void, private verboseLogging: boolean ) { @@ -38,6 +36,11 @@ export class OutOfProcessWin32FolderWatcher { this.ignored = []; } + // Logging + if (this.verboseLogging) { + console.log('%c[File Watcher (C#)]', 'color: blue', `Start watching: ${watchedFolder}`); + } + this.startWatcher(); } @@ -47,7 +50,7 @@ export class OutOfProcessWin32FolderWatcher { args.push('-verbose'); } - this.handle = cp.spawn(getPathFromAmdModule(require, 'vs/workbench/services/files/node/watcher/win32/CodeHelper.exe'), args); + this.handle = cp.spawn(getPathFromAmdModule(require, 'vs/workbench/services/files2/node/watcher/win32/CodeHelper.exe'), args); const stdoutLineDecoder = new decoder.LineDecoder(); @@ -55,7 +58,7 @@ export class OutOfProcessWin32FolderWatcher { this.handle.stdout.on('data', (data: Buffer) => { // Collect raw events from output - const rawEvents: IRawFileChange[] = []; + const rawEvents: IDiskFileChange[] = []; stdoutLineDecoder.write(data).forEach((line) => { const eventParts = line.split('|'); if (eventParts.length === 2) { diff --git a/src/vs/workbench/services/files2/node/watcher/win32/watcherService.ts b/src/vs/workbench/services/files2/node/watcher/win32/watcherService.ts new file mode 100644 index 0000000000..56e6c6d4c8 --- /dev/null +++ b/src/vs/workbench/services/files2/node/watcher/win32/watcherService.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher'; +import { OutOfProcessWin32FolderWatcher } from 'vs/workbench/services/files2/node/watcher/win32/csharpWatcherService'; +import { posix } from 'vs/base/common/path'; +import { rtrim, endsWith } from 'vs/base/common/strings'; +import { Disposable } from 'vs/base/common/lifecycle'; + +export class FileWatcher extends Disposable { + private isDisposed: boolean; + private folder: { path: string, excludes: string[] }; + + constructor( + folders: { path: string, excludes: string[] }[], + private onFileChanges: (changes: IDiskFileChange[]) => void, + private errorLogger: (msg: string) => void, + private verboseLogging: boolean + ) { + super(); + + this.folder = folders[0]; + + if (this.folder.path.indexOf('\\\\') === 0 && endsWith(this.folder.path, posix.sep)) { + // for some weird reason, node adds a trailing slash to UNC paths + // we never ever want trailing slashes as our base path unless + // someone opens root ("/"). + // See also https://github.com/nodejs/io.js/issues/1765 + this.folder.path = rtrim(this.folder.path, posix.sep); + } + + this.startWatching(); + } + + private startWatching(): void { + this._register(new OutOfProcessWin32FolderWatcher( + this.folder.path, + this.folder.excludes, + events => this.onFileEvents(events), + error => this.onError(error), + this.verboseLogging + )); + } + + private onFileEvents(events: IDiskFileChange[]): void { + if (this.isDisposed) { + return; + } + + // Emit through event emitter + if (events.length > 0) { + this.onFileChanges(events); + } + } + + private onError(error: string): void { + if (!this.isDisposed) { + this.errorLogger(error); + } + } + + dispose(): void { + this.isDisposed = true; + + super.dispose(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/files2/test/browser/fileService2.test.ts b/src/vs/workbench/services/files2/test/browser/fileService2.test.ts index 71d8bf0253..5bf9128104 100644 --- a/src/vs/workbench/services/files2/test/browser/fileService2.test.ts +++ b/src/vs/workbench/services/files2/test/browser/fileService2.test.ts @@ -7,9 +7,10 @@ import * as assert from 'assert'; import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; import { URI } from 'vs/base/common/uri'; import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { NullFileSystemProvider } from 'vs/workbench/test/workbenchTestServices'; import { NullLogService } from 'vs/platform/log/common/log'; +import { timeout } from 'vs/base/common/async'; suite('File Service 2', () => { @@ -50,8 +51,8 @@ suite('File Service 2', () => { await service.activateProvider('test'); assert.equal(callCount, 2); // activation is called again - assert.equal(await service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true); - assert.equal(await service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false); + assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true); + assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false); registrationDisposable!.dispose(); @@ -61,4 +62,51 @@ suite('File Service 2', () => { assert.equal(registrations[1].scheme, 'test'); assert.equal(registrations[1].added, false); }); + + test('watch', async () => { + const service = new FileService2(new NullLogService()); + + let disposeCounter = 0; + service.registerProvider('test', new NullFileSystemProvider(() => { + return toDisposable(() => { + disposeCounter++; + }); + })); + await service.activateProvider('test'); + + const resource1 = URI.parse('test://foo/bar1'); + const watcher1Disposable = service.watch(resource1); + + await timeout(0); // service.watch() is async + assert.equal(disposeCounter, 0); + watcher1Disposable.dispose(); + assert.equal(disposeCounter, 1); + + disposeCounter = 0; + const resource2 = URI.parse('test://foo/bar2'); + const watcher2Disposable1 = service.watch(resource2); + const watcher2Disposable2 = service.watch(resource2); + const watcher2Disposable3 = service.watch(resource2); + + await timeout(0); // service.watch() is async + assert.equal(disposeCounter, 0); + watcher2Disposable1.dispose(); + assert.equal(disposeCounter, 0); + watcher2Disposable2.dispose(); + assert.equal(disposeCounter, 0); + watcher2Disposable3.dispose(); + assert.equal(disposeCounter, 1); + + disposeCounter = 0; + const resource3 = URI.parse('test://foo/bar3'); + const watcher3Disposable1 = service.watch(resource3); + const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] }); + + await timeout(0); // service.watch() is async + assert.equal(disposeCounter, 0); + watcher3Disposable1.dispose(); + assert.equal(disposeCounter, 1); + watcher3Disposable2.dispose(); + assert.equal(disposeCounter, 2); + }); }); 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 51718c1283..cf5370b81b 100644 --- a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts @@ -12,15 +12,14 @@ import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { generateUuid } from 'vs/base/common/uuid'; import { join, basename, dirname, posix } from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { copy, del, symlink } from 'vs/base/node/pfs'; +import { copy, rimraf, symlink, RimRafMode, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { existsSync, statSync, readdirSync, readFileSync } from 'fs'; -import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from 'fs'; +import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange } 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 { promisify } from 'util'; -import { exec } from 'child_process'; +import { isEqual } from 'vs/base/common/resources'; function getByName(root: IFileStat, name: string): IFileStat | null { if (root.children === undefined) { @@ -78,10 +77,12 @@ suite('Disk File Service', () => { disposables.push(service); fileProvider = new TestDiskFileSystemProvider(logService); - service.registerProvider(Schemas.file, fileProvider); + disposables.push(service.registerProvider(Schemas.file, fileProvider)); + disposables.push(fileProvider); testProvider = new TestDiskFileSystemProvider(logService); - service.registerProvider(testSchema, testProvider); + disposables.push(service.registerProvider(testSchema, testProvider)); + disposables.push(testProvider); const id = generateUuid(); testDir = join(parentDir, id); @@ -93,7 +94,7 @@ suite('Disk File Service', () => { teardown(async () => { disposables = dispose(disposables); - await del(parentDir, tmpdir()); + await rimraf(parentDir, RimRafMode.MOVE); }); test('createFolder', async () => { @@ -775,4 +776,230 @@ suite('Disk File Service', () => { assert.ok(error); } }); -}); + + test('watch - file', done => { + const toWatch = URI.file(join(testDir, 'index-watch1.html')); + writeFileSync(toWatch.fsPath, 'Init'); + + assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); + + setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); + }); + + test('watch - file symbolic link', async done => { + if (isWindows) { + return done(); // not happy + } + + const toWatch = URI.file(join(testDir, 'lorem.txt-linked')); + await symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); + + assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); + + setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); + }); + + test('watch - file - multiple writes', done => { + const toWatch = URI.file(join(testDir, 'index-watch1.html')); + writeFileSync(toWatch.fsPath, 'Init'); + + assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); + + setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 1'), 0); + setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 2'), 10); + setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 3'), 20); + }); + + test('watch - file - delete file', done => { + const toWatch = URI.file(join(testDir, 'index-watch1.html')); + writeFileSync(toWatch.fsPath, 'Init'); + + assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); + + setTimeout(() => unlinkSync(toWatch.fsPath), 50); + }); + + test('watch - file - rename file', done => { + const toWatch = URI.file(join(testDir, 'index-watch1.html')); + const toWatchRenamed = URI.file(join(testDir, 'index-watch1-renamed.html')); + writeFileSync(toWatch.fsPath, 'Init'); + + assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); + + setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50); + }); + + test('watch - file - rename file (different case)', done => { + const toWatch = URI.file(join(testDir, 'index-watch1.html')); + const toWatchRenamed = URI.file(join(testDir, 'INDEX-watch1.html')); + writeFileSync(toWatch.fsPath, 'Init'); + + if (isLinux) { + assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); + } else { + assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); // case insensitive file system treat this as change + } + + setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50); + }); + + test('watch - file (atomic save)', function (done) { + const toWatch = URI.file(join(testDir, 'index-watch2.html')); + writeFileSync(toWatch.fsPath, 'Init'); + + assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); + + setTimeout(() => { + // Simulate atomic save by deleting the file, creating it under different name + // and then replacing the previously deleted file with those contents + const renamed = `${toWatch.fsPath}.bak`; + unlinkSync(toWatch.fsPath); + writeFileSync(renamed, 'Changes'); + renameSync(renamed, toWatch.fsPath); + }, 50); + }); + + test('watch - folder (non recursive) - change file', done => { + const watchDir = URI.file(join(testDir, 'watch3')); + mkdirSync(watchDir.fsPath); + + const file = URI.file(join(watchDir.fsPath, 'index.html')); + writeFileSync(file.fsPath, 'Init'); + + assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done); + + setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + }); + + test('watch - folder (non recursive) - add file', done => { + const watchDir = URI.file(join(testDir, 'watch4')); + mkdirSync(watchDir.fsPath); + + const file = URI.file(join(watchDir.fsPath, 'index.html')); + + assertWatch(watchDir, [[FileChangeType.ADDED, file]], done); + + setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + }); + + test('watch - folder (non recursive) - delete file', done => { + const watchDir = URI.file(join(testDir, 'watch5')); + mkdirSync(watchDir.fsPath); + + const file = URI.file(join(watchDir.fsPath, 'index.html')); + writeFileSync(file.fsPath, 'Init'); + + assertWatch(watchDir, [[FileChangeType.DELETED, file]], done); + + setTimeout(() => unlinkSync(file.fsPath), 50); + }); + + test('watch - folder (non recursive) - add folder', done => { + const watchDir = URI.file(join(testDir, 'watch6')); + mkdirSync(watchDir.fsPath); + + const folder = URI.file(join(watchDir.fsPath, 'folder')); + + assertWatch(watchDir, [[FileChangeType.ADDED, folder]], done); + + setTimeout(() => mkdirSync(folder.fsPath), 50); + }); + + test('watch - folder (non recursive) - delete folder', done => { + const watchDir = URI.file(join(testDir, 'watch7')); + mkdirSync(watchDir.fsPath); + + const folder = URI.file(join(watchDir.fsPath, 'folder')); + mkdirSync(folder.fsPath); + + assertWatch(watchDir, [[FileChangeType.DELETED, folder]], done); + + setTimeout(() => rimrafSync(folder.fsPath), 50); + }); + + test('watch - folder (non recursive) - symbolic link - change file', async done => { + if (isWindows) { + return done(); // not happy + } + + const watchDir = URI.file(join(testDir, 'deep-link')); + await symlink(join(testDir, 'deep'), watchDir.fsPath); + + const file = URI.file(join(watchDir.fsPath, 'index.html')); + writeFileSync(file.fsPath, 'Init'); + + assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done); + + setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + }); + + test('watch - folder (non recursive) - rename file', done => { + const watchDir = URI.file(join(testDir, 'watch8')); + mkdirSync(watchDir.fsPath); + + const file = URI.file(join(watchDir.fsPath, 'index.html')); + writeFileSync(file.fsPath, 'Init'); + + const fileRenamed = URI.file(join(watchDir.fsPath, 'index-renamed.html')); + + assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done); + + setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50); + }); + + test('watch - folder (non recursive) - rename file (different case)', done => { + if (!isLinux) { + return done(); // not happy + } + + const watchDir = URI.file(join(testDir, 'watch8')); + mkdirSync(watchDir.fsPath); + + const file = URI.file(join(watchDir.fsPath, 'index.html')); + writeFileSync(file.fsPath, 'Init'); + + const fileRenamed = URI.file(join(watchDir.fsPath, 'INDEX.html')); + + assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done); + + setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50); + }); + + function assertWatch(toWatch: URI, expected: [FileChangeType, URI][], done: MochaDone): void { + const watcherDisposable = service.watch(toWatch); + + function toString(type: FileChangeType): string { + switch (type) { + case FileChangeType.ADDED: return 'added'; + case FileChangeType.DELETED: return 'deleted'; + case FileChangeType.UPDATED: return 'updated'; + } + } + + const listenerDisposable = service.onFileChanges(event => { + watcherDisposable.dispose(); + listenerDisposable.dispose(); + + try { + assert.equal(event.changes.length, expected.length); + + if (expected.length === 1) { + assert.equal(event.changes[0].type, expected[0][0], `Expected ${toString(expected[0][0])} but got ${toString(event.changes[0].type)}`); + assert.equal(event.changes[0].resource.fsPath, expected[0][1].fsPath); + } else { + for (const expect of expected) { + assert.equal(hasChange(event.changes, expect[0], expect[1]), true, `Unable to find ${toString(expect[0])} for ${expect[1].fsPath}`); + } + } + + done(); + } catch (error) { + done(error); + } + }); + } + + function hasChange(changes: IFileChange[], type: FileChangeType, resource: URI): boolean { + return changes.some(change => change.type === type && isEqual(change.resource, resource)); + } +}); \ No newline at end of file diff --git a/src/vs/workbench/services/files/test/electron-browser/watcher.test.ts b/src/vs/workbench/services/files2/test/node/normalizer.test.ts similarity index 85% rename from src/vs/workbench/services/files/test/electron-browser/watcher.test.ts rename to src/vs/workbench/services/files2/test/node/normalizer.test.ts index 5b1a72dcc6..478673327a 100644 --- a/src/vs/workbench/services/files/test/electron-browser/watcher.test.ts +++ b/src/vs/workbench/services/files2/test/node/normalizer.test.ts @@ -4,13 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; - import * as platform from 'vs/base/common/platform'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { URI as uri } from 'vs/base/common/uri'; -import { IRawFileChange, toFileChangesEvent, normalize } from 'vs/workbench/services/files/node/watcher/common'; +import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher'; import { Event, Emitter } from 'vs/base/common/event'; +function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent { + return new FileChangesEvent(toFileChanges(changes)); +} + class TestFileWatcher { private readonly _onFileChanges: Emitter; @@ -18,18 +21,18 @@ class TestFileWatcher { this._onFileChanges = new Emitter(); } - public get onFileChanges(): Event { + get onFileChanges(): Event { return this._onFileChanges.event; } - public report(changes: IRawFileChange[]): void { + report(changes: IDiskFileChange[]): void { this.onRawFileEvents(changes); } - private onRawFileEvents(events: IRawFileChange[]): void { + private onRawFileEvents(events: IDiskFileChange[]): void { // Normalize - let normalizedEvents = normalize(events); + let normalizedEvents = normalizeFileChanges(events); // Emit through event emitter if (normalizedEvents.length > 0) { @@ -44,16 +47,16 @@ enum Path { UNC } -suite('Watcher', () => { +suite('Normalizer', () => { - test('watching - simple add/update/delete', function (done: () => void) { + test('simple add/update/delete', function (done: () => void) { const watch = new TestFileWatcher(); const added = uri.file('/users/data/src/added.txt'); const updated = uri.file('/users/data/src/updated.txt'); const deleted = uri.file('/users/data/src/deleted.txt'); - const raw: IRawFileChange[] = [ + const raw: IDiskFileChange[] = [ { path: added.fsPath, type: FileChangeType.ADDED }, { path: updated.fsPath, type: FileChangeType.UPDATED }, { path: deleted.fsPath, type: FileChangeType.DELETED }, @@ -74,7 +77,7 @@ suite('Watcher', () => { let pathSpecs = platform.isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX]; pathSpecs.forEach((p) => { - test('watching - delete only reported for top level folder (' + p + ')', function (done: () => void) { + test('delete only reported for top level folder (' + p + ')', function (done: () => void) { const watch = new TestFileWatcher(); const deletedFolderA = uri.file(p === Path.UNIX ? '/users/data/src/todelete1' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1'); @@ -87,7 +90,7 @@ suite('Watcher', () => { const addedFile = uri.file(p === Path.UNIX ? '/users/data/src/added.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt'); const updatedFile = uri.file(p === Path.UNIX ? '/users/data/src/updated.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt'); - const raw: IRawFileChange[] = [ + const raw: IDiskFileChange[] = [ { path: deletedFolderA.fsPath, type: FileChangeType.DELETED }, { path: deletedFolderB.fsPath, type: FileChangeType.DELETED }, { path: deletedFolderBF1.fsPath, type: FileChangeType.DELETED }, @@ -115,14 +118,14 @@ suite('Watcher', () => { }); }); - test('watching - event normalization: ignore CREATE followed by DELETE', function (done: () => void) { + test('event normalization: ignore CREATE followed by DELETE', function (done: () => void) { const watch = new TestFileWatcher(); const created = uri.file('/users/data/src/related'); const deleted = uri.file('/users/data/src/related'); const unrelated = uri.file('/users/data/src/unrelated'); - const raw: IRawFileChange[] = [ + const raw: IDiskFileChange[] = [ { path: created.fsPath, type: FileChangeType.ADDED }, { path: deleted.fsPath, type: FileChangeType.DELETED }, { path: unrelated.fsPath, type: FileChangeType.UPDATED }, @@ -140,14 +143,14 @@ suite('Watcher', () => { watch.report(raw); }); - test('watching - event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) { + test('event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) { const watch = new TestFileWatcher(); const deleted = uri.file('/users/data/src/related'); const created = uri.file('/users/data/src/related'); const unrelated = uri.file('/users/data/src/unrelated'); - const raw: IRawFileChange[] = [ + const raw: IDiskFileChange[] = [ { path: deleted.fsPath, type: FileChangeType.DELETED }, { path: created.fsPath, type: FileChangeType.ADDED }, { path: unrelated.fsPath, type: FileChangeType.UPDATED }, @@ -166,14 +169,14 @@ suite('Watcher', () => { watch.report(raw); }); - test('watching - event normalization: ignore UPDATE when CREATE received', function (done: () => void) { + test('event normalization: ignore UPDATE when CREATE received', function (done: () => void) { const watch = new TestFileWatcher(); const created = uri.file('/users/data/src/related'); const updated = uri.file('/users/data/src/related'); const unrelated = uri.file('/users/data/src/unrelated'); - const raw: IRawFileChange[] = [ + const raw: IDiskFileChange[] = [ { path: created.fsPath, type: FileChangeType.ADDED }, { path: updated.fsPath, type: FileChangeType.UPDATED }, { path: unrelated.fsPath, type: FileChangeType.UPDATED }, @@ -193,7 +196,7 @@ suite('Watcher', () => { watch.report(raw); }); - test('watching - event normalization: apply DELETE', function (done: () => void) { + test('event normalization: apply DELETE', function (done: () => void) { const watch = new TestFileWatcher(); const updated = uri.file('/users/data/src/related'); @@ -201,7 +204,7 @@ suite('Watcher', () => { const deleted = uri.file('/users/data/src/related'); const unrelated = uri.file('/users/data/src/unrelated'); - const raw: IRawFileChange[] = [ + const raw: IDiskFileChange[] = [ { path: updated.fsPath, type: FileChangeType.UPDATED }, { path: updated2.fsPath, type: FileChangeType.UPDATED }, { path: unrelated.fsPath, type: FileChangeType.UPDATED }, diff --git a/src/vs/workbench/services/hash/common/hashService.ts b/src/vs/workbench/services/hash/common/hashService.ts deleted file mode 100644 index 90df38dcf0..0000000000 --- a/src/vs/workbench/services/hash/common/hashService.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -export const IHashService = createDecorator('hashService'); - -export interface IHashService { - _serviceBrand: any; - - /** - * Produce a SHA1 hash of the provided content. - */ - createSHA1(content: string): Thenable; -} - -export class HashService implements IHashService { - - _serviceBrand: any; - - createSHA1(content: string): Thenable { - return crypto.subtle.digest('SHA-1', new TextEncoder().encode(content)).then(buffer => { - // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#Converting_a_digest_to_a_hex_string - return Array.prototype.map.call(new Uint8Array(buffer), (value: number) => `00${value.toString(16)}`.slice(-2)).join(''); - }); - } -} - -registerSingleton(IHashService, HashService, true); \ No newline at end of file diff --git a/src/vs/workbench/services/hash/node/hashService.ts b/src/vs/workbench/services/hash/node/hashService.ts deleted file mode 100644 index b4f65bab00..0000000000 --- a/src/vs/workbench/services/hash/node/hashService.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createHash } from 'crypto'; -import { IHashService } from 'vs/workbench/services/hash/common/hashService'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -export class HashService implements IHashService { - - _serviceBrand: any; - - createSHA1(content: string): Promise { - return Promise.resolve(createHash('sha1').update(content).digest('hex')); - } -} - -registerSingleton(IHashService, HashService, true); \ No newline at end of file diff --git a/src/vs/workbench/services/hash/test/hashService.test.ts b/src/vs/workbench/services/hash/test/hashService.test.ts deleted file mode 100644 index ae522e8d07..0000000000 --- a/src/vs/workbench/services/hash/test/hashService.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { HashService } from 'vs/workbench/services/hash/common/hashService'; - -suite('Hash Service', () => { - - test('computeSHA1Hash', async () => { - const service = new HashService(); - - assert.equal(await service.createSHA1(''), 'da39a3ee5e6b4b0d3255bfef95601890afd80709'); - assert.equal(await service.createSHA1('hello world'), '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'); - assert.equal(await service.createSHA1('da39a3ee5e6b4b0d3255bfef95601890afd80709'), '10a34637ad661d98ba3344717656fcc76209c2f8'); - assert.equal(await service.createSHA1('2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'), 'd6b0d82cea4269b51572b8fab43adcee9fc3cf9a'); - assert.equal(await service.createSHA1('öäü_?ß()<>ÖÄÜ'), 'b64beaeff9e317b0193c8e40a2431b210388eba9'); - }); -}); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 24b77dadf6..b93d09c2a1 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -24,7 +24,7 @@ import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { getExcludes, ISearchConfiguration } from 'vs/workbench/services/search/common/search'; import { IExpression } from 'vs/base/common/glob'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ResourceGlobMatcher } from 'vs/workbench/common/resources'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -98,7 +98,7 @@ interface IRecentlyClosedFile { export class HistoryService extends Disposable implements IHistoryService { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; private static readonly STORAGE_KEY = 'history.entries'; private static readonly MAX_HISTORY_ITEMS = 200; @@ -838,7 +838,7 @@ export class HistoryService extends Disposable implements IHistoryService { const registry = Registry.as(EditorExtensions.EditorInputFactories); - const entries: ISerializedEditorHistoryEntry[] = coalesce(this.history.map(input => { + const entries: ISerializedEditorHistoryEntry[] = coalesce(this.history.map((input): ISerializedEditorHistoryEntry | undefined => { // Editor input: try via factory if (input instanceof EditorInput) { @@ -846,14 +846,14 @@ export class HistoryService extends Disposable implements IHistoryService { if (factory) { const deserialized = factory.serialize(input); if (deserialized) { - return { editorInputJSON: { typeId: input.getTypeId(), deserialized } } as ISerializedEditorHistoryEntry; + return { editorInputJSON: { typeId: input.getTypeId(), deserialized } }; } } } // File resource: via URI.toJSON() else { - return { resourceJSON: (input as IResourceInput).resource.toJSON() } as ISerializedEditorHistoryEntry; + return { resourceJSON: (input as IResourceInput).resource.toJSON() }; } return undefined; @@ -884,11 +884,11 @@ export class HistoryService extends Disposable implements IHistoryService { } private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceInput | undefined { - const serializedEditorHistoryEntry = entry as ISerializedEditorHistoryEntry; + const serializedEditorHistoryEntry = entry; // File resource: via URI.revive() if (serializedEditorHistoryEntry.resourceJSON) { - return { resource: URI.revive(serializedEditorHistoryEntry.resourceJSON) } as IResourceInput; + return { resource: URI.revive(serializedEditorHistoryEntry.resourceJSON) }; } // Editor input: via factory diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index fe748292b3..e59ad24417 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -11,8 +11,7 @@ import * as json from 'vs/base/common/json'; import { ChordKeybinding, KeyCode, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { OS } from 'vs/base/common/platform'; import * as uuid from 'vs/base/common/uuid'; -import * as extfs from 'vs/base/node/extfs'; -import { mkdirp } from 'vs/base/node/pfs'; +import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -21,7 +20,6 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; @@ -31,20 +29,22 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { ILogService } from 'vs/platform/log/common/log'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService, Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { LegacyFileService } from 'vs/workbench/services/files/node/fileService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestLogService, TestStorageService, TestTextFileService, TestTextResourceConfigurationService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices'; +import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestLogService, TestTextFileService, TestTextResourceConfigurationService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices'; +import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; +import { Schemas } from 'vs/base/common/network'; +import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider'; interface Modifiers { metaKey?: boolean; @@ -82,16 +82,15 @@ suite('KeybindingsEditing', () => { instantiationService.stub(ILogService, new TestLogService()); instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(instantiationService.get(IConfigurationService))); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); - instantiationService.stub(IFileService, new FileService( + const fileService = new FileService2(new NullLogService()); + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); + fileService.setLegacyService(new LegacyFileService( + fileService, new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), - new TestConfigurationService(), - lifecycleService, - new TestStorageService(), - new TestNotificationService(), - { disableWatcher: true }) - ); + )); + instantiationService.stub(IFileService, fileService); instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService)); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); @@ -101,15 +100,15 @@ suite('KeybindingsEditing', () => { }); }); - async function setUpWorkspace(): Promise { + async function setUpWorkspace(): Promise { testDir = path.join(os.tmpdir(), 'vsctests', uuid.generateUuid()); return await mkdirp(testDir, 493); } teardown(() => { - return new Promise((c, e) => { + return new Promise((c) => { if (testDir) { - extfs.del(testDir, os.tmpdir(), () => c(undefined), () => c(undefined)); + rimraf(testDir, RimRafMode.MOVE).then(c, c); } else { c(undefined); } diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index 57226ae1ae..dca4f68ebd 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -182,13 +182,15 @@ export class LabelService implements ILabelService { } // Workspace: Saved - const filename = basename(workspace.configPath); - const workspaceName = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); + let filename = basename(workspace.configPath); + if (endsWith(filename, WORKSPACE_EXTENSION)) { + filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); + } let label; if (options && options.verbose) { - label = localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(joinPath(dirname(workspace.configPath), workspaceName))); + label = localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(joinPath(dirname(workspace.configPath), filename))); } else { - label = localize('workspaceName', "{0} (Workspace)", workspaceName); + label = localize('workspaceName', "{0} (Workspace)", filename); } return this.appendWorkspaceSuffix(label, workspace.configPath); } diff --git a/src/vs/workbench/services/output/common/outputChannelModel.ts b/src/vs/workbench/services/output/common/outputChannelModel.ts index 8bba70b0a0..8c40e8d832 100644 --- a/src/vs/workbench/services/output/common/outputChannelModel.ts +++ b/src/vs/workbench/services/output/common/outputChannelModel.ts @@ -17,7 +17,7 @@ import { isNumber } from 'vs/base/common/types'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { binarySearch } from 'vs/base/common/arrays'; -import { toUint8ArrayBuffer } from 'vs/base/common/uint'; +import { VSBuffer } from 'vs/base/common/buffer'; export interface IOutputChannelModel extends IDisposable { readonly onDidAppendedContent: Event; @@ -129,6 +129,7 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen } } +// TODO@ben see if new watchers can cope with spdlog and avoid polling then class OutputFileListener extends Disposable { private readonly _onDidContentChange = new Emitter(); @@ -259,10 +260,7 @@ class FileOutputChannelModel extends AbstractFileOutputChannelModel implements I } protected getByteLength(str: string): number { - if (typeof Buffer !== 'undefined') { - return Buffer.from(str).byteLength; - } - return toUint8ArrayBuffer(str).byteLength; + return VSBuffer.fromString(str).byteLength; } update(size?: number): void { diff --git a/src/vs/workbench/services/output/node/outputChannelModelService.ts b/src/vs/workbench/services/output/node/outputChannelModelService.ts index f3aab0c89e..bc4cb3c7de 100644 --- a/src/vs/workbench/services/output/node/outputChannelModelService.ts +++ b/src/vs/workbench/services/output/node/outputChannelModelService.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import * as extfs from 'vs/base/node/extfs'; -import { dirname, join } from 'vs/base/common/path'; +import { join } from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import { ITextModel } from 'vs/editor/common/model'; import { URI } from 'vs/base/common/uri'; @@ -13,7 +12,7 @@ import { ThrottledDelayer } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; +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'; @@ -24,34 +23,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Emitter, Event } from 'vs/base/common/event'; -let watchingOutputDir = false; -let callbacks: ((eventType: string, fileName?: string) => void)[] = []; -function watchOutputDirectory(outputDir: string, logService: ILogService, onChange: (eventType: string, fileName: string) => void): IDisposable { - callbacks.push(onChange); - if (!watchingOutputDir) { - const watcherDisposable = extfs.watch(outputDir, (eventType, fileName) => { - for (const callback of callbacks) { - callback(eventType, fileName); - } - }, (error: string) => { - logService.error(error); - }); - watchingOutputDir = true; - return toDisposable(() => { - callbacks = []; - watcherDisposable.dispose(); - }); - } - return toDisposable(() => { }); -} - class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implements IOutputChannelModel { private appender: OutputAppender; private appendedMessage: string; private loadingFromFileInProgress: boolean; private resettingDelayer: ThrottledDelayer; - private readonly rotatingFilePath: string; + private readonly rotatingFilePath: URI; constructor( id: string, @@ -69,8 +47,16 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implement // Use one rotating file to check for main file reset this.appender = new OutputAppender(id, this.file.fsPath); - this.rotatingFilePath = `${id}.1.log`; - this._register(watchOutputDirectory(dirname(this.file.fsPath), logService, (eventType, file) => this.onFileChangedInOutputDirector(eventType, file))); + + const rotatingFilePathDirectory = resources.dirname(this.file); + this.rotatingFilePath = resources.joinPath(rotatingFilePathDirectory, `${id}.1.log`); + + this._register(fileService.watch(rotatingFilePathDirectory)); + this._register(fileService.onFileChanges(e => { + if (e.contains(this.rotatingFilePath)) { + this.resettingDelayer.trigger(() => this.resetModel()); + } + })); this.resettingDelayer = new ThrottledDelayer(50); } @@ -143,13 +129,6 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implement } } - private onFileChangedInOutputDirector(eventType: string, fileName?: string): void { - // Check if rotating file has changed. It changes only when the main file exceeds its limit. - if (this.rotatingFilePath === fileName) { - this.resettingDelayer.trigger(() => this.resetModel()); - } - } - private write(content: string): void { this.appender.append(content); } diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts index 32d637ba5d..3a02fcdac8 100644 --- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -38,7 +38,7 @@ export class DefaultPreferencesEditorInput extends ResourceEditorInput { return DefaultPreferencesEditorInput.ID; } - matches(other: any): boolean { + matches(other: unknown): boolean { if (other instanceof DefaultPreferencesEditorInput) { return true; } @@ -49,11 +49,19 @@ export class DefaultPreferencesEditorInput extends ResourceEditorInput { } } +export interface IKeybindingsEditorSearchOptions { + searchValue: string; + recordKeybindings: boolean; + sortByPrecedence: boolean; +} + export class KeybindingsEditorInput extends EditorInput { static readonly ID: string = 'workbench.input.keybindings'; readonly keybindingsModel: KeybindingsEditorModel; + searchOptions: IKeybindingsEditorSearchOptions | null; + constructor(@IInstantiationService instantiationService: IInstantiationService) { super(); this.keybindingsModel = instantiationService.createInstance(KeybindingsEditorModel, OS); @@ -71,7 +79,7 @@ export class KeybindingsEditorInput extends EditorInput { return Promise.resolve(this.keybindingsModel); } - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { return otherInput instanceof KeybindingsEditorInput; } } @@ -93,7 +101,7 @@ export class SettingsEditor2Input extends EditorInput { this._settingsModel = _preferencesService.createSettings2EditorModel(); } - matches(otherInput: any): boolean { + matches(otherInput: unknown): boolean { return otherInput instanceof SettingsEditor2Input; } diff --git a/src/vs/workbench/services/progress/browser/progressService2.ts b/src/vs/workbench/services/progress/browser/progressService2.ts index 067c9c340e..6a138b54be 100644 --- a/src/vs/workbench/services/progress/browser/progressService2.ts +++ b/src/vs/workbench/services/progress/browser/progressService2.ts @@ -31,7 +31,7 @@ export class ProgressService2 implements IProgressService2 { @IStatusbarService private readonly _statusbarService: IStatusbarService, ) { } - withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: () => void): Promise { + withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: () => void): Promise { const { location } = options; if (typeof location === 'string') { @@ -58,7 +58,7 @@ export class ProgressService2 implements IProgressService2 { } } - private _withWindowProgress(options: IProgressOptions, callback: (progress: IProgress<{ message?: string }>) => Promise): Promise { + private _withWindowProgress(options: IProgressOptions, callback: (progress: IProgress<{ message?: string }>) => Promise): Promise { const task: [IProgressOptions, Progress] = [options, new Progress(() => this._updateWindowProgress())]; @@ -126,7 +126,7 @@ export class ProgressService2 implements IProgressService2 { } } - private _withNotificationProgress

, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: () => void): P { + private _withNotificationProgress

, R = unknown>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: () => void): P { const toDispose: IDisposable[] = []; const createNotification = (message: string | undefined, increment?: number): INotificationHandle | undefined => { @@ -221,7 +221,7 @@ export class ProgressService2 implements IProgressService2 { return p; } - private _withViewletProgress

, R=any>(viewletId: string, task: (progress: IProgress<{ message?: string }>) => P): P { + private _withViewletProgress

, R = unknown>(viewletId: string, task: (progress: IProgress<{ message?: string }>) => P): P { const promise = task(emptyProgress); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 258803980a..ccd5af2f73 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -53,7 +53,7 @@ export interface ISearchResultProvider { clearCache(cacheKey: string): Promise; } -export interface IFolderQuery { +export interface IFolderQuery { folder: U; excludePattern?: glob.IExpression; includePattern?: glob.IExpression; diff --git a/src/vs/workbench/services/search/common/searchExtTypes.ts b/src/vs/workbench/services/search/common/searchExtTypes.ts new file mode 100644 index 0000000000..6e75b70b49 --- /dev/null +++ b/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -0,0 +1,413 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { IProgress } from 'vs/platform/progress/common/progress'; + +export class Position { + constructor(readonly line: number, readonly character: number) { } + + isBefore(other: Position): boolean { return false; } + isBeforeOrEqual(other: Position): boolean { return false; } + isAfter(other: Position): boolean { return false; } + isAfterOrEqual(other: Position): boolean { return false; } + isEqual(other: Position): boolean { return false; } + compareTo(other: Position): number { return 0; } + translate(lineDelta?: number, characterDelta?: number): Position; + translate(change: { lineDelta?: number; characterDelta?: number; }): Position; + translate(_?: any, _2?: any): Position { return new Position(0, 0); } + with(line?: number, character?: number): Position; + with(change: { line?: number; character?: number; }): Position; + with(_: any): Position { return new Position(0, 0); } +} + +export class Range { + readonly start: Position; + readonly end: Position; + + constructor(startLine: number, startCol: number, endLine: number, endCol: number) { + this.start = new Position(startLine, startCol); + this.end = new Position(endLine, endCol); + } + + isEmpty: boolean; + isSingleLine: boolean; + contains(positionOrRange: Position | Range): boolean { return false; } + isEqual(other: Range): boolean { return false; } + intersection(range: Range): Range | undefined { return undefined; } + union(other: Range): Range { return new Range(0, 0, 0, 0); } + + with(start?: Position, end?: Position): Range; + with(change: { start?: Position, end?: Position }): Range; + with(_: any): Range { return new Range(0, 0, 0, 0); } +} + +export type ProviderResult = T | undefined | null | Thenable; + +/** + * A relative pattern is a helper to construct glob patterns that are matched + * relatively to a base path. The base path can either be an absolute file path + * or a [workspace folder](#WorkspaceFolder). + */ +export interface RelativePattern { + + /** + * A base file path to which this pattern will be matched against relatively. + */ + base: string; + + /** + * A file glob pattern like `*.{ts,js}` that will be matched on file paths + * relative to the base path. + * + * Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`, + * the file glob pattern will match on `index.js`. + */ + pattern: string; +} + +/** + * A file glob pattern to match file paths against. This can either be a glob pattern string + * (like `**​/*.{ts,js}` or `*.{ts,js}`) or a [relative pattern](#RelativePattern). + * + * Glob patterns can have the following syntax: + * * `*` to match one or more characters in a path segment + * * `?` to match on one character in a path segment + * * `**` to match any number of path segments, including none + * * `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + * * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) + * + * Note: a backslash (`\`) is not valid within a glob pattern. If you have an existing file + * path to match against, consider to use the [relative pattern](#RelativePattern) support + * that takes care of converting any backslash into slash. Otherwise, make sure to convert + * any backslash to slash when creating the glob pattern. + */ +export type GlobPattern = string | RelativePattern; + +/** + * The parameters of a query for text search. + */ +export interface TextSearchQuery { + /** + * The text pattern to search for. + */ + pattern: string; + + /** + * Whether or not `pattern` should match multiple lines of text. + */ + isMultiline?: boolean; + + /** + * Whether or not `pattern` should be interpreted as a regular expression. + */ + isRegExp?: boolean; + + /** + * Whether or not the search should be case-sensitive. + */ + isCaseSensitive?: boolean; + + /** + * Whether or not to search for whole word matches only. + */ + isWordMatch?: boolean; +} + +/** + * A file glob pattern to match file paths against. + * TODO@roblou - merge this with the GlobPattern docs/definition in vscode.d.ts. + * @see [GlobPattern](#GlobPattern) + */ +export type GlobString = string; + +/** + * Options common to file and text search + */ +export interface SearchOptions { + /** + * The root folder to search within. + */ + folder: URI; + + /** + * Files that match an `includes` glob pattern should be included in the search. + */ + includes: GlobString[]; + + /** + * Files that match an `excludes` glob pattern should be excluded from the search. + */ + excludes: GlobString[]; + + /** + * Whether external files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useIgnoreFiles"`. + */ + useIgnoreFiles: boolean; + + /** + * Whether symlinks should be followed while searching. + * See the vscode setting `"search.followSymlinks"`. + */ + followSymlinks: boolean; + + /** + * Whether global files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useGlobalIgnoreFiles"`. + */ + useGlobalIgnoreFiles: boolean; +} + +/** + * Options to specify the size of the result text preview. + * These options don't affect the size of the match itself, just the amount of preview text. + */ +export interface TextSearchPreviewOptions { + /** + * The maximum number of lines in the preview. + * Only search providers that support multiline search will ever return more than one line in the match. + */ + matchLines: number; + + /** + * The maximum number of characters included per line. + */ + charsPerLine: number; +} + +/** + * Options that apply to text search. + */ +export interface TextSearchOptions extends SearchOptions { + /** + * The maximum number of results to be returned. + */ + maxResults: number; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: TextSearchPreviewOptions; + + /** + * Exclude files larger than `maxFileSize` in bytes. + */ + maxFileSize?: number; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ + encoding?: string; + + /** + * Number of lines of context to include before each match. + */ + beforeContext?: number; + + /** + * Number of lines of context to include after each match. + */ + afterContext?: number; +} + +/** + * Information collected when text search is complete. + */ +export interface TextSearchComplete { + /** + * Whether the search hit the limit on the maximum number of search results. + * `maxResults` on [`TextSearchOptions`](#TextSearchOptions) specifies the max number of results. + * - If exactly that number of matches exist, this should be false. + * - If `maxResults` matches are returned and more exist, this should be true. + * - If search hits an internal limit which is less than `maxResults`, this should be true. + */ + limitHit?: boolean; +} + +/** + * The parameters of a query for file search. + */ +export interface FileSearchQuery { + /** + * The search pattern to match against file paths. + */ + pattern: string; +} + +/** + * Options that apply to file search. + */ +export interface FileSearchOptions extends SearchOptions { + /** + * The maximum number of results to be returned. + */ + maxResults?: number; + + /** + * A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache, + * and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared. + */ + session?: CancellationToken; +} + +/** + * A preview of the text result. + */ +export interface TextSearchMatchPreview { + /** + * The matching lines of text, or a portion of the matching line that contains the match. + */ + text: string; + + /** + * The Range within `text` corresponding to the text of the match. + * The number of matches must match the TextSearchMatch's range property. + */ + matches: Range | Range[]; +} + +/** + * A match from a text search + */ +export interface TextSearchMatch { + /** + * The uri for the matching document. + */ + uri: URI; + + /** + * The range of the match within the document, or multiple ranges for multiple matches. + */ + ranges: Range | Range[]; + + /** + * A preview of the text match. + */ + preview: TextSearchMatchPreview; +} + +/** + * A line of context surrounding a TextSearchMatch. + */ +export interface TextSearchContext { + /** + * The uri for the matching document. + */ + uri: URI; + + /** + * One line of text. + * previewOptions.charsPerLine applies to this + */ + text: string; + + /** + * The line number of this line of context. + */ + lineNumber: number; +} + +export type TextSearchResult = TextSearchMatch | TextSearchContext; + +/** + * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions. + * + * A FileSearchProvider is the more powerful of two ways to implement file search in VS Code. Use a FileSearchProvider if you wish to search within a folder for + * all files that match the user's query. + * + * The FileSearchProvider will be invoked on every keypress in quickopen. When `workspace.findFiles` is called, it will be invoked with an empty query string, + * and in that case, every file in the folder should be returned. + */ +export interface FileSearchProvider { + /** + * Provide the set of files that match a certain file path pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching files. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult; +} + +/** + * A TextSearchProvider provides search results for text results inside files in the workspace. + */ +export interface TextSearchProvider { + /** + * Provide results that match the given text pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: IProgress, token: CancellationToken): ProviderResult; +} + +/** + * Options that can be set on a findTextInFiles search. + */ +export interface FindTextInFilesOptions { + /** + * A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern + * will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern) + * to restrict the search results to a [workspace folder](#WorkspaceFolder). + */ + include?: GlobPattern; + + /** + * A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will + * apply, when `null` no excludes will apply. + */ + exclude?: GlobPattern | null; + + /** + * The maximum number of results to search for + */ + maxResults?: number; + + /** + * Whether external files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useIgnoreFiles"`. + */ + useIgnoreFiles?: boolean; + + /** + * Whether global files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useGlobalIgnoreFiles"`. + */ + useGlobalIgnoreFiles?: boolean; + + /** + * Whether symlinks should be followed while searching. + * See the vscode setting `"search.followSymlinks"`. + */ + followSymlinks?: boolean; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ + encoding?: string; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: TextSearchPreviewOptions; + + /** + * Number of lines of context to include before each match. + */ + beforeContext?: number; + + /** + * Number of lines of context to include after each match. + */ + afterContext?: number; +} \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 539934c056..d678655a62 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -19,8 +19,7 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import * as strings from 'vs/base/common/strings'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import * as extfs from 'vs/base/node/extfs'; -import * as flow from 'vs/base/node/flow'; +import { readdir } from 'vs/base/node/pfs'; import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/common/search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; @@ -128,7 +127,7 @@ export class FileWalker { this.cmdSW = StopWatch.create(false); // For each root folder - flow.parallel(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => { + this.parallel(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => { this.call(this.cmdTraversal, this, folderQuery, onResult, onMessage, (err?: Error) => { if (err) { const errorMessage = toErrorMessage(err); @@ -146,6 +145,34 @@ export class FileWalker { }); } + private parallel(list: T[], fn: (item: T, callback: (err: Error | null, result: E | null) => void) => void, callback: (err: Array | null, result: E[]) => void): void { + const results = new Array(list.length); + const errors = new Array(list.length); + let didErrorOccur = false; + let doneCount = 0; + + if (list.length === 0) { + return callback(null, []); + } + + list.forEach((item, index) => { + fn(item, (error, result) => { + if (error) { + didErrorOccur = true; + results[index] = null; + errors[index] = error; + } else { + results[index] = result; + errors[index] = null; + } + + if (++doneCount === list.length) { + return callback(didErrorOccur ? errors : null, results); + } + }); + }); + } + private call(fun: F, that: any, ...args: any[]): void { try { fun.apply(that, args); @@ -440,7 +467,7 @@ export class FileWalker { // Execute tasks on each file in parallel to optimize throughput const hasSibling = glob.hasSiblingFn(() => files); - flow.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => { + this.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => { // Check canceled if (this.isCanceled || this.isLimitHit) { @@ -489,12 +516,14 @@ export class FileWalker { this.walkedPaths[realpath] = true; // remember as walked // Continue walking - return extfs.readdir(currentAbsolutePath, (error: Error, children: string[]): void => { - if (error || this.isCanceled || this.isLimitHit) { + return readdir(currentAbsolutePath).then(children => { + if (this.isCanceled || this.isLimitHit) { return clb(null); } this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err || null)); + }, error => { + clb(null); }); }); } diff --git a/src/vs/workbench/services/search/node/fileSearchManager.ts b/src/vs/workbench/services/search/node/fileSearchManager.ts index 3cc69a9451..7f7d3da4ef 100644 --- a/src/vs/workbench/services/search/node/fileSearchManager.ts +++ b/src/vs/workbench/services/search/node/fileSearchManager.ts @@ -11,7 +11,7 @@ import * as resources from 'vs/base/common/resources'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; -import * as vscode from 'vscode'; +import { FileSearchProvider, FileSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes'; export interface IInternalFileMatch { base: URI; @@ -45,7 +45,7 @@ class FileSearchEngine { private globalExcludePattern?: glob.ParsedExpression; - constructor(private config: IFileQuery, private provider: vscode.FileSearchProvider, private sessionToken?: CancellationToken) { + constructor(private config: IFileQuery, private provider: FileSearchProvider, private sessionToken?: CancellationToken) { this.filePattern = config.filePattern; this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || undefined; @@ -172,7 +172,7 @@ class FileSearchEngine { }); } - private getSearchOptionsForFolder(fq: IFolderQuery): vscode.FileSearchOptions { + private getSearchOptionsForFolder(fq: IFolderQuery): FileSearchOptions { const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern); const excludes = resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern); @@ -283,7 +283,7 @@ export class FileSearchManager { private readonly sessions = new Map(); - fileSearch(config: IFileQuery, provider: vscode.FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { + fileSearch(config: IFileQuery, provider: FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { const sessionTokenSource = this.getSessionTokenSource(config.cacheKey); const engine = new FileSearchEngine(config, provider, sessionTokenSource && sessionTokenSource.token); diff --git a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts index 44e08546e1..6f383347b5 100644 --- a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts @@ -144,7 +144,8 @@ function globExprsToRgGlobs(patterns: glob.IExpression, folder?: string, exclude } globArgs.push(fixDriveC(key)); - } else if (value && value.when) { + // {{SQL CARBON EDIT}} @todo anthonydresser cast value because we aren't using strict null checks + } else if (value && (value).when) { siblingClauses[key] = value; } }); diff --git a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts index 556489d227..bc8bb69e56 100644 --- a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts +++ b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts @@ -3,24 +3,25 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils'; import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; -import * as vscode from 'vscode'; +import { TextSearchProvider, TextSearchComplete, TextSearchResult, TextSearchQuery, TextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes'; +import { Progress } from 'vs/platform/progress/common/progress'; -export class RipgrepSearchProvider implements vscode.TextSearchProvider { - private inProgress: Set = new Set(); +export class RipgrepSearchProvider implements TextSearchProvider { + private inProgress: Set = new Set(); constructor(private outputChannel: OutputChannel) { process.once('exit', () => this.dispose()); } - provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Promise { + provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { const engine = new RipgrepTextSearchEngine(this.outputChannel); return this.withToken(token, token => engine.provideTextSearchResults(query, options, progress, token)); } - private async withToken(token: vscode.CancellationToken, fn: (token: vscode.CancellationToken) => Promise): Promise { + private async withToken(token: CancellationToken, fn: (token: CancellationToken) => Promise): Promise { const merged = mergedTokenSource(token); this.inProgress.add(merged); const result = await fn(merged.token); @@ -34,7 +35,7 @@ export class RipgrepSearchProvider implements vscode.TextSearchProvider { } } -function mergedTokenSource(token: vscode.CancellationToken): vscode.CancellationTokenSource { +function mergedTokenSource(token: CancellationToken): CancellationTokenSource { const tokenSource = new CancellationTokenSource(); token.onCancellationRequested(() => tokenSource.cancel()); diff --git a/src/vs/workbench/services/search/node/ripgrepSearchUtils.ts b/src/vs/workbench/services/search/node/ripgrepSearchUtils.ts index 21915a0dd4..861667e27e 100644 --- a/src/vs/workbench/services/search/node/ripgrepSearchUtils.ts +++ b/src/vs/workbench/services/search/node/ripgrepSearchUtils.ts @@ -6,8 +6,9 @@ import { startsWith } from 'vs/base/common/strings'; import { ILogService } from 'vs/platform/log/common/log'; import { SearchRange, TextSearchMatch } from 'vs/workbench/services/search/common/search'; -import * as vscode from 'vscode'; import { mapArrayOrNot } from 'vs/base/common/arrays'; +import { URI } from 'vs/base/common/uri'; +import * as searchExtTypes from 'vs/workbench/services/search/common/searchExtTypes'; export type Maybe = T | null | undefined; @@ -16,9 +17,9 @@ export function anchorGlob(glob: string): string { } /** - * Create a vscode.TextSearchResult by using our internal TextSearchResult type for its previewOptions logic. + * Create a vscode.TextSearchMatch by using our internal TextSearchMatch type for its previewOptions logic. */ -export function createTextSearchResult(uri: vscode.Uri, text: string, range: Range | Range[], previewOptions?: vscode.TextSearchPreviewOptions): vscode.TextSearchMatch { +export function createTextSearchResult(uri: URI, text: string, range: searchExtTypes.Range | searchExtTypes.Range[], previewOptions?: searchExtTypes.TextSearchPreviewOptions): searchExtTypes.TextSearchMatch { const searchRange = mapArrayOrNot(range, rangeToSearchRange); const internalResult = new TextSearchMatch(text, searchRange, previewOptions); @@ -33,50 +34,12 @@ export function createTextSearchResult(uri: vscode.Uri, text: string, range: Ran }; } -function rangeToSearchRange(range: Range): SearchRange { +function rangeToSearchRange(range: searchExtTypes.Range): SearchRange { return new SearchRange(range.start.line, range.start.character, range.end.line, range.end.character); } -function searchRangeToRange(range: SearchRange): Range { - return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); -} - -export class Position { - constructor(readonly line: number, readonly character: number) { } - - isBefore(other: Position): boolean { return false; } - isBeforeOrEqual(other: Position): boolean { return false; } - isAfter(other: Position): boolean { return false; } - isAfterOrEqual(other: Position): boolean { return false; } - isEqual(other: Position): boolean { return false; } - compareTo(other: Position): number { return 0; } - translate(lineDelta?: number, characterDelta?: number): Position; - translate(change: { lineDelta?: number; characterDelta?: number; }): Position; - translate(_?: any, _2?: any): Position { return new Position(0, 0); } - with(line?: number, character?: number): Position; - with(change: { line?: number; character?: number; }): Position; - with(_: any): Position { return new Position(0, 0); } -} - -export class Range { - readonly start: Position; - readonly end: Position; - - constructor(startLine: number, startCol: number, endLine: number, endCol: number) { - this.start = new Position(startLine, startCol); - this.end = new Position(endLine, endCol); - } - - isEmpty: boolean; - isSingleLine: boolean; - contains(positionOrRange: Position | Range): boolean { return false; } - isEqual(other: Range): boolean { return false; } - intersection(range: Range): Range | undefined { return undefined; } - union(other: Range): Range { return new Range(0, 0, 0, 0); } - - with(start?: Position, end?: Position): Range; - with(change: { start?: Position, end?: Position }): Range; - with(_: any): Range { return new Range(0, 0, 0, 0); } +function searchRangeToRange(range: SearchRange): searchExtTypes.Range { + return new searchExtTypes.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); } export interface IOutputChannel { diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index ac529fdcd8..ebac0b2291 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -10,12 +10,14 @@ import { NodeStringDecoder, StringDecoder } from 'string_decoder'; import { createRegExp, startsWith, startsWithUTF8BOM, stripUTF8BOM, escapeRegExpCharacters, endsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IExtendedExtensionSearchOptions, SearchError, SearchErrorCode, serializeSearchError } from 'vs/workbench/services/search/common/search'; -import * as vscode from 'vscode'; import { rgPath } from 'vscode-ripgrep'; -import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe, Range } from './ripgrepSearchUtils'; +import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe } from './ripgrepSearchUtils'; import { coalesce } from 'vs/base/common/arrays'; import { splitGlobAware } from 'vs/base/common/glob'; import { groupBy } from 'vs/base/common/collections'; +import { TextSearchQuery, TextSearchOptions, TextSearchResult, TextSearchComplete, TextSearchPreviewOptions, TextSearchContext, TextSearchMatch, Range } from 'vs/workbench/services/search/common/searchExtTypes'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { CancellationToken } from 'vs/base/common/cancellation'; // If vscode-ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); @@ -24,7 +26,7 @@ export class RipgrepTextSearchEngine { constructor(private outputChannel: IOutputChannel) { } - provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Promise { + provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { this.outputChannel.appendLine(`provideTextSearchResults ${query.pattern}, ${JSON.stringify({ ...options, ...{ @@ -53,7 +55,7 @@ export class RipgrepTextSearchEngine { let gotResult = false; const ripgrepParser = new RipgrepParser(options.maxResults, cwd, options.previewOptions); - ripgrepParser.on('result', (match: vscode.TextSearchResult) => { + ripgrepParser.on('result', (match: TextSearchResult) => { gotResult = true; progress.report(match); }); @@ -155,7 +157,7 @@ export class RipgrepParser extends EventEmitter { private numResults = 0; - constructor(private maxResults: number, private rootFolder: string, private previewOptions?: vscode.TextSearchPreviewOptions) { + constructor(private maxResults: number, private rootFolder: string, private previewOptions?: TextSearchPreviewOptions) { super(); this.stringDecoder = new StringDecoder(); } @@ -169,7 +171,7 @@ export class RipgrepParser extends EventEmitter { } - on(event: 'result', listener: (result: vscode.TextSearchResult) => void): this; + on(event: 'result', listener: (result: TextSearchResult) => void): this; on(event: 'hitLimit', listener: () => void): this; on(event: string, listener: (...args: any[]) => void): this { super.on(event, listener); @@ -240,7 +242,7 @@ export class RipgrepParser extends EventEmitter { } } - private createTextSearchMatch(data: IRgMatch, uri: vscode.Uri): vscode.TextSearchMatch { + private createTextSearchMatch(data: IRgMatch, uri: URI): TextSearchMatch { const lineNumber = data.line_number - 1; let isBOMStripped = false; let fullText = bytesOrTextToString(data.lines); @@ -290,7 +292,7 @@ export class RipgrepParser extends EventEmitter { return createTextSearchResult(uri, fullText, ranges, this.previewOptions); } - private createTextSearchContext(data: IRgMatch, uri: URI): vscode.TextSearchContext[] { + private createTextSearchContext(data: IRgMatch, uri: URI): TextSearchContext[] { const text = bytesOrTextToString(data.lines); const startLine = data.line_number; return text @@ -305,7 +307,7 @@ export class RipgrepParser extends EventEmitter { }); } - private onResult(match: vscode.TextSearchResult): void { + private onResult(match: TextSearchResult): void { this.emit('result', match); } } @@ -333,7 +335,7 @@ function getNumLinesAndLastNewlineLength(text: string): { numLines: number, last return { numLines, lastLineLength }; } -function getRgArgs(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions): string[] { +function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): string[] { const args = ['--hidden']; args.push(query.isCaseSensitive ? '--case-sensitive' : '--ignore-case'); diff --git a/src/vs/workbench/services/search/node/textSearchAdapter.ts b/src/vs/workbench/services/search/node/textSearchAdapter.ts index be96dce3db..0576ec2651 100644 --- a/src/vs/workbench/services/search/node/textSearchAdapter.ts +++ b/src/vs/workbench/services/search/node/textSearchAdapter.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; -import * as extfs from 'vs/base/node/extfs'; +import * as pfs from 'vs/base/node/pfs'; import { IFileMatch, IProgressMessage, ITextQuery, ITextSearchStats, ITextSearchMatch, ISerializedFileMatch, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search'; import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager'; @@ -30,7 +30,7 @@ export class TextSearchEngineAdapter { onMessage({ message: msg }); } }; - const textSearchManager = new TextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), extfs); + const textSearchManager = new TextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs); return new Promise((resolve, reject) => { return textSearchManager .search( diff --git a/src/vs/workbench/services/search/node/textSearchManager.ts b/src/vs/workbench/services/search/node/textSearchManager.ts index 33b1d6ca75..876864d49e 100644 --- a/src/vs/workbench/services/search/node/textSearchManager.ts +++ b/src/vs/workbench/services/search/node/textSearchManager.ts @@ -11,9 +11,9 @@ import * as glob from 'vs/base/common/glob'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { toCanonicalName } from 'vs/base/node/encoding'; -import * as extfs from 'vs/base/node/extfs'; +import * as pfs from 'vs/base/node/pfs'; import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; -import * as vscode from 'vscode'; +import { TextSearchProvider, TextSearchResult, TextSearchMatch, TextSearchComplete, Range, TextSearchOptions, TextSearchQuery } from 'vs/workbench/services/search/common/searchExtTypes'; export class TextSearchManager { @@ -22,7 +22,7 @@ export class TextSearchManager { private isLimitHit: boolean; private resultCount = 0; - constructor(private query: ITextQuery, private provider: vscode.TextSearchProvider, private _extfs: typeof extfs = extfs) { + constructor(private query: ITextQuery, private provider: TextSearchProvider, private _pfs: typeof pfs = pfs) { } search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { @@ -34,7 +34,7 @@ export class TextSearchManager { this.collector = new TextSearchResultsCollector(onProgress); let isCanceled = false; - const onResult = (result: vscode.TextSearchResult, folderIdx: number) => { + const onResult = (result: TextSearchResult, folderIdx: number) => { if (isCanceled) { return; } @@ -79,14 +79,14 @@ export class TextSearchManager { }); } - private resultSize(result: vscode.TextSearchResult): number { - const match = result; + private resultSize(result: TextSearchResult): number { + const match = result; return Array.isArray(match.ranges) ? match.ranges.length : 1; } - private trimResultToSize(result: vscode.TextSearchMatch, size: number): vscode.TextSearchMatch { + private trimResultToSize(result: TextSearchMatch, size: number): TextSearchMatch { const rangesArr = Array.isArray(result.ranges) ? result.ranges : [result.ranges]; const matchesArr = Array.isArray(result.preview.matches) ? result.preview.matches : [result.preview.matches]; @@ -100,11 +100,11 @@ export class TextSearchManager { }; } - private searchInFolder(folderQuery: IFolderQuery, onResult: (result: vscode.TextSearchResult) => void, token: CancellationToken): Promise { + private searchInFolder(folderQuery: IFolderQuery, onResult: (result: TextSearchResult) => void, token: CancellationToken): Promise { const queryTester = new QueryGlobTester(this.query, folderQuery); const testingPs: Promise[] = []; const progress = { - report: (result: vscode.TextSearchResult) => { + report: (result: TextSearchResult) => { if (!this.validateProviderResult(result)) { return; } @@ -135,7 +135,7 @@ export class TextSearchManager { }); } - private validateProviderResult(result: vscode.TextSearchResult): boolean { + private validateProviderResult(result: TextSearchResult): boolean { if (extensionResultIsMatch(result)) { if (Array.isArray(result.ranges)) { if (!Array.isArray(result.preview.matches)) { @@ -143,7 +143,7 @@ export class TextSearchManager { return false; } - if ((result.preview.matches).length !== result.ranges.length) { + if ((result.preview.matches).length !== result.ranges.length) { console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.'); return false; } @@ -159,22 +159,14 @@ export class TextSearchManager { } private readdir(dirname: string): Promise { - return new Promise((resolve, reject) => { - this._extfs.readdir(dirname, (err, files) => { - if (err) { - return reject(err); - } - - resolve(files); - }); - }); + return this._pfs.readdir(dirname); } - private getSearchOptionsForFolder(fq: IFolderQuery): vscode.TextSearchOptions { + private getSearchOptionsForFolder(fq: IFolderQuery): TextSearchOptions { const includes = resolvePatternsForProvider(this.query.includePattern, fq.includePattern); const excludes = resolvePatternsForProvider(this.query.excludePattern, fq.excludePattern); - const options = { + const options = { folder: URI.from(fq.folder), excludes, includes, @@ -193,8 +185,8 @@ export class TextSearchManager { } } -function patternInfoToQuery(patternInfo: IPatternInfo): vscode.TextSearchQuery { - return { +function patternInfoToQuery(patternInfo: IPatternInfo): TextSearchQuery { + return { isCaseSensitive: patternInfo.isCaseSensitive || false, isRegExp: patternInfo.isRegExp || false, isWordMatch: patternInfo.isWordMatch || false, @@ -214,7 +206,7 @@ export class TextSearchResultsCollector { this._batchedCollector = new BatchedCollector(512, items => this.sendItems(items)); } - add(data: vscode.TextSearchResult, folderIdx: number): void { + add(data: TextSearchResult, folderIdx: number): void { // Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector. // This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search // providers that send results in random order. We could do this step afterwards instead. @@ -251,8 +243,8 @@ export class TextSearchResultsCollector { } } -function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSearchResult { - // Warning: result from RipgrepTextSearchEH has fake vscode.Range. Don't depend on any other props beyond these... +function extensionResultToFrontendResult(data: TextSearchResult): ITextSearchResult { + // Warning: result from RipgrepTextSearchEH has fake Range. Don't depend on any other props beyond these... if (extensionResultIsMatch(data)) { return { preview: { @@ -279,8 +271,8 @@ function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSe } } -export function extensionResultIsMatch(data: vscode.TextSearchResult): data is vscode.TextSearchMatch { - return !!(data).preview; +export function extensionResultIsMatch(data: TextSearchResult): data is TextSearchMatch { + return !!(data).preview; } /** diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts index 159fa33324..68e966f693 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts @@ -6,9 +6,8 @@ import * as assert from 'assert'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { Range } from 'vs/workbench/services/search/node/ripgrepSearchUtils'; import { fixRegexCRMatchingNonWordClass, fixRegexCRMatchingWhitespaceClass, fixRegexEndingPattern, fixRegexNewline, IRgMatch, IRgMessage, RipgrepParser, unicodeEscapesToPCRE2, fixNewline } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; -import { TextSearchResult } from 'vscode'; +import { Range, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; suite('RipgrepTextSearchEngine', () => { test('unicodeEscapesToPCRE2', async () => { diff --git a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts index 5fc412e22f..c654d90a92 100644 --- a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts +++ b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; +import { Progress } from 'vs/platform/progress/common/progress'; import { ITextQuery, QueryType } from 'vs/workbench/services/search/common/search'; +import { ProviderResult, TextSearchComplete, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager'; -import * as vscode from 'vscode'; suite('TextSearchManager', () => { test('fixes encoding', async () => { let correctEncoding = false; - const provider: vscode.TextSearchProvider = { - provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): vscode.ProviderResult { + const provider: TextSearchProvider = { + provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): ProviderResult { correctEncoding = options.encoding === 'windows-1252'; return null; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 69df1af9cd..50bccaa526 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -23,7 +23,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RunOnceScheduler, timeout } from 'vs/base/common/async'; import { ITextBufferFactory } from 'vs/editor/common/model'; -import { IHashService } from 'vs/workbench/services/hash/common/hashService'; +import { hash } from 'vs/base/common/hash'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { isLinux } from 'vs/base/common/platform'; @@ -88,7 +88,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @IBackupFileService private readonly backupFileService: IBackupFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IHashService private readonly hashService: IHashService, @ILogService private readonly logService: ILogService ) { super(modelService, modeService); @@ -215,7 +214,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Unset flags const undo = this.setDirty(false); - let loadPromise: Promise; + let loadPromise: Promise; if (soft) { loadPromise = Promise.resolve(); } else { @@ -734,7 +733,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this._onDidStateChange.fire(StateChange.SAVED); // Telemetry - let telemetryPromise: Thenable; const settingsType = this.getTypeIfSettings(); if (settingsType) { /* __GDPR__ @@ -743,22 +741,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } */ this.telemetryService.publicLog('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data - - telemetryPromise = Promise.resolve(); } else { - telemetryPromise = this.getTelemetryData(options.reason).then(data => { - /* __GDPR__ + /* __GDPR__ "filePUT" : { "${include}": [ "${FileTelemetryData}" ] } */ - this.telemetryService.publicLog('filePUT', data); - }); + this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason)); } - - return telemetryPromise; }, error => { if (!error) { error = new Error('Unknown Save Error'); // TODO@remote we should never get null as error (https://github.com/Microsoft/vscode/issues/55051) @@ -824,32 +816,30 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return ''; } - private getTelemetryData(reason: number | undefined): Thenable { - return this.hashService.createSHA1(this.resource.fsPath).then(hashedPath => { - const ext = extname(this.resource); - const fileName = basename(this.resource); - const telemetryData = { - mimeType: guessMimeTypes(this.resource.fsPath).join(', '), - ext, - path: hashedPath, - reason - }; + private getTelemetryData(reason: number | undefined): object { + const ext = extname(this.resource); + const fileName = basename(this.resource); + const telemetryData = { + mimeType: guessMimeTypes(this.resource.fsPath).join(', '), + ext, + path: hash(this.resource.fsPath), + reason + }; - if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) { - telemetryData['whitelistedjson'] = fileName; + if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) { + telemetryData['whitelistedjson'] = fileName; + } + + /* __GDPR__FRAGMENT__ + "FileTelemetryData" : { + "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } - - /* __GDPR__FRAGMENT__ - "FileTelemetryData" : { - "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - return telemetryData; - }); + */ + return telemetryData; } private doTouch(versionId: number): Promise { @@ -918,7 +908,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - private onSaveError(error: any): void { + private onSaveError(error: Error): void { // Prepare handler if (!TextFileEditorModel.saveErrorHandler) { @@ -1151,7 +1141,7 @@ class DefaultSaveErrorHandler implements ISaveErrorHandler { constructor(@INotificationService private readonly notificationService: INotificationService) { } - onSaveError(error: any, model: TextFileEditorModel): void { + onSaveError(error: Error, model: TextFileEditorModel): void { this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.getResource()), toErrorMessage(error, false))); } } diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index af33f39c21..31b0492c1a 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -22,7 +22,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' 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'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -50,7 +50,7 @@ export interface IBackupResult { */ export class TextFileService extends Disposable implements ITextFileService { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; private readonly _onAutoSaveConfigurationChange: Emitter = this._register(new Emitter()); get onAutoSaveConfigurationChange(): Event { return this._onAutoSaveConfigurationChange.event; } @@ -118,7 +118,7 @@ export class TextFileService extends Disposable implements ITextFileService { isReadonly: streamContent.isReadonly, size: streamContent.size, value: res - } as IRawTextContent; + }; }); }); } @@ -292,7 +292,7 @@ export class TextFileService extends Disposable implements ITextFileService { } private backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): Promise { - return this.windowsService.getWindowCount().then(windowCount => { + return this.windowsService.getWindowCount().then(windowCount => { // 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 @@ -534,7 +534,7 @@ export class TextFileService extends Disposable implements ITextFileService { saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise; saveAll(resources: URI[], options?: ISaveOptions): Promise; - saveAll(arg1?: any, options?: ISaveOptions): Promise { + saveAll(arg1?: boolean | URI[], options?: ISaveOptions): Promise { // get all dirty let toSave: URI[] = []; @@ -649,9 +649,7 @@ export class TextFileService extends Disposable implements ITextFileService { })).then(r => ({ results: mapResourceToResult.values() })); } - private getFileModels(resources?: URI[]): ITextFileEditorModel[]; - private getFileModels(resource?: URI): ITextFileEditorModel[]; - private getFileModels(arg1?: any): ITextFileEditorModel[] { + private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] { if (Array.isArray(arg1)) { const models: ITextFileEditorModel[] = []; (arg1).forEach(resource => { @@ -664,10 +662,8 @@ export class TextFileService extends Disposable implements ITextFileService { return this._models.getAll(arg1); } - private getDirtyFileModels(resources?: URI[]): ITextFileEditorModel[]; - private getDirtyFileModels(resource?: URI): ITextFileEditorModel[]; - private getDirtyFileModels(arg1?: any): ITextFileEditorModel[] { - return this.getFileModels(arg1).filter(model => model.isDirty()); + private getDirtyFileModels(resources?: URI | URI[]): ITextFileEditorModel[] { + return this.getFileModels(resources).filter(model => model.isDirty()); } saveAs(resource: URI, target?: URI, options?: ISaveOptions): Promise { @@ -748,7 +744,7 @@ export class TextFileService extends Disposable implements ITextFileService { // 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 => { + targetModelResolver = this.fileService.exists(target).then(exists => { targetExists = exists; // create target model adhoc if file does not exist yet @@ -756,7 +752,7 @@ export class TextFileService extends Disposable implements ITextFileService { return this.fileService.updateContent(target, ''); } - return undefined; + return Promise.resolve(undefined); }).then(() => this.models.loadOrCreate(target)); } @@ -899,13 +895,13 @@ export class TextFileService extends Disposable implements ITextFileService { } move(source: URI, target: URI, overwrite?: boolean): Promise { - const waitForPromises: Promise[] = []; + const waitForPromises: Promise[] = []; // Event this._onWillMove.fire({ oldResource: source, newResource: target, - waitUntil(promise: Promise) { + waitUntil(promise: Promise) { waitForPromises.push(promise.then(undefined, errors.onUnexpectedError)); } }); @@ -916,7 +912,7 @@ export class TextFileService extends Disposable implements ITextFileService { 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(); + 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 }); @@ -925,7 +921,7 @@ export class TextFileService extends Disposable implements ITextFileService { return handleTargetModelPromise.then(() => { // Handle dirty source models if existing (if source URI is a folder, this can be multiple) - let handleDirtySourceModels: Promise; + let handleDirtySourceModels: Promise; const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */)); const dirtyTargetModels: URI[] = []; if (dirtySourceModels.length) { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index f21d68ddc7..3a64e58d77 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -265,7 +265,7 @@ export interface IResolvedTextFileEditorModel extends ITextFileEditorModel { export interface IWillMoveEvent { oldResource: URI; newResource: URI; - waitUntil(p: Promise): void; + waitUntil(p: Promise): void; } export interface ITextFileService extends IDisposable { diff --git a/src/vs/workbench/services/themes/browser/colorThemeData.ts b/src/vs/workbench/services/themes/browser/colorThemeData.ts index 2df353155f..2fed7f90a9 100644 --- a/src/vs/workbench/services/themes/browser/colorThemeData.ts +++ b/src/vs/workbench/services/themes/browser/colorThemeData.ts @@ -58,13 +58,40 @@ export class ColorThemeData implements IColorTheme { } get tokenColors(): ITokenColorizationRule[] { + const result: ITokenColorizationRule[] = []; + + // the default rule (scope empty) is always the first rule. Ignore all other default rules. + const foreground = this.getColor(editorForeground) || this.getDefault(editorForeground)!; + const background = this.getColor(editorBackground) || this.getDefault(editorBackground)!; + result.push({ + settings: { + foreground: Color.Format.CSS.formatHexA(foreground), + background: Color.Format.CSS.formatHexA(background) + } + }); + + let hasDefaultTokens = false; + + function addRule(rule: ITokenColorizationRule) { + if (rule.scope && rule.settings) { + if (rule.scope === 'token.info-token') { + hasDefaultTokens = true; + } + result.push(rule); + } + } + + this.themeTokenColors.forEach(addRule); // Add the custom colors after the theme colors // so that they will override them - return this.themeTokenColors.concat(this.customTokenColors); + this.customTokenColors.forEach(addRule); + + if (!hasDefaultTokens) { + defaultThemeColors[this.type].forEach(addRule); + } + return result; } - - public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined { let color: Color | undefined = this.customColorMap[colorId]; if (color) { @@ -93,9 +120,6 @@ export class ColorThemeData implements IColorTheme { if (types.isObject(themeSpecificColors)) { this.overwriteCustomColors(themeSpecificColors); } - if (this.themeTokenColors && this.themeTokenColors.length) { - updateDefaultRuleSettings(this.themeTokenColors[0], this); - } } private overwriteCustomColors(colors: IColorCustomizations) { @@ -155,32 +179,13 @@ export class ColorThemeData implements IColorTheme { if (!this.location) { return Promise.resolve(undefined); } + this.themeTokenColors = []; + this.colorMap = {}; return _loadColorTheme(fileService, this.location, this.themeTokenColors, this.colorMap).then(_ => { this.isLoaded = true; - this.sanitizeTokenColors(); }); } - /** - * Place the default settings first and add the token-info rules - */ - private sanitizeTokenColors() { - let hasDefaultTokens = false; - let updatedTokenColors: ITokenColorizationRule[] = [updateDefaultRuleSettings({ settings: {} }, this)]; - this.themeTokenColors.forEach(rule => { - if (rule.scope && rule.settings) { - if (rule.scope === 'token.info-token') { - hasDefaultTokens = true; - } - updatedTokenColors.push(rule); - } - }); - if (!hasDefaultTokens) { - updatedTokenColors.push(...defaultThemeColors[this.type]); - } - this.themeTokenColors = updatedTokenColors; - } - toStorageData() { let colorMapData = {}; for (let key in this.colorMap) { @@ -200,7 +205,7 @@ export class ColorThemeData implements IColorTheme { } hasEqualData(other: ColorThemeData) { - return objects.equals(this.colorMap, other.colorMap) && objects.equals(this.tokenColors, other.tokenColors); + return objects.equals(this.colorMap, other.colorMap) && objects.equals(this.themeTokenColors, other.themeTokenColors); } get baseTheme(): string { @@ -220,7 +225,7 @@ export class ColorThemeData implements IColorTheme { static createUnloadedTheme(id: string): ColorThemeData { let themeData = new ColorThemeData(id, '', '__' + id); themeData.isLoaded = false; - themeData.themeTokenColors = [{ settings: {} }]; + themeData.themeTokenColors = []; themeData.watch = false; return themeData; } @@ -228,7 +233,7 @@ export class ColorThemeData implements IColorTheme { static createLoadedEmptyTheme(id: string, settingsId: string): ColorThemeData { let themeData = new ColorThemeData(id, '', settingsId); themeData.isLoaded = true; - themeData.themeTokenColors = [{ settings: {} }]; + themeData.themeTokenColors = []; themeData.watch = false; return themeData; } @@ -357,14 +362,6 @@ function _loadSyntaxTokens(fileService: IFileService, themeLocation: URI, result }); } -function updateDefaultRuleSettings(defaultRule: ITokenColorizationRule, theme: ColorThemeData): ITokenColorizationRule { - const foreground = theme.getColor(editorForeground) || theme.getDefault(editorForeground)!; - const background = theme.getColor(editorBackground) || theme.getDefault(editorBackground)!; - defaultRule.settings.foreground = Color.Format.CSS.formatHexA(foreground); - defaultRule.settings.background = Color.Format.CSS.formatHexA(background); - return defaultRule; -} - let defaultThemeColors: { [baseTheme: string]: ITokenColorizationRule[] } = { 'light': [ { scope: 'token.info-token', settings: { foreground: '#316bcd' } }, diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 036cb4fac9..3b8b930b5c 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -17,7 +17,7 @@ import { ColorThemeData } from './colorThemeData'; import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema'; -import { IDisposable } from 'vs/base/common/lifecycle'; +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'; @@ -77,11 +77,13 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private container: HTMLElement; private readonly onColorThemeChange: Emitter; private watchedColorThemeLocation: URI | undefined; + private watchedColorThemeDisposable: IDisposable; private iconThemeStore: FileIconThemeStore; private currentIconTheme: FileIconThemeData; private readonly onFileIconThemeChange: Emitter; private watchedIconThemeLocation: URI | undefined; + private watchedIconThemeDisposable: IDisposable; private themingParticipantChangeListener: IDisposable; @@ -264,7 +266,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (devThemes.length) { return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); } else { - return this.setFileIconTheme(theme && theme.id, undefined); + return this.setFileIconTheme(theme && theme.id || DEFAULT_ICON_THEME_SETTING_VALUE, undefined); } }); }), @@ -287,7 +289,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { let iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); if (iconThemeSetting !== this.currentIconTheme.settingsId) { this.iconThemeStore.findThemeBySettingsId(iconThemeSetting).then(theme => { - this.setFileIconTheme(theme && theme.id, undefined); + this.setFileIconTheme(theme && theme.id || DEFAULT_ICON_THEME_SETTING_VALUE, undefined); }); } } @@ -395,13 +397,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } if (this.fileService && !resources.isEqual(newTheme.location, this.watchedColorThemeLocation)) { - if (this.watchedColorThemeLocation) { - this.fileService.unwatch(this.watchedColorThemeLocation); - this.watchedColorThemeLocation = undefined; - } + dispose(this.watchedColorThemeDisposable); + this.watchedColorThemeLocation = undefined; + if (newTheme.location && (newTheme.watch || !!this.environmentService.extensionDevelopmentLocationURI)) { this.watchedColorThemeLocation = newTheme.location; - this.fileService.watch(this.watchedColorThemeLocation); + this.watchedColorThemeDisposable = this.fileService.watch(newTheme.location); } } @@ -513,13 +514,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } if (this.fileService && !resources.isEqual(iconThemeData.location, this.watchedIconThemeLocation)) { - if (this.watchedIconThemeLocation) { - this.fileService.unwatch(this.watchedIconThemeLocation); - this.watchedIconThemeLocation = undefined; - } + dispose(this.watchedIconThemeDisposable); + this.watchedIconThemeLocation = undefined; + if (iconThemeData.location && (iconThemeData.watch || !!this.environmentService.extensionDevelopmentLocationURI)) { this.watchedIconThemeLocation = iconThemeData.location; - this.fileService.watch(this.watchedIconThemeLocation); + this.watchedIconThemeDisposable = this.fileService.watch(iconThemeData.location); } } diff --git a/src/vs/workbench/services/untitled/common/untitledEditorService.ts b/src/vs/workbench/services/untitled/common/untitledEditorService.ts index fe2865f0d0..6f3defe6d2 100644 --- a/src/vs/workbench/services/untitled/common/untitledEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledEditorService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +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'; @@ -113,7 +113,7 @@ export interface IUntitledEditorService { export class UntitledEditorService extends Disposable implements IUntitledEditorService { - _serviceBrand: any; + _serviceBrand: ServiceIdentifier; private mapResourceToInput = new ResourceMap(); private mapResourceToAssociatedFilePath = new ResourceMap(); diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts similarity index 91% rename from src/vs/workbench/services/workspace/node/workspaceEditingService.ts rename to src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts index 95f2765276..9f66b0f199 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts @@ -10,7 +10,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IWindowService, MessageBoxOptions, IWindowsService } from 'vs/platform/windows/common/windows'; import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing'; import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; -import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; +import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { StorageService } from 'vs/platform/storage/node/storageService'; import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; @@ -203,7 +203,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { // If we are in no-workspace or single-folder workspace, adding folders has to // enter a workspace. if (state !== WorkbenchState.WORKSPACE) { - let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri } as IWorkspaceFolderCreationData)); + let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd); newWorkspaceFolders = distinct(newWorkspaceFolders, folder => getComparisonKey(folder.uri)); @@ -334,64 +334,37 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { ); } - enterWorkspace(path: URI): Promise { + async enterWorkspace(path: URI): Promise { if (!!this.environmentService.extensionTestsLocationURI) { return Promise.reject(new Error('Entering a new workspace is not possible in tests.')); } + const workspace = await this.workspaceService.getWorkspaceIdentifier(path); + // Settings migration (only if we come from a folder workspace) + if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + await this.migrateWorkspaceSettings(workspace); + } + const workspaceImpl = this.contextService as WorkspaceService; + await workspaceImpl.initialize(workspace); + // Restart extension host if first root folder changed (impact on deprecated workspace.rootPath API) // Stop the extension host first to give extensions most time to shutdown this.extensionService.stopExtensionHost(); - let extensionHostStarted: boolean = false; - const startExtensionHost = () => { - if (this.windowService.getConfiguration().remoteAuthority) { - this.windowService.reloadWindow(); // TODO aeschli: workaround until restarting works + const result = await this.windowService.enterWorkspace(path); + if (result) { + await this.migrateStorage(result.workspace); + // Reinitialize backup service + if (this.backupFileService instanceof BackupFileService) { + this.backupFileService.initialize(result.backupPath!); } + } + if (this.windowService.getConfiguration().remoteAuthority) { + this.windowService.reloadWindow(); // TODO aeschli: workaround until restarting works + } else { this.extensionService.startExtensionHost(); - extensionHostStarted = true; - }; - - return this.windowService.enterWorkspace(path).then(result => { - - // Migrate storage and settings if we are to enter a workspace - if (result) { - return this.migrate(result.workspace).then(() => { - - // Reinitialize backup service - if (this.backupFileService instanceof BackupFileService) { - this.backupFileService.initialize(result.backupPath!); - } - - // Reinitialize configuration service - const workspaceImpl = this.contextService as WorkspaceService; - return workspaceImpl.initialize(result.workspace, startExtensionHost); - }); - } - - return Promise.resolve(); - }).then(undefined, error => { - if (!extensionHostStarted) { - startExtensionHost(); // start the extension host if not started - } - - return Promise.reject(error); - }); - } - - private migrate(toWorkspace: IWorkspaceIdentifier): Promise { - - // Storage migration - return this.migrateStorage(toWorkspace).then(() => { - - // Settings migration (only if we come from a folder workspace) - if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { - return this.migrateWorkspaceSettings(toWorkspace); - } - - return undefined; - }); + } } private migrateStorage(toWorkspace: IWorkspaceIdentifier): Promise { 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 548f2d8947..d65b46c434 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { mapArrayOrNot } from 'vs/base/common/arrays'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -9,12 +10,11 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import { dispose } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; -import * as extfs from 'vs/base/node/extfs'; -import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteStats, ISearchQuery, ITextQuery, QueryType, resultIsMatch } from 'vs/workbench/services/search/common/search'; +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 { extensionResultIsMatch } from 'vs/workbench/services/search/node/textSearchManager'; +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'; import * as vscode from 'vscode'; @@ -55,7 +55,11 @@ class MockMainThreadSearch implements MainThreadSearchShape { } } -let mockExtfs: Partial; +let mockPFS: Partial; + +export function extensionResultIsMatch(data: vscode.TextSearchResult): data is vscode.TextSearchMatch { + return !!(data).preview; +} suite('ExtHostSearch', () => { async function registerTestTextSearchProvider(provider: vscode.TextSearchProvider, scheme = 'file'): Promise { @@ -130,8 +134,8 @@ suite('ExtHostSearch', () => { rpcProtocol.set(MainContext.MainThreadSearch, mockMainThreadSearch); - mockExtfs = {}; - extHostSearch = new ExtHostSearch(rpcProtocol, null!, logService, mockExtfs as typeof extfs); + mockPFS = {}; + extHostSearch = new ExtHostSearch(rpcProtocol, null!, logService, mockPFS as any); }); teardown(() => { @@ -856,14 +860,14 @@ suite('ExtHostSearch', () => { }); test('basic sibling clause', async () => { - mockExtfs.readdir = (_path: string, callback: (error: Error, files: string[]) => void) => { + mockPFS.readdir = (_path: string) => { if (_path === rootFolderA.fsPath) { - callback(null!, [ + return Promise.resolve([ 'file1.js', 'file1.ts' ]); } else { - callback(new Error('Wrong path'), null!); + return Promise.reject(new Error('Wrong path')); } }; @@ -899,21 +903,21 @@ suite('ExtHostSearch', () => { }); test('multiroot sibling clause', async () => { - mockExtfs.readdir = (_path: string, callback: (error: Error, files: string[]) => void) => { + mockPFS.readdir = (_path: string) => { if (_path === joinPath(rootFolderA, 'folder').fsPath) { - callback(null!, [ + return Promise.resolve([ 'fileA.scss', 'fileA.css', 'file2.css' ]); } else if (_path === rootFolderB.fsPath) { - callback(null!, [ + return Promise.resolve([ 'fileB.ts', 'fileB.js', 'file3.js' ]); } else { - callback(new Error('Wrong path'), null!); + return Promise.reject(new Error('Wrong path')); } }; diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts index 83a7bb023b..a0bbe0957d 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts @@ -13,7 +13,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { MainThreadConfiguration } from 'vs/workbench/api/browser/mainThreadConfiguration'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; +import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; suite('MainThreadConfiguration', function () { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 89fcdbf374..f95cd4ff76 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -48,7 +48,6 @@ import { IRecentlyOpened, IRecent } from 'vs/platform/history/common/history'; import { ITextResourceConfigurationService, ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration'; import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; import { IMenuService, MenuId, IMenu, ISerializableCommandAction } from 'vs/platform/actions/common/actions'; -import { IHashService } from 'vs/workbench/services/hash/common/hashService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, IModelDecorationOptions, ITextModel } from 'vs/editor/common/model'; @@ -309,7 +308,6 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); instantiationService.stub(IThemeService, new TestThemeService()); - instantiationService.stub(IHashService, new TestHashService()); instantiationService.stub(ILogService, new TestLogService()); instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService([new TestEditorGroup(0)])); instantiationService.stub(ILabelService, instantiationService.createInstance(LabelService)); @@ -897,6 +895,7 @@ export class TestFileService implements IFileService { private readonly _onAfterOperation: Emitter; readonly onWillActivateFileSystemProvider = Event.None; + readonly onError: Event = Event.None; private content = 'Hello Html'; @@ -1026,16 +1025,14 @@ export class TestFileService implements IFileService { return resource.scheme === 'file'; } - hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise { return Promise.resolve(false); } + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean { return false; } del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean }): Promise { return Promise.resolve(); } - watch(_resource: URI): void { - } - - unwatch(_resource: URI): void { + watch(_resource: URI): IDisposable { + return Disposable.None; } getWriteEncoding(_resource: URI): IResourceEncoding { @@ -1556,14 +1553,6 @@ export class TestTextResourcePropertiesService implements ITextResourcePropertie } -export class TestHashService implements IHashService { - _serviceBrand: any; - - createSHA1(content: string): Thenable { - return Promise.resolve(content); - } -} - export class TestSharedProcessService implements ISharedProcessService { _serviceBrand: ServiceIdentifier; @@ -1582,7 +1571,9 @@ export class NullFileSystemProvider implements IFileSystemProvider { onDidChangeCapabilities: Event = Event.None; onDidChangeFile: Event = Event.None; - watch(resource: URI, opts: IWatchOptions): IDisposable { return Disposable.None; } + constructor(private disposableFactory: () => IDisposable = () => Disposable.None) { } + + watch(resource: URI, opts: IWatchOptions): IDisposable { return this.disposableFactory(); } stat(resource: URI): Promise { return Promise.resolve(undefined!); } mkdir(resource: URI): Promise { return Promise.resolve(undefined!); } readdir(resource: URI): Promise<[string, FileType][]> { return Promise.resolve(undefined!); } diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 8cefda42ea..16aa1e5aa9 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -94,16 +94,16 @@ import { RelayURLService } from 'vs/platform/url/electron-browser/urlService'; 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/hash/node/hashService'; import 'vs/workbench/services/textMate/electron-browser/textMateService'; import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; -import 'vs/workbench/services/workspace/node/workspaceEditingService'; +import 'vs/workbench/services/workspace/electron-browser/workspaceEditingService'; import 'vs/workbench/services/extensions/common/inactiveExtensionUrlHandler'; import 'vs/workbench/services/decorations/browser/decorationsService'; import 'vs/workbench/services/search/node/searchService'; import 'vs/workbench/services/progress/browser/progressService2'; import 'vs/workbench/services/editor/browser/codeEditorService'; import 'vs/workbench/services/broadcast/electron-browser/broadcastService'; +import 'vs/workbench/services/extensions/electron-browser/extensionHostDebugService'; import 'vs/workbench/services/preferences/browser/preferencesService'; import 'vs/workbench/services/output/node/outputChannelModelService'; import 'vs/workbench/services/configuration/common/jsonEditingService'; @@ -125,7 +125,7 @@ import 'vs/workbench/services/commands/common/commandService'; import 'vs/workbench/services/themes/browser/workbenchThemeService'; import 'vs/workbench/services/extensions/electron-browser/extensionService'; import 'vs/workbench/services/contextmenu/electron-browser/contextmenuService'; -import 'vs/workbench/services/extensionManagement/node/multiExtensionManagement'; +import 'vs/workbench/services/extensions/node/multiExtensionManagement'; import 'vs/workbench/services/label/common/labelService'; import 'vs/workbench/services/extensions/electron-browser/extensionManagementServerService'; import 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; @@ -281,6 +281,9 @@ import 'vs/workbench/browser/parts/statusbar/statusbarPart'; //#region --- workbench contributions +// Workspace File Watching +import 'vs/workbench/services/files2/common/workspaceWatcher'; + // Telemetry import 'vs/workbench/contrib/telemetry/browser/telemetry.contribution'; @@ -338,6 +341,7 @@ import 'vs/workbench/contrib/comments/browser/comments.contribution'; import 'vs/workbench/contrib/url/common/url.contribution'; // Webview +import 'vs/workbench/contrib/webview/browser/webview.contribution'; import 'vs/workbench/contrib/webview/electron-browser/webview.contribution'; // Extensions Management diff --git a/src/vs/workbench/workbench.nodeless.main.ts b/src/vs/workbench/workbench.nodeless.main.ts index 62302a6fda..1dbd859f2a 100644 --- a/src/vs/workbench/workbench.nodeless.main.ts +++ b/src/vs/workbench/workbench.nodeless.main.ts @@ -106,7 +106,7 @@ import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/hash/common/hashService'; // import 'vs/workbench/services/textMate/electron-browser/textMateService'; import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; -// import 'vs/workbench/services/workspace/node/workspaceEditingService'; +// import 'vs/workbench/services/workspace/electron-browser/workspaceEditingService'; // import 'vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler'; import 'vs/workbench/services/decorations/browser/decorationsService'; // import 'vs/workbench/services/search/node/searchService'; @@ -192,6 +192,9 @@ import 'vs/workbench/browser/parts/statusbar/statusbarPart'; //#region --- workbench contributions +// Workspace File Watching +import 'vs/workbench/services/files2/common/workspaceWatcher'; + // Telemetry import 'vs/workbench/contrib/telemetry/browser/telemetry.contribution'; @@ -237,6 +240,7 @@ import 'vs/workbench/contrib/scm/browser/scmViewlet'; // import 'vs/workbench/contrib/debug/browser/debugEditorContribution'; // import 'vs/workbench/contrib/debug/browser/repl'; // import 'vs/workbench/contrib/debug/browser/debugViewlet'; +// import 'vs/workbench/services/extensions/electron-browser/extensionHostDebugService'; // Markers import 'vs/workbench/contrib/markers/browser/markers.contribution'; diff --git a/test/smoke/README.md b/test/smoke/README.md index 57c8ada658..d918fd357a 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -5,6 +5,11 @@ Make sure you are on **Node v10.x**. ### Run ```bash +# Compile +cd test/smoke +yarn compile +cd ../.. + # Dev yarn smoketest diff --git a/test/smoke/package.json b/test/smoke/package.json index bef1e5f59d..00b04f8453 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -22,7 +22,7 @@ "@types/webdriverio": "4.6.1", "concurrently": "^3.5.1", "cpx": "^1.5.0", - "electron": "3.1.6", + "electron": "3.1.8", "htmlparser2": "^3.9.2", "mkdirp": "^0.5.1", "mocha": "^5.2.0", diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock index d90dbd6007..5e1fd36aa4 100644 --- a/test/smoke/yarn.lock +++ b/test/smoke/yarn.lock @@ -596,10 +596,10 @@ electron-download@^4.1.0: semver "^5.4.1" sumchecker "^2.0.2" -electron@3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/electron/-/electron-3.1.6.tgz#f6222e1964838b31c5806dd041b1b58a941998f6" - integrity sha512-elEKKlFMnR0bhR/Uttk0TI496ZadxYsecyKgj2tZgrWx/F/anzfxbEYNcv134vT+qMFC/BXvoaeaIIj2YYdVuA== +electron@3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/electron/-/electron-3.1.8.tgz#01b0b147dfcca47967ff07dbf72bf5e96125a2ac" + integrity sha512-1MiFoMzxGaR0wDfwFE5Ydnuk6ry/4lKgF0c+NFyEItxM/WyEHNZPNjJAeKJ+M/0sevmZ+6W4syNZnQL5M3GgsQ== dependencies: "@types/node" "^8.0.24" electron-download "^4.1.0" diff --git a/yarn.lock b/yarn.lock index 7c68650288..9118479b29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9205,10 +9205,10 @@ typescript-tslint-plugin@^0.0.7: minimatch "^3.0.4" vscode-languageserver "^5.1.0" -typescript@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.1.tgz#6de14e1db4b8a006ac535e482c8ba018c55f750b" - integrity sha512-cTmIDFW7O0IHbn1DPYjkiebHxwtCMU+eTy30ZtJNBPF9j2O1ITu5XH2YnBeVRKWHqF+3JQwWJv0Q0aUgX8W7IA== +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@^2.6.2: version "2.6.2"