From 0d8ef9583b1c99180c55819bcd666f91d21cce4a Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Tue, 26 Mar 2019 11:43:38 -0700 Subject: [PATCH] Merge from vscode 966b87dd4013be1a9c06e2b8334522ec61905cc2 (#4696) --- .vscode/launch.json | 3 + build/gulpfile.vscode.js | 2 +- build/lib/i18n.js | 37 +- build/lib/i18n.ts | 49 +- .../lib/tslint/noNlsInStandaloneEditorRule.js | 54 ++ .../lib/tslint/noNlsInStandaloneEditorRule.ts | 67 ++ build/lib/tslint/noStandaloneEditorRule.js | 4 +- build/lib/tslint/noStandaloneEditorRule.ts | 4 +- extensions/git/package.json | 39 +- package.json | 4 +- resources/linux/snap/electron-launch | 35 +- resources/linux/snap/snapcraft.yaml | 49 +- scripts/generate-definitelytyped.sh | 31 + .../viewlet/serverTreeActionProvider.ts | 3 +- src/tsconfig.base.json | 1 + src/vs/base/browser/event.ts | 18 +- .../base/browser/ui/dialog/close-inverse.svg | 1 + src/vs/base/browser/ui/dialog/close.svg | 1 + src/vs/base/browser/ui/dialog/dialog.css | 154 +++ src/vs/base/browser/ui/dialog/dialog.ts | 187 ++++ .../base/browser/ui/dialog/error-inverse.svg | 26 + src/vs/base/browser/ui/dialog/error.svg | 25 + .../base/browser/ui/dialog/info-inverse.svg | 17 + src/vs/base/browser/ui/dialog/info.svg | 17 + .../browser/ui/dialog/warning-inverse.svg | 15 + src/vs/base/browser/ui/dialog/warning.svg | 15 + src/vs/base/browser/ui/list/listWidget.ts | 35 +- src/vs/base/browser/ui/tree/abstractTree.ts | 8 +- src/vs/base/common/arrays.ts | 12 + src/vs/base/common/async.ts | 16 + src/vs/base/common/buffer.ts | 109 +++ src/vs/base/common/labels.ts | 4 +- src/vs/base/node/extfs.ts | 8 +- src/vs/base/node/storage.ts | 2 +- src/vs/base/parts/ipc/common/ipc.net.ts | 774 +++++++++++++++ src/vs/base/parts/ipc/common/ipc.ts | 749 ++++++++++++++- .../electron-browser/ipc.electron-browser.ts | 5 +- .../ipc/electron-main/ipc.electron-main.ts | 12 +- src/vs/base/parts/ipc/node/ipc.cp.ts | 14 +- src/vs/base/parts/ipc/node/ipc.electron.ts | 9 +- src/vs/base/parts/ipc/node/ipc.net.ts | 883 ++---------------- src/vs/base/parts/ipc/node/ipc.ts | 733 --------------- .../base/parts/ipc/test/node/ipc.net.test.ts | 47 +- src/vs/base/parts/ipc/test/node/ipc.test.ts | 16 +- src/vs/base/test/common/async.test.ts | 26 + src/vs/base/test/common/filters.test.ts | 13 +- src/vs/base/test/node/extfs/extfs.test.ts | 65 +- .../issue/issueReporterMain.ts | 2 +- .../sharedProcess/sharedProcessMain.ts | 3 +- src/vs/code/electron-main/app.ts | 55 +- src/vs/code/electron-main/main.ts | 30 +- src/vs/code/electron-main/window.ts | 3 - src/vs/code/electron-main/windows.ts | 23 +- src/vs/code/node/cli.ts | 6 +- src/vs/editor/common/modes.ts | 4 +- .../modes/languageConfigurationRegistry.ts | 20 +- .../common/modes/tokenizationRegistry.ts | 30 +- src/vs/editor/common/standaloneStrings.ts | 84 ++ .../contrib/folding/indentRangeProvider.ts | 2 - src/vs/editor/contrib/format/format.ts | 54 ++ src/vs/editor/contrib/format/formatActions.ts | 36 +- .../editor/contrib/hover/modesContentHover.ts | 9 +- .../contrib/referenceSearch/peekViewWidget.ts | 33 +- .../referenceSearch/referencesController.ts | 1 + .../referenceSearch/referencesWidget.ts | 195 ++-- .../editor/contrib/rename/renameInputField.ts | 4 +- src/vs/editor/editor.all.ts | 4 + .../accessibilityHelp/accessibilityHelp.ts | 53 +- .../browser/inspectTokens/inspectTokens.ts | 4 +- .../standalone/browser/quickOpen/gotoLine.ts | 17 +- .../browser/quickOpen/quickCommand.ts | 11 +- .../browser/quickOpen/quickOutline.ts | 30 +- .../standalone/browser/simpleServices.ts | 5 +- .../browser/standaloneCodeEditor.ts | 8 +- .../toggleHighContrast/toggleHighContrast.ts | 4 +- .../common/modes/textToHtmlTokenizer.test.ts | 4 +- .../actions/browser/menuItemActionItem.ts | 4 +- src/vs/platform/actions/common/actions.ts | 2 +- .../configuration/common/configuration.ts | 6 + .../common/configurationModels.ts | 64 +- .../configuration/node/configuration.ts | 57 +- .../node/configurationService.ts | 12 +- .../test/common/testConfigurationService.ts | 2 + .../electron-main/diagnosticsService.ts | 35 +- .../platform/dialogs/browser/dialogService.ts | 92 ++ src/vs/platform/dialogs/common/dialogs.ts | 4 +- .../platform/driver/electron-main/driver.ts | 2 +- src/vs/platform/driver/node/driver.ts | 3 +- src/vs/platform/environment/node/argv.ts | 19 + .../platform/environment/node/argvHelper.ts | 21 - src/vs/platform/files/common/files.ts | 40 +- .../electron-browser/sharedProcessService.ts | 6 +- src/vs/platform/issue/common/issue.ts | 1 + .../issue/electron-browser/issueService.ts | 4 + .../issue/electron-main/issueService.ts | 6 + src/vs/platform/issue/node/issueIpc.ts | 2 + .../launch/electron-main/launchService.ts | 20 +- .../platform/quickinput/common/quickInput.ts | 2 + .../browser/remoteAuthorityResolverService.ts | 30 + .../remote/common/remoteAgentConnection.ts | 207 ++++ .../remoteAgentFileSystemChannel.ts | 12 +- .../remote/node/nodeWebSocketFactory.ts | 12 + .../remote/node/remoteAgentConnection.ts | 60 -- .../storage/node/storageMainService.ts | 2 +- src/vs/platform/theme/common/colorRegistry.ts | 2 +- src/vs/platform/theme/common/styler.ts | 23 +- .../electron-main/updateService.win32.ts | 2 +- src/vs/platform/windows/common/windows.ts | 6 +- .../windows/electron-browser/windowService.ts | 6 +- .../electron-browser/windowsService.ts | 8 +- .../platform/windows/electron-main/windows.ts | 1 + .../windows/electron-main/windowsService.ts | 19 +- src/vs/platform/windows/node/windowsIpc.ts | 11 +- src/vs/vscode.d.ts | 3 +- src/vs/vscode.proposed.d.ts | 30 +- .../mainThreadComments.ts | 6 +- .../api/browser/mainThreadDebugService.ts | 4 +- .../api/browser/mainThreadSaveParticipant.ts | 25 +- .../mainThreadTask.ts | 0 .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/menusExtensionPoint.ts | 21 +- .../extensionHost.contribution.ts | 4 +- .../api/electron-browser/mainThreadWebview.ts | 18 +- src/vs/workbench/api/node/apiCommands.ts | 31 +- src/vs/workbench/api/node/extHost.api.impl.ts | 8 +- src/vs/workbench/api/node/extHostCLIServer.ts | 48 +- .../api/node/extHostConfiguration.ts | 2 +- .../workbench/api/node/extHostDebugService.ts | 4 +- .../api/node/extHostLanguageFeatures.ts | 2 +- src/vs/workbench/api/node/extHostSearch.ts | 4 +- src/vs/workbench/browser/contextkeys.ts | 4 +- src/vs/workbench/browser/editor.ts | 2 +- .../browser/nodeless.simpleservices.ts | 27 +- .../parts/activitybar/activitybarPart.ts | 51 +- .../parts/editor/breadcrumbsControl.ts | 13 +- .../browser/parts/editor/breadcrumbsModel.ts | 2 +- .../browser/parts/editor/breadcrumbsPicker.ts | 29 +- .../browser/parts/panel/panelPart.ts | 13 +- .../browser/parts/quickinput/quickInput.ts | 4 + .../browser/parts/quickinput/quickInputBox.ts | 4 + src/vs/workbench/browser/workbench.ts | 10 +- src/vs/workbench/common/contextkeys.ts | 1 + src/vs/workbench/common/editor.ts | 4 +- src/vs/workbench/common/theme.ts | 5 + .../browser/callHierarchyPeek.ts | 4 +- .../browser/media/callHierarchy.css | 5 +- .../commentGlyphWidget.ts | 0 .../commentNode.ts | 4 +- .../commentService.ts | 6 +- .../commentThreadWidget.ts | 6 +- .../comments.contribution.ts | 4 +- .../commentsEditorContribution.ts | 14 +- .../commentsPanel.ts | 6 +- .../commentsTreeViewer.ts | 0 .../media/close.svg | 0 .../media/comment.svg | 0 .../media/panel.css | 0 .../media/reaction-dark.svg | 0 .../media/reaction-hc.svg | 0 .../media/reaction.svg | 0 .../media/review.css | 0 .../reactionsAction.ts | 0 .../simpleCommentEditor.ts | 0 .../contrib/debug/browser/baseDebugView.ts | 2 +- .../contrib/debug/browser/breakpointsView.ts | 2 +- .../contrib/debug/browser/callStackView.ts | 8 +- .../contrib/debug/browser/debugCommands.ts | 2 +- .../{debugToolbar.ts => debugToolBar.ts} | 12 +- .../contrib/debug/browser/debugViewlet.ts | 17 +- .../debug/browser/loadedScriptsView.ts | 4 +- .../{debugToolbar.css => debugToolBar.css} | 0 .../workbench/contrib/debug/browser/repl.ts | 12 +- .../contrib/debug/browser/variablesView.ts | 4 +- .../debug/browser/watchExpressionsView.ts | 4 +- .../workbench/contrib/debug/common/debug.ts | 7 +- .../debug/common/debugContentProvider.ts | 2 +- .../contrib/debug/common/debugModel.ts | 7 + .../contrib/debug/common/debugUtils.ts | 2 +- .../electron-browser/debug.contribution.ts | 30 +- .../debug/electron-browser/debugService.ts | 2 +- .../debug/electron-browser/rawDebugSession.ts | 2 +- .../workbench/contrib/debug/node/terminals.ts | 2 +- .../experimentalPrompts.test.ts | 2 +- .../electron-browser/extensionTipsService.ts | 33 +- .../electron-browser/extensionsViewlet.ts | 20 +- .../electron-browser/extensionsViews.ts | 50 +- .../node/extensionsWorkbenchService.ts | 4 +- .../externalTerminalService.ts | 16 +- .../files/browser/views/explorerView.ts | 9 +- .../files/browser/views/explorerViewer.ts | 16 +- .../format/browser/formatActionsMultiple.ts | 45 +- .../electron-browser/issue.contribution.ts | 6 + .../markers/browser/markersTreeViewer.ts | 2 +- .../contrib/markers/browser/media/markers.css | 2 +- .../contrib/outline/browser/outlinePanel.ts | 2 +- .../preferences/browser/preferencesActions.ts | 18 + .../preferences/browser/settingsTree.ts | 3 + .../preferences.contribution.ts | 16 +- .../contrib/scm/browser/scmViewlet.ts | 2 +- .../search/browser/media/searchview.css | 2 +- .../contrib/search/browser/openFileHandler.ts | 64 +- .../workbench/contrib/tasks/common/tasks.ts | 6 +- .../electron-browser/task.contribution.ts | 18 +- .../terminal/browser/terminalInstance.ts | 4 +- .../contrib/terminal/common/terminal.ts | 6 +- .../themes/browser/themes.contribution.ts | 70 +- .../electron-browser/releaseNotesEditor.ts | 2 +- .../webview/electron-browser/webviewEditor.ts | 8 +- .../electron-browser/webviewEditorInput.ts | 20 +- .../webviewEditorInputFactory.ts | 11 +- .../electron-browser/webviewEditorService.ts | 27 +- .../electron-browser/webviewElement.ts | 34 +- .../electron-browser/actions/windowActions.ts | 2 +- .../electron-browser/main.contribution.ts | 10 - src/vs/workbench/electron-browser/main.ts | 30 +- .../bulkEdit/browser/bulkEditService.ts | 9 +- .../common/configurationEditingService.ts | 27 +- .../common/configurationModels.ts | 26 +- .../configuration/node/configuration.ts | 140 ++- .../node/configurationService.ts | 71 +- .../test/common/configurationModels.test.ts | 2 +- .../configurationEditingService.test.ts | 7 +- .../configurationService.test.ts | 4 + .../dialogs/browser/remoteFileDialog.ts | 36 +- .../dialogs/electron-browser/dialogService.ts | 36 + .../electron-browser/extensionHost.ts | 10 +- .../extensionHostProcessManager.ts | 4 +- .../extensions/node/extensionHostMain.ts | 2 +- .../extensions/node/extensionHostProcess.ts | 14 +- .../extensions/node/extensionHostProtocol.ts | 18 +- .../services/extensions/node/proxyResolver.ts | 4 +- .../services/extensions/node/rpcProtocol.ts | 240 +++-- .../extensions/test/node/rpcProtocol.test.ts | 9 +- .../services/files/node/fileService.ts | 8 +- .../services/files/node/remoteFileService.ts | 75 +- .../files/node/watcher/nsfw/watcherService.ts | 2 +- .../files/node/watcher/unix/watcherService.ts | 2 +- .../services/files2/common/fileService2.ts | 458 ++++++--- .../diskFileSystemProvider.ts | 33 + .../files2/node/diskFileSystemProvider.ts | 170 +++- .../files2/node/diskFileSystemSupport.ts | 19 - .../files2/test/browser/fileService2.test.ts | 12 +- .../files2/test/node/diskFileService.test.ts | 542 ++++++----- .../preferences/browser/preferencesService.ts | 13 +- .../preferences/common/preferences.ts | 1 + .../common/abstractRemoteAgentService.ts | 113 +++ .../remoteAgentEnvironmentChannel.ts | 0 .../remote/common/remoteAgentService.ts | 2 +- .../remoteAgentServiceImpl.ts | 98 +- .../services/search/node/fileSearchManager.ts | 4 +- .../search/node/ripgrepTextSearchEngine.ts | 4 + .../services/search/node/searchService.ts | 9 +- .../themes/common/fileIconThemeStore.ts | 2 +- .../themes/common/workbenchThemeService.ts | 2 +- .../api/extHostConfiguration.test.ts | 6 +- .../api/extHostSearch.test.ts | 4 +- .../api/extHostTreeViews.test.ts | 10 +- .../colorRegistry.releaseTest.ts | 2 +- .../workbench/test/workbenchTestServices.ts | 18 +- src/vs/workbench/workbench.main.ts | 3 +- src/vs/workbench/workbench.nodeless.main.ts | 4 +- test/smoke/src/application.ts | 2 +- test/smoke/src/areas/debug/debug.test.ts | 8 +- test/smoke/src/areas/debug/debugSmoke.ts | 5 +- test/smoke/src/areas/search/search.test.ts | 2 +- test/smoke/src/vscode/code.ts | 1 + tslint.json | 5 +- yarn.lock | 23 +- 268 files changed, 5947 insertions(+), 3422 deletions(-) create mode 100644 build/lib/tslint/noNlsInStandaloneEditorRule.js create mode 100644 build/lib/tslint/noNlsInStandaloneEditorRule.ts create mode 100755 scripts/generate-definitelytyped.sh create mode 100644 src/vs/base/browser/ui/dialog/close-inverse.svg create mode 100644 src/vs/base/browser/ui/dialog/close.svg create mode 100644 src/vs/base/browser/ui/dialog/dialog.css create mode 100644 src/vs/base/browser/ui/dialog/dialog.ts create mode 100644 src/vs/base/browser/ui/dialog/error-inverse.svg create mode 100644 src/vs/base/browser/ui/dialog/error.svg create mode 100644 src/vs/base/browser/ui/dialog/info-inverse.svg create mode 100644 src/vs/base/browser/ui/dialog/info.svg create mode 100644 src/vs/base/browser/ui/dialog/warning-inverse.svg create mode 100644 src/vs/base/browser/ui/dialog/warning.svg create mode 100644 src/vs/base/common/buffer.ts create mode 100644 src/vs/base/parts/ipc/common/ipc.net.ts delete mode 100644 src/vs/base/parts/ipc/node/ipc.ts create mode 100644 src/vs/editor/common/standaloneStrings.ts create mode 100644 src/vs/platform/dialogs/browser/dialogService.ts create mode 100644 src/vs/platform/remote/browser/remoteAuthorityResolverService.ts create mode 100644 src/vs/platform/remote/common/remoteAgentConnection.ts rename src/vs/platform/remote/{node => common}/remoteAgentFileSystemChannel.ts (91%) create mode 100644 src/vs/platform/remote/node/nodeWebSocketFactory.ts delete mode 100644 src/vs/platform/remote/node/remoteAgentConnection.ts rename src/vs/workbench/api/{electron-browser => browser}/mainThreadComments.ts (99%) rename src/vs/workbench/api/{electron-browser => browser}/mainThreadTask.ts (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/commentGlyphWidget.ts (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/commentNode.ts (99%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/commentService.ts (98%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/commentThreadWidget.ts (99%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/comments.contribution.ts (91%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/commentsEditorContribution.ts (98%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/commentsPanel.ts (98%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/commentsTreeViewer.ts (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/media/close.svg (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/media/comment.svg (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/media/panel.css (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/media/reaction-dark.svg (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/media/reaction-hc.svg (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/media/reaction.svg (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/media/review.css (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/reactionsAction.ts (100%) rename src/vs/workbench/contrib/comments/{electron-browser => browser}/simpleCommentEditor.ts (100%) rename src/vs/workbench/contrib/debug/browser/{debugToolbar.ts => debugToolBar.ts} (97%) rename src/vs/workbench/contrib/debug/browser/media/{debugToolbar.css => debugToolBar.css} (100%) create mode 100644 src/vs/workbench/services/files2/electron-browser/diskFileSystemProvider.ts delete mode 100644 src/vs/workbench/services/files2/node/diskFileSystemSupport.ts create mode 100644 src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts rename src/vs/workbench/services/remote/{node => common}/remoteAgentEnvironmentChannel.ts (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 04058a1233..26e4b56fc9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -75,6 +75,9 @@ "linux": { "runtimeExecutable": "${workspaceFolder}/scripts/sql.sh" }, + "env": { + "VSCODE_EXTHOST_WILL_SEND_SOCKET": null + }, "breakOnLoad": false, "urlFilter": "*workbench.html*", "runtimeArgs": [ diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 5c1a35dc54..e7d4335c91 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -362,7 +362,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(util.cleanNodeModule('vscode-nsfw', ['binding.gyp', 'build/**', 'src/**', 'openpa/**', 'includes/**'], ['**/*.node', '**/*.a'])) // {{SQL CARBON EDIT}} - End .pipe(util.cleanNodeModule('vsda', ['binding.gyp', 'README.md', 'build/**', '*.bat', '*.sh', '*.cpp', '*.h'], ['build/Release/vsda.node'])) - .pipe(util.cleanNodeModule('win-ca-lib', ['**/*'], ['package.json', '**/*.node'])) + .pipe(util.cleanNodeModule('vscode-windows-ca-certs', ['**/*'], ['package.json', '**/*.node'])) .pipe(util.cleanNodeModule('node-addon-api', ['**/*'])) .pipe(createAsar(path.join(process.cwd(), 'node_modules'), ['**/*.node', '**/vscode-ripgrep/bin/*', '**/node-pty/build/Release/*'], 'app/node_modules.asar')); diff --git a/build/lib/i18n.js b/build/lib/i18n.js index f263e0bc92..50d2eeb082 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -21,8 +21,8 @@ function log(message, ...rest) { fancyLog(ansiColors.green('[i18n]'), message, ...rest); } exports.defaultLanguages = [ - { id: 'zh-tw', folderName: 'cht', transifexId: 'zh-hant' }, - { id: 'zh-cn', folderName: 'chs', transifexId: 'zh-hans' }, + { id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' }, + { id: 'zh-cn', folderName: 'chs', translationId: 'zh-hans' }, { id: 'ja', folderName: 'jpn' }, { id: 'ko', folderName: 'kor' }, { id: 'de', folderName: 'deu' }, @@ -372,7 +372,11 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) { } }); }); - let languageDirectory = path.join(__dirname, '..', '..', 'i18n'); + let languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n'); + if (!fs.existsSync(languageDirectory)) { + log(`No VS Code localization repository found. Looking at ${languageDirectory}`); + log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); + } let sortedLanguages = sortLanguages(languages); sortedLanguages.forEach((language) => { if (process.env['VSCODE_BUILD_VERBOSE']) { @@ -380,22 +384,25 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) { } statistics[language.id] = 0; let localizedModules = Object.create(null); - let languageFolderName = language.folderName || language.id; - let cwd = path.join(languageDirectory, languageFolderName, 'src'); + let languageFolderName = language.translationId || language.id; + let i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); + let allMessages; + if (fs.existsSync(i18nFile)) { + let content = stripComments(fs.readFileSync(i18nFile, 'utf8')); + allMessages = JSON.parse(content); + } modules.forEach((module) => { let order = keysSection[module]; - let i18nFile = path.join(cwd, module) + '.i18n.json'; - let messages = null; - if (fs.existsSync(i18nFile)) { - let content = stripComments(fs.readFileSync(i18nFile, 'utf8')); - messages = JSON.parse(content); + let moduleMessage; + if (allMessages) { + moduleMessage = allMessages.contents[module]; } - else { + if (!moduleMessage) { if (process.env['VSCODE_BUILD_VERBOSE']) { log(`No localized messages found for module ${module}. Using default messages.`); } - messages = defaultMessages[module]; - statistics[language.id] = statistics[language.id] + Object.keys(messages).length; + moduleMessage = defaultMessages[module]; + statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length; } let localizedMessages = []; order.forEach((keyInfo) => { @@ -406,7 +413,7 @@ function processCoreBundleFormat(fileHeader, languages, json, emitter) { else { key = keyInfo.key; } - let message = messages[key]; + let message = moduleMessage[key]; if (!message) { if (process.env['VSCODE_BUILD_VERBOSE']) { log(`No localized message found for key ${key} in module ${module}. Using default message.`); @@ -956,7 +963,7 @@ function retrieveResource(language, resource, apiHostname, credentials) { return limiter.queue(() => new Promise((resolve, reject) => { const slug = resource.name.replace(/\//g, '_'); const project = resource.project; - let transifexLanguageId = language.id === 'ps' ? 'en' : language.transifexId || language.id; + let transifexLanguageId = language.id === 'ps' ? 'en' : language.translationId || language.id; const options = { hostname: apiHostname, path: `/api/2/project/${project}/resource/${slug}/translation/${transifexLanguageId}?file&mode=onlyreviewed`, diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 05981eed84..8d3f22f5ac 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -25,7 +25,7 @@ function log(message: any, ...rest: any[]): void { export interface Language { id: string; // laguage id, e.g. zh-tw, de - transifexId?: string; // language id used in transifex, e.g zh-hant, de (optional, if not set, the id is used) + translationId?: string; // language id used in translation tools, e.g zh-hant, de (optional, if not set, the id is used) folderName?: string; // language specific folder name, e.g. cht, deu (optional, if not set, the id is used) } @@ -38,8 +38,8 @@ export interface InnoSetup { } export const defaultLanguages: Language[] = [ - { id: 'zh-tw', folderName: 'cht', transifexId: 'zh-hant' }, - { id: 'zh-cn', folderName: 'chs', transifexId: 'zh-hans' }, + { id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' }, + { id: 'zh-cn', folderName: 'chs', translationId: 'zh-hans' }, { id: 'ja', folderName: 'jpn' }, { id: 'ko', folderName: 'kor' }, { id: 'de', folderName: 'deu' }, @@ -144,6 +144,15 @@ interface BundledExtensionFormat { }; } +interface I18nFormat { + version: string; + contents: { + [module: string]: { + [messageKey: string]: string; + }; + }; +} + export class Line { private buffer: string[] = []; @@ -486,7 +495,11 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json }); }); - let languageDirectory = path.join(__dirname, '..', '..', 'i18n'); + let languageDirectory = path.join(__dirname, '..', '..', '..', 'vscode-loc', 'i18n'); + if (!fs.existsSync(languageDirectory)) { + log(`No VS Code localization repository found. Looking at ${languageDirectory}`); + log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); + } let sortedLanguages = sortLanguages(languages); sortedLanguages.forEach((language) => { if (process.env['VSCODE_BUILD_VERBOSE']) { @@ -495,21 +508,25 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json statistics[language.id] = 0; let localizedModules: Map = Object.create(null); - let languageFolderName = language.folderName || language.id; - let cwd = path.join(languageDirectory, languageFolderName, 'src'); + let languageFolderName = language.translationId || language.id; + let i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); + let allMessages: I18nFormat | undefined; + if (fs.existsSync(i18nFile)) { + let content = stripComments(fs.readFileSync(i18nFile, 'utf8')); + allMessages = JSON.parse(content); + } modules.forEach((module) => { let order = keysSection[module]; - let i18nFile = path.join(cwd, module) + '.i18n.json'; - let messages: Map | null = null; - if (fs.existsSync(i18nFile)) { - let content = stripComments(fs.readFileSync(i18nFile, 'utf8')); - messages = JSON.parse(content); - } else { + let moduleMessage: { [messageKey: string]: string } | undefined; + if (allMessages) { + moduleMessage = allMessages.contents[module]; + } + if (!moduleMessage) { if (process.env['VSCODE_BUILD_VERBOSE']) { log(`No localized messages found for module ${module}. Using default messages.`); } - messages = defaultMessages[module]; - statistics[language.id] = statistics[language.id] + Object.keys(messages).length; + moduleMessage = defaultMessages[module]; + statistics[language.id] = statistics[language.id] + Object.keys(moduleMessage).length; } let localizedMessages: string[] = []; order.forEach((keyInfo) => { @@ -519,7 +536,7 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json } else { key = keyInfo.key; } - let message: string = messages![key]; + let message: string = moduleMessage![key]; if (!message) { if (process.env['VSCODE_BUILD_VERBOSE']) { log(`No localized message found for key ${key} in module ${module}. Using default message.`); @@ -1093,7 +1110,7 @@ function retrieveResource(language: Language, resource: Resource, apiHostname: s return limiter.queue(() => new Promise((resolve, reject) => { const slug = resource.name.replace(/\//g, '_'); const project = resource.project; - let transifexLanguageId = language.id === 'ps' ? 'en' : language.transifexId || language.id; + let transifexLanguageId = language.id === 'ps' ? 'en' : language.translationId || language.id; const options = { hostname: apiHostname, path: `/api/2/project/${project}/resource/${slug}/translation/${transifexLanguageId}?file&mode=onlyreviewed`, diff --git a/build/lib/tslint/noNlsInStandaloneEditorRule.js b/build/lib/tslint/noNlsInStandaloneEditorRule.js new file mode 100644 index 0000000000..affd4cc837 --- /dev/null +++ b/build/lib/tslint/noNlsInStandaloneEditorRule.js @@ -0,0 +1,54 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const ts = require("typescript"); +const Lint = require("tslint"); +const path_1 = require("path"); +class Rule extends Lint.Rules.AbstractRule { + apply(sourceFile) { + if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName) + || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName) + || /vs(\/|\\)editor(\/|\\)editor.api/.test(sourceFile.fileName) + || /vs(\/|\\)editor(\/|\\)editor.main/.test(sourceFile.fileName) + || /vs(\/|\\)editor(\/|\\)editor.worker/.test(sourceFile.fileName)) { + return this.applyWithWalker(new NoNlsInStandaloneEditorRuleWalker(sourceFile, this.getOptions())); + } + return []; + } +} +exports.Rule = Rule; +class NoNlsInStandaloneEditorRuleWalker extends Lint.RuleWalker { + constructor(file, opts) { + super(file, opts); + } + visitImportEqualsDeclaration(node) { + if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { + this._validateImport(node.moduleReference.expression.getText(), node); + } + } + visitImportDeclaration(node) { + this._validateImport(node.moduleSpecifier.getText(), node); + } + visitCallExpression(node) { + super.visitCallExpression(node); + // import('foo') statements inside the code + if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { + const [path] = node.arguments; + this._validateImport(path.getText(), node); + } + } + _validateImport(path, node) { + // remove quotes + path = path.slice(1, -1); + // resolve relative paths + if (path[0] === '.') { + path = path_1.join(this.getSourceFile().fileName, path); + } + if (/vs(\/|\\)nls/.test(path)) { + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts`)); + } + } +} diff --git a/build/lib/tslint/noNlsInStandaloneEditorRule.ts b/build/lib/tslint/noNlsInStandaloneEditorRule.ts new file mode 100644 index 0000000000..ae23d74d78 --- /dev/null +++ b/build/lib/tslint/noNlsInStandaloneEditorRule.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; +import * as Lint from 'tslint'; +import { join } from 'path'; + +export class Rule extends Lint.Rules.AbstractRule { + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + if ( + /vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName) + || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName) + || /vs(\/|\\)editor(\/|\\)editor.api/.test(sourceFile.fileName) + || /vs(\/|\\)editor(\/|\\)editor.main/.test(sourceFile.fileName) + || /vs(\/|\\)editor(\/|\\)editor.worker/.test(sourceFile.fileName) + ) { + return this.applyWithWalker(new NoNlsInStandaloneEditorRuleWalker(sourceFile, this.getOptions())); + } + + return []; + } +} + +class NoNlsInStandaloneEditorRuleWalker extends Lint.RuleWalker { + + constructor(file: ts.SourceFile, opts: Lint.IOptions) { + super(file, opts); + } + + protected visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration): void { + if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { + this._validateImport(node.moduleReference.expression.getText(), node); + } + } + + protected visitImportDeclaration(node: ts.ImportDeclaration): void { + this._validateImport(node.moduleSpecifier.getText(), node); + } + + protected visitCallExpression(node: ts.CallExpression): void { + super.visitCallExpression(node); + + // import('foo') statements inside the code + if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { + const [path] = node.arguments; + this._validateImport(path.getText(), node); + } + } + + private _validateImport(path: string, node: ts.Node): void { + // remove quotes + path = path.slice(1, -1); + + // resolve relative paths + if (path[0] === '.') { + path = join(this.getSourceFile().fileName, path); + } + + if ( + /vs(\/|\\)nls/.test(path) + ) { + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts`)); + } + } +} diff --git a/build/lib/tslint/noStandaloneEditorRule.js b/build/lib/tslint/noStandaloneEditorRule.js index cb2f039ac5..c1f2ed51e6 100644 --- a/build/lib/tslint/noStandaloneEditorRule.js +++ b/build/lib/tslint/noStandaloneEditorRule.js @@ -45,8 +45,8 @@ class NoStandaloneEditorRuleWalker extends Lint.RuleWalker { if (path[0] === '.') { path = path_1.join(this.getSourceFile().fileName, path); } - if (/vs(\/|\\)editor(\/|\\)standalone/.test(path) - || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone/.test(path) + if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path) + || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path) || /vs(\/|\\)editor(\/|\\)editor.api/.test(path) || /vs(\/|\\)editor(\/|\\)editor.main/.test(path) || /vs(\/|\\)editor(\/|\\)editor.worker/.test(path)) { diff --git a/build/lib/tslint/noStandaloneEditorRule.ts b/build/lib/tslint/noStandaloneEditorRule.ts index 1319a8f622..a29e9b18ab 100644 --- a/build/lib/tslint/noStandaloneEditorRule.ts +++ b/build/lib/tslint/noStandaloneEditorRule.ts @@ -54,8 +54,8 @@ class NoStandaloneEditorRuleWalker extends Lint.RuleWalker { } if ( - /vs(\/|\\)editor(\/|\\)standalone/.test(path) - || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone/.test(path) + /vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path) + || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path) || /vs(\/|\\)editor(\/|\\)editor.api/.test(path) || /vs(\/|\\)editor(\/|\\)editor.main/.test(path) || /vs(\/|\\)editor(\/|\\)editor.worker/.test(path) diff --git a/extensions/git/package.json b/extensions/git/package.json index 30ec14b227..058fd6691a 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -703,89 +703,94 @@ "group": "1_sync", "when": "scmProvider == git && config.git.allowForcePush" }, + { + "command": "git.checkout", + "group": "2_branch", + "when": "scmProvider == git" + }, { "command": "git.publish", - "group": "2_publish", + "group": "2_branch", "when": "scmProvider == git" }, { "command": "git.commitStaged", - "group": "3_commit", + "group": "4_commit", "when": "scmProvider == git" }, { "command": "git.commitStagedSigned", - "group": "3_commit", + "group": "4_commit", "when": "scmProvider == git" }, { "command": "git.commitStagedAmend", - "group": "3_commit", + "group": "4_commit", "when": "scmProvider == git" }, { "command": "git.commitAll", - "group": "3_commit", + "group": "4_commit", "when": "scmProvider == git" }, { "command": "git.commitAllSigned", - "group": "3_commit", + "group": "4_commit", "when": "scmProvider == git" }, { "command": "git.commitAllAmend", - "group": "3_commit", + "group": "4_commit", "when": "scmProvider == git" }, { "command": "git.undoCommit", - "group": "3_commit", + "group": "4_commit", "when": "scmProvider == git" }, { "command": "git.stageAll", - "group": "4_stage", + "group": "5_stage", "when": "scmProvider == git" }, { "command": "git.unstageAll", - "group": "4_stage", + "group": "5_stage", "when": "scmProvider == git" }, { "command": "git.cleanAll", - "group": "4_stage", + "group": "5_stage", "when": "scmProvider == git && !gitFreshRepository" }, { "command": "git.stashIncludeUntracked", - "group": "5_stash", + "group": "6_stash", "when": "scmProvider == git" }, { "command": "git.stash", - "group": "5_stash", + "group": "6_stash", "when": "scmProvider == git" }, { "command": "git.stashPop", - "group": "5_stash", + "group": "6_stash", "when": "scmProvider == git" }, { "command": "git.stashPopLatest", - "group": "5_stash", + "group": "6_stash", "when": "scmProvider == git" }, { "command": "git.stashApply", - "group": "5_stash", + "group": "6_stash", "when": "scmProvider == git" }, { "command": "git.stashApplyLatest", - "group": "5_stash", + "group": "6_stash", "when": "scmProvider == git" }, { diff --git a/package.json b/package.json index c5b234b578..4727c59096 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "vscode-ripgrep": "^1.2.5", "vscode-sqlite3": "4.0.7", "vscode-textmate": "^4.0.1", - "vscode-xterm": "3.13.0-beta1", + "vscode-xterm": "3.13.0-beta2", "yauzl": "^2.9.1", "yazl": "^2.4.3", "zone.js": "^0.8.4" @@ -177,7 +177,7 @@ }, "optionalDependencies": { "vscode-windows-registry": "1.0.1", - "win-ca-lib": "https://github.com/chrmarti/win-ca/releases/download/v2.4.1-lib-test/win-ca-lib-2.4.1.tgz", + "vscode-windows-ca-certs": "0.1.0", "windows-foreground-love": "0.1.0", "windows-mutex": "0.2.1", "windows-process-tree": "0.2.3" diff --git a/resources/linux/snap/electron-launch b/resources/linux/snap/electron-launch index 65c91d6fc1..2a1c439518 100644 --- a/resources/linux/snap/electron-launch +++ b/resources/linux/snap/electron-launch @@ -1,3 +1,34 @@ -#!/bin/sh +#!/usr/bin/env bash -exec "$@" --executed-from="$(pwd)" --pid=$$ +# On Fedora $SNAP is under /var and there is some magic to map it to /snap. +# We need to handle that case and reset $SNAP +SNAP=$(echo $SNAP | sed -e "s|/var/lib/snapd||g") + +if [ "$SNAP_ARCH" == "amd64" ]; then + ARCH="x86_64-linux-gnu" +elif [ "$SNAP_ARCH" == "armhf" ]; then + ARCH="arm-linux-gnueabihf" +elif [ "$SNAP_ARCH" == "arm64" ]; then + ARCH="aarch64-linux-gnu" +else + ARCH="$SNAP_ARCH-linux-gnu" +fi + +export XDG_CACHE_HOME=$SNAP_USER_COMMON/.cache +if [[ -d $SNAP_USER_DATA/.cache && ! -e $XDG_CACHE_HOME ]]; then + # the .cache directory used to be stored under $SNAP_USER_DATA, migrate it + mv $SNAP_USER_DATA/.cache $SNAP_USER_COMMON/ +fi +mkdir -p $XDG_CACHE_HOME + +# Gdk-pixbuf loaders +export GDK_PIXBUF_MODULE_FILE=$XDG_CACHE_HOME/gdk-pixbuf-loaders.cache +export GDK_PIXBUF_MODULEDIR=$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/2.10.0/loaders +if [ -f $SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders ]; then + $SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders > $GDK_PIXBUF_MODULE_FILE +fi + +# Create $XDG_RUNTIME_DIR if not exists (to be removed when https://pad.lv/1656340 is fixed) +[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p $XDG_RUNTIME_DIR -m 700 + +exec "$@" diff --git a/resources/linux/snap/snapcraft.yaml b/resources/linux/snap/snapcraft.yaml index 6c1c9b8eac..f11f3b7591 100644 --- a/resources/linux/snap/snapcraft.yaml +++ b/resources/linux/snap/snapcraft.yaml @@ -10,33 +10,54 @@ grade: stable confinement: classic parts: + gnome: + plugin: nil + build-packages: + - software-properties-common + override-pull: | + add-apt-repository -y ppa:ubuntu-desktop/gnome-3-26 + apt -y update + code: + after: + - gnome plugin: dump source: . stage-packages: + - fcitx-frontend-gtk3 + - gvfs-libs - libasound2 - - libc++1 + - libgconf-2-4 + - libglib2.0-bin + - libgnome-keyring0 + - libgtk-3-0 - libnotify4 - libnspr4 - libnss3 - libpcre3 - libpulse0 + - libsecret-1-0 - libxss1 - libxtst6 - # desktop-gtk2 deps below - - libxkbcommon0 - - libgtk2.0-0 - # - unity-gtk2-module - - libappindicator1 + - zlib1g prime: - - -usr/share/dh-python - electron-launch: - plugin: dump - source: . - organize: - electron-launch: bin/electron-launch + - -usr/share/doc + - -usr/share/fonts + - -usr/share/icons + - -usr/share/lintian + - -usr/share/man apps: @@NAME@@: - command: bin/electron-launch ${SNAP}/usr/share/@@NAME@@/bin/@@NAME@@ - desktop: usr/share/applications/@@NAME@@.desktop \ No newline at end of file + command: electron-launch $SNAP/usr/share/@@NAME@@/bin/@@NAME@@ + desktop: usr/share/applications/@@NAME@@.desktop + environment: + DISABLE_WAYLAND: 1 + GSETTINGS_SCHEMA_DIR: $SNAP/usr/share/glib-2.0/schemas + + url-handler: + command: electron-launch $SNAP/usr/share/@@NAME@@/bin/@@NAME@@ --open-url + desktop: usr/share/applications/@@NAME@@-url-handler.desktop + environment: + DISABLE_WAYLAND: 1 + GSETTINGS_SCHEMA_DIR: $SNAP/usr/share/glib-2.0/schemas diff --git a/scripts/generate-definitelytyped.sh b/scripts/generate-definitelytyped.sh new file mode 100755 index 0000000000..82de9124a0 --- /dev/null +++ b/scripts/generate-definitelytyped.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +if [ $# -eq 0 ]; then + echo "Pass in a version like ./scripts/generate-vscode-dts.sh 1.30." + echo "Failed to generate index.d.ts." + exit 1 +fi + +header="// Type definitions for Visual Studio Code ${1} +// Project: https://github.com/microsoft/vscode +// Definitions by: Visual Studio Code Team, Microsoft +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Type Definition for Visual Studio Code ${1} Extension API + * See https://code.visualstudio.com/api for more information + */" + +if [ -f ./src/vs/vscode.d.ts ]; then + echo "$header" > index.d.ts + sed "1,4d" ./src/vs/vscode.d.ts >> index.d.ts + echo "Generated index.d.ts for version ${1}." +else + echo "Can't find ./src/vs/vscode.d.ts. Run this script at vscode root." +fi \ No newline at end of file diff --git a/src/sql/parts/objectExplorer/viewlet/serverTreeActionProvider.ts b/src/sql/parts/objectExplorer/viewlet/serverTreeActionProvider.ts index 6839ad2588..dfe27d95a1 100644 --- a/src/sql/parts/objectExplorer/viewlet/serverTreeActionProvider.ts +++ b/src/sql/parts/objectExplorer/viewlet/serverTreeActionProvider.ts @@ -31,7 +31,6 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { TreeNodeContextKey } from 'sql/parts/objectExplorer/viewlet/treeNodeContextKey'; import { IQueryManagementService } from 'sql/platform/query/common/queryManagement'; import { IScriptingService } from 'sql/platform/scripting/common/scriptingService'; -import * as constants from 'sql/common/constants'; import { ServerInfoContextKey } from 'sql/parts/connection/common/serverInfoContextKey'; /** @@ -101,7 +100,7 @@ export class ServerTreeActionProvider extends ContributableActionProvider { let actions = getDefaultActions(context); let options = { arg: undefined, shouldForwardArgs: true }; const groups = menu.getActions(options); - fillInActions(groups, actions, this.contextMenuService); + fillInActions(groups, actions, false); // Cleanup scopedContextService.dispose(); diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 6ae2897421..180035e142 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -11,6 +11,7 @@ "alwaysStrict": true, "strictBindCallApply": true, "strictNullChecks": false, + "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { "vs/*": [ diff --git a/src/vs/base/browser/event.ts b/src/vs/base/browser/event.ts index 021a0be5b2..5235aed400 100644 --- a/src/vs/base/browser/event.ts +++ b/src/vs/base/browser/event.ts @@ -3,18 +3,18 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; +import { Event as BaseEvent, Emitter } from 'vs/base/common/event'; export type EventHandler = HTMLElement | HTMLDocument | Window; export interface IDomEvent { - (element: EventHandler, type: K, useCapture?: boolean): Event; - (element: EventHandler, type: string, useCapture?: boolean): Event; + (element: EventHandler, type: K, useCapture?: boolean): BaseEvent; + (element: EventHandler, type: string, useCapture?: boolean): BaseEvent; } export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => { - const fn = e => emitter.fire(e); - const emitter = new Emitter({ + const fn = (e: Event) => emitter.fire(e); + const emitter = new Emitter({ onFirstListenerAdd: () => { element.addEventListener(type, fn, useCapture); }, @@ -27,12 +27,12 @@ export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapt }; export interface CancellableEvent { - preventDefault(); - stopPropagation(); + preventDefault(): void; + stopPropagation(): void; } -export function stop(event: Event): Event { - return Event.map(event, e => { +export function stop(event: BaseEvent): BaseEvent { + return BaseEvent.map(event, e => { e.preventDefault(); e.stopPropagation(); return e; diff --git a/src/vs/base/browser/ui/dialog/close-inverse.svg b/src/vs/base/browser/ui/dialog/close-inverse.svg new file mode 100644 index 0000000000..a174033d91 --- /dev/null +++ b/src/vs/base/browser/ui/dialog/close-inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/base/browser/ui/dialog/close.svg b/src/vs/base/browser/ui/dialog/close.svg new file mode 100644 index 0000000000..f4038b8bfa --- /dev/null +++ b/src/vs/base/browser/ui/dialog/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css new file mode 100644 index 0000000000..791be617fd --- /dev/null +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/** Dialog: Modal Block */ +.monaco-workbench .dialog-modal-block { + position: fixed; + height: 100%; + width: 100%; + left:0; + top:0; + z-index: 2000; +} + +/** Dialog: Container */ +.monaco-workbench .dialog-box { + position: absolute; + display: flex; + flex-direction: column-reverse; + top: 200px; + left: 50%; + margin-left: -250px; + width: 500px; + min-height: 75px; + padding: 5px; +} + +/** Dialog: Title Actions Row */ +.monaco-workbench .dialog-box .dialog-toolbar-row { + padding-right: 1px; +} + +.monaco-workbench .dialog-box .action-label { + height: 16px; + min-width: 16px; + background-size: 16px; + background-position: 50%; + background-repeat: no-repeat; + margin: 0px; + margin-left: 4px; +} + + +.monaco-workbench .dialog-box .dialog-close-action { + background: url('close.svg') center center no-repeat; +} + +.vs-dark .monaco-workbench .dialog-box .dialog-close-action, +.hc-black .monaco-workbench .dialog-box .dialog-close-action { + background: url('close-inverse.svg') center center no-repeat; +} + +/** Dialog: Message Row */ +.monaco-workbench .dialog-box .dialog-message-row { + display: flex; + flex-grow: 1; + padding: 10px 15px 20px; +} + +.monaco-workbench .dialog-box .dialog-message-row .dialog-icon { + flex: 0 0 30px; + padding-right: 4px; + padding-left: 4px; + background-position: center; + background-repeat: no-repeat; +} + +.vs .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info { + background-image: url('info.svg'); +} + +.vs .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-warning { + background-image: url('warning.svg'); +} + +.vs .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-error { + background-image: url('error.svg'); +} + +.vs-dark .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info, +.hc-black .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-info { + background-image: url('info-inverse.svg'); +} + +.vs-dark .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-warning, +.hc-black .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-warning { + background-image: url('warning-inverse.svg'); +} + +.vs-dark .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-error, +.hc-black .monaco-workbench .dialog-box .dialog-message-row .dialog-icon.icon-error { + background-image: url('error-inverse.svg'); +} + +/** Dialog: Message Container */ +.monaco-workbench .dialog-box .dialog-message-row .dialog-message-container { + display: flex; + flex-direction: column; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 20px; + user-select: text; + word-wrap: break-word; /* never overflow long words, but break to next line */ + white-space: normal; +} + +/** Dialog: Message */ +.monaco-workbench .dialog-box .dialog-message-row .dialog-message-container .dialog-message { + line-height: 22px; + font-size: 18px; + flex: 1; /* let the message always grow */ + white-space: normal; + word-wrap: break-word; /* never overflow long words, but break to next line */ + padding-bottom: 10px; +} + +/** Dialog: Details */ +.monaco-workbench .dialog-box .dialog-message-row .dialog-message-container .dialog-message-detail { + line-height: 22px; + flex: 1; /* let the message always grow */ + opacity: .9; +} + +.monaco-workbench .dialog-box .dialog-message-row .dialog-message-container .dialog-message a:focus { + outline-width: 1px; + outline-style: solid; +} + +/** Dialog: Buttons Row */ +.monaco-workbench .dialog-box > .dialog-buttons-row { + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 1px; + overflow: hidden; /* buttons row should never overflow */ +} + +.monaco-workbench .monaco-workbench .dialog-box > .dialog-buttons-row { + display: flex; +} + +/** Dialog: Buttons */ +.monaco-workbench .monaco-workbench .dialog-box > .dialog-buttons-row > .dialog-buttons { + display: flex; + overflow: hidden; +} + +.monaco-workbench .dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button { + max-width: fit-content; + padding: 5px 10px; + margin: 4px 5px; /* allows button focus outline to be visible */ + overflow: hidden; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts new file mode 100644 index 0000000000..61311f4aed --- /dev/null +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./dialog'; +import * as nls from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { $, hide, show, EventHelper, clearNode, removeClasses, addClass, removeNode } from 'vs/base/browser/dom'; +import { domEvent } from 'vs/base/browser/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Color } from 'vs/base/common/color'; +import { ButtonGroup, IButtonStyles } from 'vs/base/browser/ui/button/button'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action } from 'vs/base/common/actions'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; + +export interface IDialogOptions { + cancelId?: number; + detail?: string; + type?: 'none' | 'info' | 'error' | 'question' | 'warning'; +} + +export interface IDialogStyles extends IButtonStyles { + dialogForeground?: Color; + dialogBackground?: Color; + dialogShadow?: Color; +} + +export class Dialog extends Disposable { + private element: HTMLElement | undefined; + private modal: HTMLElement | undefined; + private buttonsContainer: HTMLElement | undefined; + private iconElement: HTMLElement | undefined; + private toolbarContainer: HTMLElement | undefined; + private buttonGroup: ButtonGroup | undefined; + private styles: IDialogStyles | undefined; + + constructor(private container: HTMLElement, private message: string, private buttons: string[], private options: IDialogOptions) { + super(); + this.modal = this.container.appendChild($('.dialog-modal-block')); + this.element = this.modal.appendChild($('.dialog-box')); + hide(this.element); + + const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row')); + this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons')); + + const messageRowElement = this.element.appendChild($('.dialog-message-row')); + this.iconElement = messageRowElement.appendChild($('.dialog-icon')); + const messageContainer = messageRowElement.appendChild($('.dialog-message-container')); + const messageElement = messageContainer.appendChild($('.dialog-message')); + messageElement.innerText = this.message; + if (this.options.detail) { + const messageDetailElement = messageContainer.appendChild($('.dialog-message-detail')); + messageDetailElement.innerText = this.options.detail; + } + + const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row')); + this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar')); + } + + async show(): Promise { + return new Promise((resolve) => { + if (!this.element || !this.buttonsContainer || !this.iconElement || !this.toolbarContainer) { + resolve(0); + return; + } + + if (this.modal) { + this._register(domEvent(this.modal, 'mousedown')(e => { + // Used to stop focusing of modal with mouse + EventHelper.stop(e, true); + })); + } + + clearNode(this.buttonsContainer); + + let focusedButton = 0; + this.buttonGroup = new ButtonGroup(this.buttonsContainer, this.buttons.length, { title: true }); + this.buttonGroup.buttons.forEach((button, index) => { + button.label = mnemonicButtonLabel(this.buttons[index], true); + + this._register(button.onDidClick(e => { + EventHelper.stop(e); + resolve(index); + })); + }); + + this._register(domEvent(this.element, 'keydown', true)((e: KeyboardEvent) => { + const evt = new StandardKeyboardEvent(e); + if (evt.equals(KeyCode.Enter)) { + return; + } + + if (this.buttonGroup) { + if ((evt.shiftKey && evt.equals(KeyCode.Tab)) || evt.equals(KeyCode.LeftArrow)) { + focusedButton = focusedButton + this.buttonGroup.buttons.length - 1; + focusedButton = focusedButton % this.buttonGroup.buttons.length; + this.buttonGroup.buttons[focusedButton].focus(); + } else if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) { + focusedButton++; + focusedButton = focusedButton % this.buttonGroup.buttons.length; + this.buttonGroup.buttons[focusedButton].focus(); + } + } + + EventHelper.stop(e, true); + })); + + this._register(domEvent(this.element, 'keyup', true)((e: KeyboardEvent) => { + EventHelper.stop(e, true); + const evt = new StandardKeyboardEvent(e); + + if (evt.equals(KeyCode.Escape)) { + resolve(this.options.cancelId || 0); + } + })); + + removeClasses(this.iconElement, 'icon-error', 'icon-warning', 'icon-info'); + + switch (this.options.type) { + case 'error': + addClass(this.iconElement, 'icon-error'); + break; + case 'warning': + addClass(this.iconElement, 'icon-warning'); + break; + case 'none': + case 'info': + case 'question': + default: + addClass(this.iconElement, 'icon-info'); + break; + } + + const actionBar = new ActionBar(this.toolbarContainer, {}); + + const action = new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), 'dialog-close-action', true, () => { + resolve(this.options.cancelId || 0); + return Promise.resolve(); + }); + + actionBar.push(action, { icon: true, label: false, }); + + this.applyStyles(); + + show(this.element); + + // Focus first element + this.buttonGroup.buttons[focusedButton].focus(); + }); + } + + private applyStyles() { + if (this.styles) { + const style = this.styles; + + const fgColor = style.dialogForeground ? `${style.dialogForeground}` : null; + const bgColor = style.dialogBackground ? `${style.dialogBackground}` : null; + const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : null; + + if (this.element) { + this.element.style.color = fgColor; + this.element.style.backgroundColor = bgColor; + this.element.style.boxShadow = shadowColor; + + if (this.buttonGroup) { + this.buttonGroup.buttons.forEach(button => button.style(style)); + } + } + } + } + + style(style: IDialogStyles): void { + this.styles = style; + this.applyStyles(); + } + + dispose(): void { + super.dispose(); + if (this.modal) { + removeNode(this.modal); + this.modal = undefined; + } + } +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/dialog/error-inverse.svg b/src/vs/base/browser/ui/dialog/error-inverse.svg new file mode 100644 index 0000000000..51e9dc81b9 --- /dev/null +++ b/src/vs/base/browser/ui/dialog/error-inverse.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/src/vs/base/browser/ui/dialog/error.svg b/src/vs/base/browser/ui/dialog/error.svg new file mode 100644 index 0000000000..04b6668901 --- /dev/null +++ b/src/vs/base/browser/ui/dialog/error.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/src/vs/base/browser/ui/dialog/info-inverse.svg b/src/vs/base/browser/ui/dialog/info-inverse.svg new file mode 100644 index 0000000000..64b801a63b --- /dev/null +++ b/src/vs/base/browser/ui/dialog/info-inverse.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/src/vs/base/browser/ui/dialog/info.svg b/src/vs/base/browser/ui/dialog/info.svg new file mode 100644 index 0000000000..3c603528a7 --- /dev/null +++ b/src/vs/base/browser/ui/dialog/info.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/src/vs/base/browser/ui/dialog/warning-inverse.svg b/src/vs/base/browser/ui/dialog/warning-inverse.svg new file mode 100644 index 0000000000..a7f4afbcc9 --- /dev/null +++ b/src/vs/base/browser/ui/dialog/warning-inverse.svg @@ -0,0 +1,15 @@ + + + + +StatusWarning_16x + + + + + diff --git a/src/vs/base/browser/ui/dialog/warning.svg b/src/vs/base/browser/ui/dialog/warning.svg new file mode 100644 index 0000000000..6d8cffe913 --- /dev/null +++ b/src/vs/base/browser/ui/dialog/warning.svg @@ -0,0 +1,15 @@ + + + + +StatusWarning_16x + + + + + diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 94307e3cfc..b8fcceb5c5 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -7,7 +7,7 @@ import 'vs/css!./list'; import { localize } from 'vs/nls'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { isNumber } from 'vs/base/common/types'; -import { range, firstIndex } from 'vs/base/common/arrays'; +import { range, firstIndex, binarySearch } from 'vs/base/common/arrays'; import { memoize } from 'vs/base/common/decorators'; import * as DOM from 'vs/base/browser/dom'; import * as platform from 'vs/base/common/platform'; @@ -107,10 +107,8 @@ class TraitRenderer implements IListRenderer class Trait implements ISpliceable, IDisposable { - /** - * Sorted indexes which have this trait. - */ - private indexes: number[]; + private indexes: number[] = []; + private sortedIndexes: number[] = []; private _onChange = new Emitter(); get onChange(): Event { return this._onChange.event; } @@ -122,21 +120,19 @@ class Trait implements ISpliceable, IDisposable { return new TraitRenderer(this); } - constructor(private _trait: string) { - this.indexes = []; - } + constructor(private _trait: string) { } splice(start: number, deleteCount: number, elements: boolean[]): void { const diff = elements.length - deleteCount; const end = start + deleteCount; const indexes = [ - ...this.indexes.filter(i => i < start), + ...this.sortedIndexes.filter(i => i < start), ...elements.map((hasTrait, i) => hasTrait ? i + start : -1).filter(i => i !== -1), - ...this.indexes.filter(i => i >= end).map(i => i + diff) + ...this.sortedIndexes.filter(i => i >= end).map(i => i + diff) ]; this.renderer.splice(start, deleteCount, elements.length); - this.set(indexes); + this._set(indexes, indexes); } renderIndex(index: number, container: HTMLElement): void { @@ -154,10 +150,17 @@ class Trait implements ISpliceable, IDisposable { * @return The old indexes which had this trait. */ set(indexes: number[], browserEvent?: UIEvent): number[] { - const result = this.indexes; - this.indexes = indexes; + return this._set(indexes, [...indexes].sort(numericSort), browserEvent); + } - const toRender = disjunction(result, indexes); + private _set(indexes: number[], sortedIndexes: number[], browserEvent?: UIEvent): number[] { + const result = this.indexes; + const sortedResult = this.sortedIndexes; + + this.indexes = indexes; + this.sortedIndexes = sortedIndexes; + + const toRender = disjunction(sortedResult, indexes); this.renderer.renderIndexes(toRender); this._onChange.fire({ indexes, browserEvent }); @@ -169,7 +172,7 @@ class Trait implements ISpliceable, IDisposable { } contains(index: number): boolean { - return this.indexes.some(i => i === index); + return binarySearch(this.sortedIndexes, index, numericSort) >= 0; } dispose() { @@ -1334,7 +1337,6 @@ export class List implements ISpliceable, IDisposable { } } - indexes = indexes.sort(numericSort); this.selection.set(indexes, browserEvent); } @@ -1353,7 +1355,6 @@ export class List implements ISpliceable, IDisposable { } } - indexes = indexes.sort(numericSort); this.focus.set(indexes, browserEvent); } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 87bd41bc0d..ec7a374145 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -14,7 +14,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; import { IDragAndDropData, StaticDND, DragAndDropData } from 'vs/base/browser/dnd'; -import { range, equals } from 'vs/base/common/arrays'; +import { range, equals, distinctES6 } from 'vs/base/common/arrays'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { domEvent } from 'vs/base/browser/event'; import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; @@ -924,7 +924,7 @@ class TreeNodeList extends List> const additionalSelection: number[] = []; elements.forEach((node, index) => { - if (this.selectionTrait.has(node)) { + if (this.focusTrait.has(node)) { additionalFocus.push(start + index); } @@ -934,11 +934,11 @@ class TreeNodeList extends List> }); if (additionalFocus.length > 0) { - super.setFocus([...super.getFocus(), ...additionalFocus]); + super.setFocus(distinctES6([...super.getFocus(), ...additionalFocus])); } if (additionalSelection.length > 0) { - super.setSelection([...super.getSelection(), ...additionalSelection]); + super.setSelection(distinctES6([...super.getSelection(), ...additionalSelection])); } } diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index d0897e31b4..6b894d7d1f 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -364,6 +364,18 @@ export function distinct(array: ReadonlyArray, keyFn?: (t: T) => string): }); } +export function distinctES6(array: ReadonlyArray): T[] { + const seen = new Set(); + return array.filter(element => { + if (seen.has(element)) { + return false; + } + + seen.add(element); + return true; + }); +} + export function uniqueFilter(keyFn: (t: T) => string): (t: T) => boolean { const seen: { [key: string]: boolean; } = Object.create(null); diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 2d7ca15f58..a5bcb9857b 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -765,3 +765,19 @@ export class IdleValue { } //#endregion + +export async function retry(task: ITask>, delay: number, retries: number): Promise { + let lastError: Error | undefined; + + for (let i = 0; i < retries; i++) { + try { + return await task(); + } catch (error) { + lastError = error; + + await timeout(delay); + } + } + + return Promise.reject(lastError); +} \ No newline at end of file diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts new file mode 100644 index 0000000000..f04e22427d --- /dev/null +++ b/src/vs/base/common/buffer.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare var Buffer: any; +const hasBuffer = (typeof Buffer !== 'undefined'); + +export class VSBuffer { + + public static alloc(byteLength: number): VSBuffer { + if (hasBuffer) { + return new VSBuffer(Buffer.allocUnsafe(byteLength)); + } else { + return new VSBuffer(new Uint8Array(byteLength)); + } + } + + public static wrap(actual: Uint8Array): VSBuffer { + return new VSBuffer(actual); + } + + public static fromString(source: string): VSBuffer { + return new VSBuffer(Buffer.from(source)); + } + + public static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer { + if (typeof totalLength === 'undefined') { + totalLength = 0; + for (let i = 0, len = buffers.length; i < len; i++) { + totalLength += buffers[i].byteLength; + } + } + + const ret = VSBuffer.alloc(totalLength); + let offset = 0; + for (let i = 0, len = buffers.length; i < len; i++) { + const element = buffers[i]; + ret.set(element, offset); + offset += element.byteLength; + } + + return ret; + } + + public readonly buffer: Uint8Array; + public readonly byteLength: number; + + private constructor(buffer: Uint8Array) { + this.buffer = buffer; + this.byteLength = this.buffer.byteLength; + } + + public toString(): string { + return this.buffer.toString(); + } + + public slice(start?: number, end?: number): VSBuffer { + return new VSBuffer(this.buffer.slice(start, end)); + } + + public set(array: VSBuffer, offset?: number): void { + this.buffer.set(array.buffer, offset); + } + + public readUint32BE(offset: number): number { + return readUint32BE(this.buffer, offset); + } + + public writeUint32BE(value: number, offset: number): void { + writeUint32BE(this.buffer, value, offset); + } + + public readUint8(offset: number): number { + return readUint8(this.buffer, offset); + } + + public writeUint8(value: number, offset: number): void { + writeUint8(this.buffer, value, offset); + } + +} + +function readUint32BE(source: Uint8Array, offset: number): number { + return ( + source[offset] * 2 ** 24 + + source[offset + 1] * 2 ** 16 + + source[offset + 2] * 2 ** 8 + + source[offset + 3] + ); +} + +function writeUint32BE(destination: Uint8Array, value: number, offset: number): void { + destination[offset + 3] = value; + value = value >>> 8; + destination[offset + 2] = value; + value = value >>> 8; + destination[offset + 1] = value; + value = value >>> 8; + destination[offset] = value; +} + +function readUint8(source: Uint8Array, offset: number): number { + return source[offset]; +} + +function writeUint8(destination: Uint8Array, value: number, offset: number): void { + destination[offset] = value; +} diff --git a/src/vs/base/common/labels.ts b/src/vs/base/common/labels.ts index e7093a367f..eaead09abb 100644 --- a/src/vs/base/common/labels.ts +++ b/src/vs/base/common/labels.ts @@ -369,8 +369,8 @@ export function mnemonicMenuLabel(label: string, forceDisableMnemonics?: boolean * - Linux: Supported via _ character (replace && with _) * - macOS: Unsupported (replace && with empty string) */ -export function mnemonicButtonLabel(label: string): string { - if (isMacintosh) { +export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: boolean): string { + if (isMacintosh || forceDisableMnemonics) { return label.replace(/\(&&\w\)|&&/g, ''); } diff --git a/src/vs/base/node/extfs.ts b/src/vs/base/node/extfs.ts index 7023e65dfc..f271a3e0db 100644 --- a/src/vs/base/node/extfs.ts +++ b/src/vs/base/node/extfs.ts @@ -371,10 +371,10 @@ export interface IWriteFileOptions { } let canFlush = true; -export function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream, options: IWriteFileOptions, callback: (error?: Error) => void): void { +export function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options: IWriteFileOptions, callback: (error?: Error) => void): void { options = ensureOptions(options); - if (typeof data === 'string' || Buffer.isBuffer(data)) { + if (typeof data === 'string' || Buffer.isBuffer(data) || data instanceof Uint8Array) { doWriteFileAndFlush(path, data, options, callback); } else { doWriteFileStreamAndFlush(path, data, options, callback); @@ -472,9 +472,9 @@ function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, // 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, options: IWriteFileOptions, callback: (error?: Error) => void): void { +function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, options: IWriteFileOptions, callback: (error?: Error) => void): void { if (options.encoding) { - data = encode(data, options.encoding.charset, { addBOM: options.encoding.addBOM }); + data = encode(data instanceof Uint8Array ? Buffer.from(data) : data, options.encoding.charset, { addBOM: options.encoding.addBOM }); } if (!canFlush) { diff --git a/src/vs/base/node/storage.ts b/src/vs/base/node/storage.ts index a019e6a5f2..9b40e5da6a 100644 --- a/src/vs/base/node/storage.ts +++ b/src/vs/base/node/storage.ts @@ -694,7 +694,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { private prepare(connection: IDatabaseConnection, sql: string, runCallback: (stmt: Statement) => void, errorDetails: () => string): void { const stmt = connection.db.prepare(sql); - const statementErrorListener = error => { + const statementErrorListener = (error: Error) => { this.handleSQLiteError(connection, error, `[storage ${this.name}] prepare(): ${error} (${sql}). Details: ${errorDetails()}`); }; diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts new file mode 100644 index 0000000000..7fdf873004 --- /dev/null +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -0,0 +1,774 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; +import { VSBuffer } from 'vs/base/common/buffer'; +import * as platform from 'vs/base/common/platform'; + +declare var process: any; + +export interface ISocket { + onData(listener: (e: VSBuffer) => void): IDisposable; + onClose(listener: () => void): IDisposable; + onEnd(listener: () => void): IDisposable; + write(buffer: VSBuffer): void; + end(): void; +} + +let emptyBuffer: VSBuffer | null = null; +function getEmptyBuffer(): VSBuffer { + if (!emptyBuffer) { + emptyBuffer = VSBuffer.alloc(0); + } + return emptyBuffer; +} + +class ChunkStream { + + private _chunks: VSBuffer[]; + private _totalLength: number; + + public get byteLength() { + return this._totalLength; + } + + constructor() { + this._chunks = []; + this._totalLength = 0; + } + + public acceptChunk(buff: VSBuffer) { + this._chunks.push(buff); + this._totalLength += buff.byteLength; + } + + public read(byteCount: number): VSBuffer { + if (byteCount === 0) { + return getEmptyBuffer(); + } + + if (byteCount > this._totalLength) { + throw new Error(`Cannot read so many bytes!`); + } + + if (this._chunks[0].byteLength === byteCount) { + // super fast path, precisely first chunk must be returned + const result = 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; + return result; + } + + let result = VSBuffer.alloc(byteCount); + let resultOffset = 0; + while (byteCount > 0) { + const chunk = this._chunks[0]; + 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; + byteCount -= byteCount; + } else { + // this chunk will be entirely read + this._chunks.shift(); + + result.set(chunk, resultOffset); + resultOffset += chunk.byteLength; + this._totalLength -= chunk.byteLength; + byteCount -= chunk.byteLength; + } + } + return result; + } +} + +const enum ProtocolMessageType { + None = 0, + Regular = 1, + Control = 2, + Ack = 3, + KeepAlive = 4 +} + +export const enum ProtocolConstants { + HeaderLength = 13, + /** + * Send an Acknowledge message at most 2 seconds later... + */ + AcknowledgeTime = 2000, // 2 seconds + /** + * If there is a message that has been unacknowledged for 10 seconds, consider the connection closed... + */ + AcknowledgeTimeoutTime = 10000, // 10 seconds + /** + * Send at least a message every 30s for keep alive reasons. + */ + KeepAliveTime = 30000, // 30 seconds + /** + * If there is no message received for 60 seconds, consider the connection closed... + */ + KeepAliveTimeoutTime = 60000, // 60 seconds + /** + * If there is no reconnection within this time-frame, consider the connection permanently closed... + */ + ReconnectionGraceTime = 60 * 60 * 1000, // 1hr +} + +class ProtocolMessage { + + public writtenTime: number; + + constructor( + public readonly type: ProtocolMessageType, + public readonly id: number, + public readonly ack: number, + public readonly data: VSBuffer + ) { + this.writtenTime = 0; + } + + public get size(): number { + return this.data.byteLength; + } +} + +class ProtocolReader extends Disposable { + + private readonly _socket: ISocket; + private _isDisposed: boolean; + private readonly _incomingData: ChunkStream; + public lastReadTime: number; + + private readonly _onMessage = new Emitter(); + public readonly onMessage: Event = this._onMessage.event; + + private readonly _state = { + readHead: true, + readLen: ProtocolConstants.HeaderLength, + messageType: ProtocolMessageType.None, + id: 0, + ack: 0 + }; + + constructor(socket: ISocket) { + super(); + this._socket = socket; + this._isDisposed = false; + this._incomingData = new ChunkStream(); + this._register(this._socket.onData(data => this.acceptChunk(data))); + this.lastReadTime = Date.now(); + } + + public acceptChunk(data: VSBuffer | null): void { + if (!data || data.byteLength === 0) { + return; + } + + this.lastReadTime = Date.now(); + + this._incomingData.acceptChunk(data); + + while (this._incomingData.byteLength >= this._state.readLen) { + + const buff = this._incomingData.read(this._state.readLen); + + if (this._state.readHead) { + // buff is the header + + // save new state => next time will read the body + this._state.readHead = false; + this._state.readLen = buff.readUint32BE(9); + this._state.messageType = buff.readUint8(0); + this._state.id = buff.readUint32BE(1); + this._state.ack = buff.readUint32BE(5); + } else { + // buff is the body + const messageType = this._state.messageType; + const id = this._state.id; + const ack = this._state.ack; + + // save new state => next time will read the header + this._state.readHead = true; + this._state.readLen = ProtocolConstants.HeaderLength; + this._state.messageType = ProtocolMessageType.None; + this._state.id = 0; + this._state.ack = 0; + + this._onMessage.fire(new ProtocolMessage(messageType, id, ack, buff)); + + if (this._isDisposed) { + // check if an event listener lead to our disposal + break; + } + } + } + } + + public readEntireBuffer(): VSBuffer { + return this._incomingData.read(this._incomingData.byteLength); + } + + public dispose(): void { + this._isDisposed = true; + super.dispose(); + } +} + +class ProtocolWriter { + + private _isDisposed: boolean; + private readonly _socket: ISocket; + private _data: VSBuffer[]; + private _totalLength: number; + public lastWriteTime: number; + + constructor(socket: ISocket) { + this._isDisposed = false; + this._socket = socket; + this._data = []; + this._totalLength = 0; + this.lastWriteTime = 0; + } + + public dispose(): void { + this.flush(); + this._isDisposed = true; + } + + public flush(): void { + // flush + this._writeNow(); + } + + public write(msg: ProtocolMessage) { + if (this._isDisposed) { + console.warn(`Cannot write message in a disposed ProtocolWriter`); + console.warn(msg); + return; + } + msg.writtenTime = Date.now(); + this.lastWriteTime = Date.now(); + const header = VSBuffer.alloc(ProtocolConstants.HeaderLength); + header.writeUint8(msg.type, 0); + header.writeUint32BE(msg.id, 1); + header.writeUint32BE(msg.ack, 5); + header.writeUint32BE(msg.data.byteLength, 9); + this._writeSoon(header, msg.data); + } + + private _bufferAdd(head: VSBuffer, body: VSBuffer): boolean { + const wasEmpty = this._totalLength === 0; + this._data.push(head, body); + this._totalLength += head.byteLength + body.byteLength; + return wasEmpty; + } + + private _bufferTake(): VSBuffer { + const ret = VSBuffer.concat(this._data, this._totalLength); + this._data.length = 0; + this._totalLength = 0; + return ret; + } + + private _writeSoon(header: VSBuffer, data: VSBuffer): void { + if (this._bufferAdd(header, data)) { + platform.setImmediate(() => { + this._writeNow(); + }); + } + } + + private _writeNow(): void { + if (this._totalLength === 0) { + return; + } + this._socket.write(this._bufferTake()); + } +} + +/** + * A message has the following format: + * ``` + * /-------------------------------|------\ + * | HEADER | | + * |-------------------------------| DATA | + * | TYPE | ID | ACK | DATA_LENGTH | | + * \-------------------------------|------/ + * ``` + * The header is 9 bytes and consists of: + * - TYPE is 1 byte (ProtocolMessageType) - the message type + * - ID is 4 bytes (u32be) - the message id (can be 0 to indicate to be ignored) + * - ACK is 4 bytes (u32be) - the acknowledged message id (can be 0 to indicate to be ignored) + * - DATA_LENGTH is 4 bytes (u32be) - the length in bytes of DATA + * + * Only Regular messages are counted, other messages are not counted, nor acknowledged. + */ +export class Protocol extends Disposable implements IMessagePassingProtocol { + + private _socket: ISocket; + private _socketWriter: ProtocolWriter; + private _socketReader: ProtocolReader; + + private _onMessage = new Emitter(); + readonly onMessage: Event = this._onMessage.event; + + private _onClose = new Emitter(); + readonly onClose: Event = this._onClose.event; + + constructor(socket: ISocket) { + super(); + this._socket = socket; + this._socketWriter = this._register(new ProtocolWriter(this._socket)); + this._socketReader = this._register(new ProtocolReader(this._socket)); + + this._register(this._socketReader.onMessage((msg) => { + if (msg.type === ProtocolMessageType.Regular) { + this._onMessage.fire(msg.data); + } + })); + + this._register(this._socket.onClose(() => this._onClose.fire())); + } + + getSocket(): ISocket { + return this._socket; + } + + send(buffer: VSBuffer): void { + this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.Regular, 0, 0, buffer)); + } +} + +export class Client extends IPCClient { + + static fromSocket(socket: ISocket, id: TContext): Client { + return new Client(new Protocol(socket), id); + } + + get onClose(): Event { return this.protocol.onClose; } + + constructor(private protocol: Protocol | PersistentProtocol, id: TContext) { + super(protocol, id); + } + + dispose(): void { + super.dispose(); + const socket = this.protocol.getSocket(); + this.protocol.dispose(); + socket.end(); + } +} + +/** + * Will ensure no messages are lost if there are no event listeners. + */ +function createBufferedEvent(source: Event): Event { + let emitter: Emitter; + let hasListeners = false; + let isDeliveringMessages = false; + let bufferedMessages: T[] = []; + + const deliverMessages = () => { + if (isDeliveringMessages) { + return; + } + isDeliveringMessages = true; + while (hasListeners && bufferedMessages.length > 0) { + emitter.fire(bufferedMessages.shift()!); + } + isDeliveringMessages = false; + }; + + source((e: T) => { + bufferedMessages.push(e); + deliverMessages(); + }); + + emitter = new Emitter({ + onFirstListenerAdd: () => { + hasListeners = true; + // it is important to deliver these messages after this call, but before + // other messages have a chance to be received (to guarantee in order delivery) + // that's why we're using here nextTick and not other types of timeouts + if (typeof process !== 'undefined') { + process.nextTick(deliverMessages); + } else { + platform.setImmediate(deliverMessages); + } + }, + onLastListenerRemove: () => { + hasListeners = false; + } + }); + + return emitter.event; +} + +class QueueElement { + public readonly data: T; + public next: QueueElement | null; + + constructor(data: T) { + this.data = data; + this.next = null; + } +} + +class Queue { + + private _first: QueueElement | null; + private _last: QueueElement | null; + + constructor() { + this._first = null; + this._last = null; + } + + public peek(): T | null { + if (!this._first) { + return null; + } + return this._first.data; + } + + public toArray(): T[] { + let result: T[] = [], resultLen = 0; + let it = this._first; + while (it) { + result[resultLen++] = it.data; + it = it.next; + } + return result; + } + + public pop(): void { + if (!this._first) { + return; + } + if (this._first === this._last) { + this._first = null; + this._last = null; + return; + } + this._first = this._first.next; + } + + public push(item: T): void { + const element = new QueueElement(item); + if (!this._first) { + this._first = element; + this._last = element; + return; + } + this._last!.next = element; + this._last = element; + } +} + +/** + * Same as Protocol, but will actually track messages and acks. + * Moreover, it will ensure no messages are lost if there are no event listeners. + */ +export class PersistentProtocol { + + private _isReconnecting: boolean; + + private _outgoingUnackMsg: Queue; + private _outgoingMsgId: number; + private _outgoingAckId: number; + private _outgoingAckTimeout: any | null; + + private _incomingMsgId: number; + private _incomingAckId: number; + private _incomingMsgLastTime: number; + private _incomingAckTimeout: any | null; + + private _outgoingKeepAliveTimeout: any | null; + private _incomingKeepAliveTimeout: any | null; + + private _socket: ISocket; + private _socketWriter: ProtocolWriter; + private _socketReader: ProtocolReader; + private _socketDisposables: IDisposable[]; + + private _onControlMessage = new Emitter(); + readonly onControlMessage: Event = createBufferedEvent(this._onControlMessage.event); + + private _onMessage = new Emitter(); + readonly onMessage: Event = createBufferedEvent(this._onMessage.event); + + private _onClose = new Emitter(); + readonly onClose: Event = createBufferedEvent(this._onClose.event); + + private _onSocketClose = new Emitter(); + readonly onSocketClose: Event = createBufferedEvent(this._onSocketClose.event); + + private _onSocketTimeout = new Emitter(); + readonly onSocketTimeout: Event = createBufferedEvent(this._onSocketTimeout.event); + + public get unacknowledgedCount(): number { + return this._outgoingMsgId - this._outgoingAckId; + } + + constructor(socket: ISocket, initialChunk: VSBuffer | null = null) { + this._isReconnecting = false; + this._outgoingUnackMsg = new Queue(); + this._outgoingMsgId = 0; + this._outgoingAckId = 0; + this._outgoingAckTimeout = null; + + this._incomingMsgId = 0; + this._incomingAckId = 0; + this._incomingMsgLastTime = 0; + this._incomingAckTimeout = null; + + this._outgoingKeepAliveTimeout = null; + this._incomingKeepAliveTimeout = null; + + this._socketDisposables = []; + this._socket = socket; + this._socketWriter = new ProtocolWriter(this._socket); + this._socketDisposables.push(this._socketWriter); + this._socketReader = new ProtocolReader(this._socket); + this._socketDisposables.push(this._socketReader); + this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg))); + this._socketDisposables.push(this._socket.onClose(() => this._onSocketClose.fire())); + this._socketDisposables.push(this._socket.onEnd(() => this._onClose.fire())); + if (initialChunk) { + this._socketReader.acceptChunk(initialChunk); + } + + this._sendKeepAliveCheck(); + this._recvKeepAliveCheck(); + } + + dispose(): void { + if (this._outgoingAckTimeout) { + clearTimeout(this._outgoingAckTimeout); + this._outgoingAckTimeout = null; + } + if (this._incomingAckTimeout) { + clearTimeout(this._incomingAckTimeout); + this._incomingAckTimeout = null; + } + if (this._outgoingKeepAliveTimeout) { + clearTimeout(this._outgoingKeepAliveTimeout); + this._outgoingKeepAliveTimeout = null; + } + if (this._incomingKeepAliveTimeout) { + clearTimeout(this._incomingKeepAliveTimeout); + this._incomingKeepAliveTimeout = null; + } + this._socketDisposables = dispose(this._socketDisposables); + } + + private _sendKeepAliveCheck(): void { + if (this._outgoingKeepAliveTimeout) { + // there will be a check in the near future + return; + } + + const timeSinceLastOutgoingMsg = Date.now() - this._socketWriter.lastWriteTime; + if (timeSinceLastOutgoingMsg >= ProtocolConstants.KeepAliveTime) { + // sufficient time has passed since last message was written, + // and no message from our side needed to be sent in the meantime, + // so we will send a message containing only a keep alive. + const msg = new ProtocolMessage(ProtocolMessageType.KeepAlive, 0, 0, getEmptyBuffer()); + this._socketWriter.write(msg); + this._sendKeepAliveCheck(); + return; + } + + this._outgoingKeepAliveTimeout = setTimeout(() => { + this._outgoingKeepAliveTimeout = null; + this._sendKeepAliveCheck(); + }, ProtocolConstants.KeepAliveTime - timeSinceLastOutgoingMsg + 5); + } + + private _recvKeepAliveCheck(): void { + if (this._incomingKeepAliveTimeout) { + // there will be a check in the near future + return; + } + + const timeSinceLastIncomingMsg = Date.now() - this._socketReader.lastReadTime; + if (timeSinceLastIncomingMsg >= ProtocolConstants.KeepAliveTimeoutTime) { + // Trash the socket + this._onSocketTimeout.fire(undefined); + return; + } + + this._incomingKeepAliveTimeout = setTimeout(() => { + this._incomingKeepAliveTimeout = null; + this._recvKeepAliveCheck(); + }, ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg + 5); + } + + public getSocket(): ISocket { + return this._socket; + } + + public beginAcceptReconnection(socket: ISocket, initialDataChunk: VSBuffer | null): void { + this._isReconnecting = true; + + this._socketDisposables = dispose(this._socketDisposables); + + this._socket = socket; + this._socketWriter = new ProtocolWriter(this._socket); + this._socketDisposables.push(this._socketWriter); + this._socketReader = new ProtocolReader(this._socket); + this._socketDisposables.push(this._socketReader); + this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg))); + this._socketDisposables.push(this._socket.onClose(() => this._onSocketClose.fire())); + this._socketDisposables.push(this._socket.onEnd(() => this._onClose.fire())); + this._socketReader.acceptChunk(initialDataChunk); + } + + public endAcceptReconnection(): void { + this._isReconnecting = false; + + // Send again all unacknowledged messages + const toSend = this._outgoingUnackMsg.toArray(); + for (let i = 0, len = toSend.length; i < len; i++) { + this._socketWriter.write(toSend[i]); + } + this._recvAckCheck(); + + this._sendKeepAliveCheck(); + this._recvKeepAliveCheck(); + } + + private _receiveMessage(msg: ProtocolMessage): void { + if (msg.ack > this._outgoingAckId) { + this._outgoingAckId = msg.ack; + do { + const first = this._outgoingUnackMsg.peek(); + if (first && first.id <= msg.ack) { + // this message has been confirmed, remove it + this._outgoingUnackMsg.pop(); + } else { + break; + } + } while (true); + } + + if (msg.type === ProtocolMessageType.Regular) { + if (msg.id > this._incomingMsgId) { + if (msg.id !== this._incomingMsgId + 1) { + console.error(`PROTOCOL CORRUPTION, LAST SAW MSG ${this._incomingMsgId} AND HAVE NOW RECEIVED MSG ${msg.id}`); + } + this._incomingMsgId = msg.id; + this._incomingMsgLastTime = Date.now(); + this._sendAckCheck(); + this._onMessage.fire(msg.data); + } + } else if (msg.type === ProtocolMessageType.Control) { + this._onControlMessage.fire(msg.data); + } + } + + readEntireBuffer(): VSBuffer { + return this._socketReader.readEntireBuffer(); + } + + flush(): void { + this._socketWriter.flush(); + } + + send(buffer: VSBuffer): void { + const myId = ++this._outgoingMsgId; + this._incomingAckId = this._incomingMsgId; + const msg = new ProtocolMessage(ProtocolMessageType.Regular, myId, this._incomingAckId, buffer); + this._outgoingUnackMsg.push(msg); + if (!this._isReconnecting) { + this._socketWriter.write(msg); + this._recvAckCheck(); + } + } + + /** + * Send a message which will not be part of the regular acknowledge flow. + * Use this for early control messages which are repeated in case of reconnection. + */ + sendControl(buffer: VSBuffer): void { + const msg = new ProtocolMessage(ProtocolMessageType.Control, 0, 0, buffer); + this._socketWriter.write(msg); + } + + private _sendAckCheck(): void { + if (this._incomingMsgId <= this._incomingAckId) { + // nothink to acknowledge + return; + } + + if (this._incomingAckTimeout) { + // there will be a check in the near future + return; + } + + const timeSinceLastIncomingMsg = Date.now() - this._incomingMsgLastTime; + if (timeSinceLastIncomingMsg >= ProtocolConstants.AcknowledgeTime) { + // sufficient time has passed since this message has been received, + // and no message from our side needed to be sent in the meantime, + // so we will send a message containing only an ack. + this._sendAck(); + return; + } + + this._incomingAckTimeout = setTimeout(() => { + this._incomingAckTimeout = null; + this._sendAckCheck(); + }, ProtocolConstants.AcknowledgeTime - timeSinceLastIncomingMsg + 5); + } + + private _recvAckCheck(): void { + if (this._outgoingMsgId <= this._outgoingAckId) { + // everything has been acknowledged + return; + } + + if (this._outgoingAckTimeout) { + // there will be a check in the near future + return; + } + + const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek()!; + const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime; + if (timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.AcknowledgeTimeoutTime) { + // Trash the socket + this._onSocketTimeout.fire(undefined); + return; + } + + this._outgoingAckTimeout = setTimeout(() => { + this._outgoingAckTimeout = null; + this._recvAckCheck(); + }, ProtocolConstants.AcknowledgeTimeoutTime - timeSinceOldestUnacknowledgedMsg + 5); + } + + private _sendAck(): void { + if (this._incomingMsgId <= this._incomingAckId) { + // nothink to acknowledge + return; + } + + this._incomingAckId = this._incomingMsgId; + const msg = new ProtocolMessage(ProtocolMessageType.Ack, 0, this._incomingAckId, getEmptyBuffer()); + this._socketWriter.write(msg); + } +} diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 498a88ffea..112fb5c422 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -3,8 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter, Relay } from 'vs/base/common/event'; +import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import * as errors from 'vs/base/common/errors'; +import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { VSBuffer } from 'vs/base/common/buffer'; /** * An `IChannel` is an abstraction over a collection of commands. @@ -26,3 +31,743 @@ export interface IServerChannel { call(ctx: TContext, command: string, arg?: any, cancellationToken?: CancellationToken): Promise; listen(ctx: TContext, event: string, arg?: any): Event; } + + +export const enum RequestType { + Promise = 100, + PromiseCancel = 101, + EventListen = 102, + EventDispose = 103 +} + +type IRawPromiseRequest = { type: RequestType.Promise; id: number; channelName: string; name: string; arg: any; }; +type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel, id: number }; +type IRawEventListenRequest = { type: RequestType.EventListen; id: number; channelName: string; name: string; arg: any; }; +type IRawEventDisposeRequest = { type: RequestType.EventDispose, id: number }; +type IRawRequest = IRawPromiseRequest | IRawPromiseCancelRequest | IRawEventListenRequest | IRawEventDisposeRequest; + +export const enum ResponseType { + Initialize = 200, + PromiseSuccess = 201, + PromiseError = 202, + PromiseErrorObj = 203, + EventFire = 204 +} + +type IRawInitializeResponse = { type: ResponseType.Initialize }; +type IRawPromiseSuccessResponse = { type: ResponseType.PromiseSuccess; id: number; data: any }; +type IRawPromiseErrorResponse = { type: ResponseType.PromiseError; id: number; data: { message: string, name: string, stack: string[] | undefined } }; +type IRawPromiseErrorObjResponse = { type: ResponseType.PromiseErrorObj; id: number; data: any }; +type IRawEventFireResponse = { type: ResponseType.EventFire; id: number; data: any }; +type IRawResponse = IRawInitializeResponse | IRawPromiseSuccessResponse | IRawPromiseErrorResponse | IRawPromiseErrorObjResponse | IRawEventFireResponse; + +interface IHandler { + (response: IRawResponse): void; +} + +export interface IMessagePassingProtocol { + send(buffer: VSBuffer): void; + onMessage: Event; +} + +enum State { + Uninitialized, + Idle +} + +/** + * An `IChannelServer` hosts a collection of channels. You are + * able to register channels onto it, provided a channel name. + */ +export interface IChannelServer { + registerChannel(channelName: string, channel: IServerChannel): void; +} + +/** + * An `IChannelClient` has access to a collection of channels. You + * are able to get those channels, given their channel name. + */ +export interface IChannelClient { + getChannel(channelName: string): T; +} + +export interface Client { + readonly ctx: TContext; +} + +export interface IConnectionHub { + readonly connections: Connection[]; + readonly onDidChangeConnections: Event>; +} + +/** + * An `IClientRouter` is responsible for routing calls to specific + * channels, in scenarios in which there are multiple possible + * channels (each from a separate client) to pick from. + */ +export interface IClientRouter { + routeCall(hub: IConnectionHub, command: string, arg?: any, cancellationToken?: CancellationToken): Promise>; + routeEvent(hub: IConnectionHub, event: string, arg?: any): Promise>; +} + +/** + * Similar to the `IChannelClient`, you can get channels from this + * collection of channels. The difference being that in the + * `IRoutingChannelClient`, there are multiple clients providing + * the same channel. You'll need to pass in an `IClientRouter` in + * order to pick the right one. + */ +export interface IRoutingChannelClient { + getChannel(channelName: string, router: IClientRouter): T; +} + +interface IReader { + read(bytes: number): VSBuffer; +} + +interface IWriter { + write(buffer: VSBuffer): void; +} + +class BufferReader implements IReader { + + private pos = 0; + + constructor(private buffer: VSBuffer) { } + + read(bytes: number): VSBuffer { + const result = this.buffer.slice(this.pos, this.pos + bytes); + this.pos += result.byteLength; + return result; + } +} + +class BufferWriter implements IWriter { + + private buffers: VSBuffer[] = []; + + get buffer(): VSBuffer { + return VSBuffer.concat(this.buffers); + } + + write(buffer: VSBuffer): void { + this.buffers.push(buffer); + } +} + +enum DataType { + Undefined = 0, + String = 1, + Buffer = 2, + VSBuffer = 3, + Array = 4, + Object = 5 +} + +function createSizeBuffer(size: number): VSBuffer { + const result = VSBuffer.alloc(4); + result.writeUint32BE(size, 0); + return result; +} + +function readSizeBuffer(reader: IReader): number { + return reader.read(4).readUint32BE(0); +} + +function createOneByteBuffer(value: number): VSBuffer { + const result = VSBuffer.alloc(1); + result.writeUint8(value, 0); + return result; +} + +const BufferPresets = { + Undefined: createOneByteBuffer(DataType.Undefined), + String: createOneByteBuffer(DataType.String), + Buffer: createOneByteBuffer(DataType.Buffer), + VSBuffer: createOneByteBuffer(DataType.VSBuffer), + Array: createOneByteBuffer(DataType.Array), + Object: createOneByteBuffer(DataType.Object), +}; + +declare var Buffer: any; +const hasBuffer = (typeof Buffer !== 'undefined'); + +function serialize(writer: IWriter, data: any): void { + if (typeof data === 'undefined') { + writer.write(BufferPresets.Undefined); + } else if (typeof data === 'string') { + const buffer = VSBuffer.fromString(data); + writer.write(BufferPresets.String); + writer.write(createSizeBuffer(buffer.byteLength)); + writer.write(buffer); + } else if (hasBuffer && Buffer.isBuffer(data)) { + const buffer = VSBuffer.wrap(data); + writer.write(BufferPresets.Buffer); + writer.write(createSizeBuffer(buffer.byteLength)); + writer.write(buffer); + } else if (data instanceof VSBuffer) { + writer.write(BufferPresets.VSBuffer); + writer.write(createSizeBuffer(data.byteLength)); + writer.write(data); + } else if (Array.isArray(data)) { + writer.write(BufferPresets.Array); + writer.write(createSizeBuffer(data.length)); + + for (const el of data) { + serialize(writer, el); + } + } else { + const buffer = VSBuffer.fromString(JSON.stringify(data)); + writer.write(BufferPresets.Object); + writer.write(createSizeBuffer(buffer.byteLength)); + writer.write(buffer); + } +} + +function deserialize(reader: IReader): any { + const type = reader.read(1).readUint8(0); + + switch (type) { + case DataType.Undefined: return undefined; + case DataType.String: return reader.read(readSizeBuffer(reader)).toString(); + case DataType.Buffer: return reader.read(readSizeBuffer(reader)).buffer; + case DataType.VSBuffer: return reader.read(readSizeBuffer(reader)); + case DataType.Array: { + const length = readSizeBuffer(reader); + const result: any[] = []; + + for (let i = 0; i < length; i++) { + result.push(deserialize(reader)); + } + + return result; + } + case DataType.Object: return JSON.parse(reader.read(readSizeBuffer(reader)).toString()); + } +} + +export class ChannelServer implements IChannelServer, IDisposable { + + private channels = new Map>(); + private activeRequests = new Map(); + private protocolListener: IDisposable | null; + + constructor(private protocol: IMessagePassingProtocol, private ctx: TContext) { + this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg)); + this.sendResponse({ type: ResponseType.Initialize }); + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.channels.set(channelName, channel); + } + + private sendResponse(response: IRawResponse): void { + switch (response.type) { + case ResponseType.Initialize: + return this.send([response.type]); + + case ResponseType.PromiseSuccess: + case ResponseType.PromiseError: + case ResponseType.EventFire: + case ResponseType.PromiseErrorObj: + return this.send([response.type, response.id], response.data); + } + } + + private send(header: any, body: any = undefined): void { + const writer = new BufferWriter(); + serialize(writer, header); + serialize(writer, body); + this.sendBuffer(writer.buffer); + } + + private sendBuffer(message: VSBuffer): void { + try { + this.protocol.send(message); + } catch (err) { + // noop + } + } + + private onRawMessage(message: VSBuffer): void { + const reader = new BufferReader(message); + const header = deserialize(reader); + const body = deserialize(reader); + const type = header[0] as RequestType; + + switch (type) { + case RequestType.Promise: + return this.onPromise({ type, id: header[1], channelName: header[2], name: header[3], arg: body }); + case RequestType.EventListen: + return this.onEventListen({ type, id: header[1], channelName: header[2], name: header[3], arg: body }); + case RequestType.PromiseCancel: + return this.disposeActiveRequest({ type, id: header[1] }); + case RequestType.EventDispose: + return this.disposeActiveRequest({ type, id: header[1] }); + } + } + + private onPromise(request: IRawPromiseRequest): void { + const channel = this.channels.get(request.channelName); + if (!channel) { + throw new Error('Unknown channel'); + } + const cancellationTokenSource = new CancellationTokenSource(); + let promise: Promise; + + try { + promise = channel.call(this.ctx, request.name, request.arg, cancellationTokenSource.token); + } catch (err) { + promise = Promise.reject(err); + } + + const id = request.id; + + promise.then(data => { + this.sendResponse({ id, data, type: ResponseType.PromiseSuccess }); + this.activeRequests.delete(request.id); + }, err => { + if (err instanceof Error) { + this.sendResponse({ + id, data: { + message: err.message, + name: err.name, + stack: err.stack ? (err.stack.split ? err.stack.split('\n') : err.stack) : undefined + }, type: ResponseType.PromiseError + }); + } else { + this.sendResponse({ id, data: err, type: ResponseType.PromiseErrorObj }); + } + + this.activeRequests.delete(request.id); + }); + + const disposable = toDisposable(() => cancellationTokenSource.cancel()); + this.activeRequests.set(request.id, disposable); + } + + private onEventListen(request: IRawEventListenRequest): void { + const channel = this.channels.get(request.channelName); + if (!channel) { + throw new Error('Unknown channel'); + } + + const id = request.id; + const event = channel.listen(this.ctx, request.name, request.arg); + const disposable = event(data => this.sendResponse({ id, data, type: ResponseType.EventFire })); + + this.activeRequests.set(request.id, disposable); + } + + private disposeActiveRequest(request: IRawRequest): void { + const disposable = this.activeRequests.get(request.id); + + if (disposable) { + disposable.dispose(); + this.activeRequests.delete(request.id); + } + } + + public dispose(): void { + if (this.protocolListener) { + this.protocolListener.dispose(); + this.protocolListener = null; + } + this.activeRequests.forEach(d => d.dispose()); + this.activeRequests.clear(); + } +} + +export class ChannelClient implements IChannelClient, IDisposable { + + private state: State = State.Uninitialized; + private activeRequests = new Set(); + private handlers = new Map(); + private lastRequestId: number = 0; + private protocolListener: IDisposable | null; + + private _onDidInitialize = new Emitter(); + readonly onDidInitialize = this._onDidInitialize.event; + + constructor(private protocol: IMessagePassingProtocol) { + this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg)); + } + + getChannel(channelName: string): T { + const that = this; + + return { + call(command: string, arg?: any, cancellationToken?: CancellationToken) { + return that.requestPromise(channelName, command, arg, cancellationToken); + }, + listen(event: string, arg: any) { + return that.requestEvent(channelName, event, arg); + } + } as T; + } + + private requestPromise(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise { + const id = this.lastRequestId++; + const type = RequestType.Promise; + const request: IRawRequest = { id, type, channelName, name, arg }; + + if (cancellationToken.isCancellationRequested) { + return Promise.reject(errors.canceled()); + } + + let disposable: IDisposable; + + const result = new Promise((c, e) => { + if (cancellationToken.isCancellationRequested) { + return e(errors.canceled()); + } + + let uninitializedPromise: CancelablePromise | null = createCancelablePromise(_ => this.whenInitialized()); + uninitializedPromise.then(() => { + uninitializedPromise = null; + + const handler: IHandler = response => { + switch (response.type) { + case ResponseType.PromiseSuccess: + this.handlers.delete(id); + c(response.data); + break; + + case ResponseType.PromiseError: + this.handlers.delete(id); + const error = new Error(response.data.message); + (error).stack = response.data.stack; + error.name = response.data.name; + e(error); + break; + + case ResponseType.PromiseErrorObj: + this.handlers.delete(id); + e(response.data); + break; + } + }; + + this.handlers.set(id, handler); + this.sendRequest(request); + }); + + const cancel = () => { + if (uninitializedPromise) { + uninitializedPromise.cancel(); + uninitializedPromise = null; + } else { + this.sendRequest({ id, type: RequestType.PromiseCancel }); + } + + e(errors.canceled()); + }; + + const cancellationTokenListener = cancellationToken.onCancellationRequested(cancel); + disposable = combinedDisposable([toDisposable(cancel), cancellationTokenListener]); + this.activeRequests.add(disposable); + }); + + return result.finally(() => this.activeRequests.delete(disposable)); + } + + private requestEvent(channelName: string, name: string, arg?: any): Event { + const id = this.lastRequestId++; + const type = RequestType.EventListen; + const request: IRawRequest = { id, type, channelName, name, arg }; + + let uninitializedPromise: CancelablePromise | null = null; + + const emitter = new Emitter({ + onFirstListenerAdd: () => { + uninitializedPromise = createCancelablePromise(_ => this.whenInitialized()); + uninitializedPromise.then(() => { + uninitializedPromise = null; + this.activeRequests.add(emitter); + this.sendRequest(request); + }); + }, + onLastListenerRemove: () => { + if (uninitializedPromise) { + uninitializedPromise.cancel(); + uninitializedPromise = null; + } else { + this.activeRequests.delete(emitter); + this.sendRequest({ id, type: RequestType.EventDispose }); + } + } + }); + + const handler: IHandler = (res: IRawEventFireResponse) => emitter.fire(res.data); + this.handlers.set(id, handler); + + return emitter.event; + } + + private sendRequest(request: IRawRequest): void { + switch (request.type) { + case RequestType.Promise: + case RequestType.EventListen: + return this.send([request.type, request.id, request.channelName, request.name], request.arg); + + case RequestType.PromiseCancel: + case RequestType.EventDispose: + return this.send([request.type, request.id]); + } + } + + private send(header: any, body: any = undefined): void { + const writer = new BufferWriter(); + serialize(writer, header); + serialize(writer, body); + this.sendBuffer(writer.buffer); + } + + private sendBuffer(message: VSBuffer): void { + try { + this.protocol.send(message); + } catch (err) { + // noop + } + } + + private onBuffer(message: VSBuffer): void { + const reader = new BufferReader(message); + const header = deserialize(reader); + const body = deserialize(reader); + const type: ResponseType = header[0]; + + switch (type) { + case ResponseType.Initialize: + return this.onResponse({ type: header[0] }); + + case ResponseType.PromiseSuccess: + case ResponseType.PromiseError: + case ResponseType.EventFire: + case ResponseType.PromiseErrorObj: + return this.onResponse({ type: header[0], id: header[1], data: body }); + } + } + + private onResponse(response: IRawResponse): void { + if (response.type === ResponseType.Initialize) { + this.state = State.Idle; + this._onDidInitialize.fire(); + return; + } + + const handler = this.handlers.get(response.id); + + if (handler) { + handler(response); + } + } + + private whenInitialized(): Promise { + if (this.state === State.Idle) { + return Promise.resolve(); + } else { + return Event.toPromise(this.onDidInitialize); + } + } + + dispose(): void { + if (this.protocolListener) { + this.protocolListener.dispose(); + this.protocolListener = null; + } + this.activeRequests.forEach(p => p.dispose()); + this.activeRequests.clear(); + } +} + +export interface ClientConnectionEvent { + protocol: IMessagePassingProtocol; + onDidClientDisconnect: Event; +} + +interface Connection extends Client { + readonly channelClient: ChannelClient; +} + +/** + * An `IPCServer` is both a channel server and a routing channel + * client. + * + * As the owner of a protocol, you should extend both this + * and the `IPCClient` classes to get IPC implementations + * for your protocol. + */ +export class IPCServer implements IChannelServer, IRoutingChannelClient, IConnectionHub, IDisposable { + + private channels = new Map>(); + private _connections = new Set>(); + + private _onDidChangeConnections = new Emitter>(); + readonly onDidChangeConnections: Event> = this._onDidChangeConnections.event; + + get connections(): Connection[] { + const result: Connection[] = []; + this._connections.forEach(ctx => result.push(ctx)); + return result; + } + + constructor(onDidClientConnect: Event) { + onDidClientConnect(({ protocol, onDidClientDisconnect }) => { + const onFirstMessage = Event.once(protocol.onMessage); + + onFirstMessage(msg => { + const reader = new BufferReader(msg); + const ctx = deserialize(reader) as TContext; + + const channelServer = new ChannelServer(protocol, ctx); + const channelClient = new ChannelClient(protocol); + + this.channels.forEach((channel, name) => channelServer.registerChannel(name, channel)); + + const connection: Connection = { channelClient, ctx }; + this._connections.add(connection); + this._onDidChangeConnections.fire(connection); + + onDidClientDisconnect(() => { + channelServer.dispose(); + channelClient.dispose(); + this._connections.delete(connection); + }); + }); + }); + } + + getChannel(channelName: string, router: IClientRouter): T { + const that = this; + + return { + call(command: string, arg?: any, cancellationToken?: CancellationToken) { + const channelPromise = router.routeCall(that, command, arg) + .then(connection => (connection as Connection).channelClient.getChannel(channelName)); + + return getDelayedChannel(channelPromise) + .call(command, arg, cancellationToken); + }, + listen(event: string, arg: any) { + const channelPromise = router.routeEvent(that, event, arg) + .then(connection => (connection as Connection).channelClient.getChannel(channelName)); + + return getDelayedChannel(channelPromise) + .listen(event, arg); + } + } as T; + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.channels.set(channelName, channel); + } + + dispose(): void { + this.channels.clear(); + this._connections.clear(); + this._onDidChangeConnections.dispose(); + } +} + +/** + * An `IPCClient` is both a channel client and a channel server. + * + * As the owner of a protocol, you should extend both this + * and the `IPCClient` classes to get IPC implementations + * for your protocol. + */ +export class IPCClient implements IChannelClient, IChannelServer, IDisposable { + + private channelClient: ChannelClient; + private channelServer: ChannelServer; + + constructor(protocol: IMessagePassingProtocol, ctx: TContext) { + const writer = new BufferWriter(); + serialize(writer, ctx); + protocol.send(writer.buffer); + + this.channelClient = new ChannelClient(protocol); + this.channelServer = new ChannelServer(protocol, ctx); + } + + getChannel(channelName: string): T { + return this.channelClient.getChannel(channelName) as T; + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.channelServer.registerChannel(channelName, channel); + } + + dispose(): void { + this.channelClient.dispose(); + this.channelServer.dispose(); + } +} + +export function getDelayedChannel(promise: Promise): T { + return { + call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise { + return promise.then(c => c.call(command, arg, cancellationToken)); + }, + + listen(event: string, arg?: any): Event { + const relay = new Relay(); + promise.then(c => relay.input = c.listen(event, arg)); + return relay.event; + } + } as T; +} + +export function getNextTickChannel(channel: T): T { + let didTick = false; + + return { + call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise { + if (didTick) { + return channel.call(command, arg, cancellationToken); + } + + return timeout(0) + .then(() => didTick = true) + .then(() => channel.call(command, arg, cancellationToken)); + }, + listen(event: string, arg?: any): Event { + if (didTick) { + return channel.listen(event, arg); + } + + const relay = new Relay(); + + timeout(0) + .then(() => didTick = true) + .then(() => relay.input = channel.listen(event, arg)); + + return relay.event; + } + } as T; +} + +export class StaticRouter implements IClientRouter { + + constructor(private fn: (ctx: TContext) => boolean | Promise) { } + + routeCall(hub: IConnectionHub): Promise> { + return this.route(hub); + } + + routeEvent(hub: IConnectionHub): Promise> { + return this.route(hub); + } + + private async route(hub: IConnectionHub): Promise> { + for (const connection of hub.connections) { + if (await Promise.resolve(this.fn(connection.ctx))) { + return Promise.resolve(connection); + } + } + + await Event.toPromise(hub.onDidChangeConnections); + return await this.route(hub); + } +} diff --git a/src/vs/base/parts/ipc/electron-browser/ipc.electron-browser.ts b/src/vs/base/parts/ipc/electron-browser/ipc.electron-browser.ts index 6caf45d5af..6fbb7e1dcc 100644 --- a/src/vs/base/parts/ipc/electron-browser/ipc.electron-browser.ts +++ b/src/vs/base/parts/ipc/electron-browser/ipc.electron-browser.ts @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { IPCClient } from 'vs/base/parts/ipc/node/ipc'; +import { IPCClient } from 'vs/base/parts/ipc/common/ipc'; import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron'; import { ipcRenderer } from 'electron'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { VSBuffer } from 'vs/base/common/buffer'; export class Client extends IPCClient implements IDisposable { private protocol: Protocol; private static createProtocol(): Protocol { - const onMessage = Event.fromNodeEventEmitter(ipcRenderer, 'ipc:message', (_, message: Buffer) => message); + const onMessage = Event.fromNodeEventEmitter(ipcRenderer, 'ipc:message', (_, message: Buffer) => VSBuffer.wrap(message)); ipcRenderer.send('ipc:hello'); return new Protocol(ipcRenderer, onMessage); } diff --git a/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts b/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts index 55b7de5313..53c1850313 100644 --- a/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts +++ b/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts @@ -4,20 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/node/ipc'; +import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc'; import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron'; import { ipcMain } from 'electron'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { VSBuffer } from 'vs/base/common/buffer'; interface IIPCEvent { event: { sender: Electron.WebContents; }; message: Buffer | null; } -function createScopedOnMessageEvent(senderId: number, eventName: string): Event { +function createScopedOnMessageEvent(senderId: number, eventName: string): Event { const onMessage = Event.fromNodeEventEmitter(ipcMain, eventName, (event, message) => ({ event, message })); const onMessageFromSender = Event.filter(onMessage, ({ event }) => event.sender.id === senderId); - return Event.map(onMessageFromSender, ({ message }) => message); + // {{SQL CARBON EDIT}} cast message as null since typescript isn't saying its always null + return Event.map(onMessageFromSender, ({ message }) => message ? VSBuffer.wrap(message) : message as null); } export class Server extends IPCServer { @@ -38,7 +40,7 @@ export class Server extends IPCServer { const onDidClientReconnect = new Emitter(); Server.Clients.set(id, toDisposable(() => onDidClientReconnect.fire())); - const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event; + const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event; const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')), onDidClientReconnect.event); const protocol = new Protocol(webContents, onMessage); @@ -49,4 +51,4 @@ export class Server extends IPCServer { constructor() { super(Server.getOnDidClientConnect()); } -} \ No newline at end of file +} diff --git a/src/vs/base/parts/ipc/node/ipc.cp.ts b/src/vs/base/parts/ipc/node/ipc.cp.ts index 2595bdd241..209feebd4e 100644 --- a/src/vs/base/parts/ipc/node/ipc.cp.ts +++ b/src/vs/base/parts/ipc/node/ipc.cp.ts @@ -9,11 +9,11 @@ import { Delayer, createCancelablePromise } from 'vs/base/common/async'; import { deepClone, assign } from 'vs/base/common/objects'; import { Emitter, Event } from 'vs/base/common/event'; import { createQueuedSender } from 'vs/base/node/processes'; -import { ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient } from 'vs/base/parts/ipc/node/ipc'; +import { IChannel, ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient } from 'vs/base/parts/ipc/common/ipc'; import { isRemoteConsoleLog, log } from 'vs/base/common/console'; import { CancellationToken } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { VSBuffer } from 'vs/base/common/buffer'; /** * This implementation doesn't perform well since it uses base64 encoding for buffers. @@ -26,11 +26,11 @@ export class Server extends IPCServer { send: r => { try { if (process.send) { - process.send(r.toString('base64')); + process.send((r.buffer).toString('base64')); } } catch (e) { /* not much to do */ } }, - onMessage: Event.fromNodeEventEmitter(process, 'message', msg => Buffer.from(msg, 'base64')) + onMessage: Event.fromNodeEventEmitter(process, 'message', msg => VSBuffer.wrap(Buffer.from(msg, 'base64'))) }, ctx); process.once('disconnect', () => this.dispose()); @@ -199,7 +199,7 @@ export class Client implements IChannelClient, IDisposable { this.child = fork(this.modulePath, args, forkOpts); - const onMessageEmitter = new Emitter(); + const onMessageEmitter = new Emitter(); const onRawMessage = Event.fromNodeEventEmitter(this.child, 'message', msg => msg); onRawMessage(msg => { @@ -211,11 +211,11 @@ export class Client implements IChannelClient, IDisposable { } // Anything else goes to the outside - onMessageEmitter.fire(Buffer.from(msg, 'base64')); + onMessageEmitter.fire(VSBuffer.wrap(Buffer.from(msg, 'base64'))); }); const sender = this.options.useQueue ? createQueuedSender(this.child) : this.child; - const send = (r: Buffer) => this.child && this.child.connected && sender.send(r.toString('base64')); + const send = (r: VSBuffer) => this.child && this.child.connected && sender.send((r.buffer).toString('base64')); const onMessage = onMessageEmitter.event; const protocol = { send, onMessage }; diff --git a/src/vs/base/parts/ipc/node/ipc.electron.ts b/src/vs/base/parts/ipc/node/ipc.electron.ts index c922569bf6..9446ecd994 100644 --- a/src/vs/base/parts/ipc/node/ipc.electron.ts +++ b/src/vs/base/parts/ipc/node/ipc.electron.ts @@ -3,8 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; +import { VSBuffer } from 'vs/base/common/buffer'; export interface Sender { send(channel: string, msg: Buffer | null): void; @@ -12,11 +13,11 @@ export interface Sender { export class Protocol implements IMessagePassingProtocol { - constructor(private sender: Sender, readonly onMessage: Event) { } + constructor(private sender: Sender, readonly onMessage: Event) { } - send(message: Buffer): void { + send(message: VSBuffer): void { try { - this.sender.send('ipc:message', message); + this.sender.send('ipc:message', (message.buffer)); } catch (e) { // systems are going down } diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index 7c6a2894f2..34fb02613f 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -4,13 +4,62 @@ *--------------------------------------------------------------------------------------------*/ import { Socket, Server as NetServer, createConnection, createServer } from 'net'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IMessagePassingProtocol, ClientConnectionEvent, IPCServer, IPCClient } from 'vs/base/parts/ipc/node/ipc'; +import { Event } from 'vs/base/common/event'; +import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import * as fs from 'fs'; import { generateUuid } from 'vs/base/common/uuid'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ISocket, Protocol, Client } from 'vs/base/parts/ipc/common/ipc.net'; + +export class NodeSocket implements ISocket { + public readonly socket: Socket; + + constructor(socket: Socket) { + this.socket = socket; + } + + public onData(_listener: (e: VSBuffer) => void): IDisposable { + const listener = (buff: Buffer) => _listener(VSBuffer.wrap(buff)); + this.socket.on('data', listener); + return { + dispose: () => this.socket.off('data', listener) + }; + } + + public onClose(listener: () => void): IDisposable { + this.socket.on('close', listener); + return { + dispose: () => this.socket.off('close', listener) + }; + } + + public onEnd(listener: () => void): IDisposable { + this.socket.on('end', listener); + return { + dispose: () => this.socket.off('end', listener) + }; + } + + public write(buffer: VSBuffer): void { + // return early if socket has been destroyed in the meantime + if (this.socket.destroyed) { + return; + } + + // we ignore the returned value from `write` because we would have to cached the data + // anyways and nodejs is already doing that for us: + // > https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback + // > However, the false return value is only advisory and the writable stream will unconditionally + // > accept and buffer chunk even if it has not not been allowed to drain. + this.socket.write(buffer.buffer); + } + + public end(): void { + this.socket.end(); + } +} export function generateRandomPipeName(): string { const randomSuffix = generateUuid(); @@ -22,386 +71,13 @@ export function generateRandomPipeName(): string { } } -function log(fd: number, msg: string, data?: Buffer): void { - const date = new Date(); - fs.writeSync(fd, `[${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}] ${msg}\n`); - if (data) { - fs.writeSync(fd, data); - fs.writeSync(fd, `\n---------------------------------------------------------------------------------------------------------\n`); - } - fs.fdatasyncSync(fd); -} - -const EMPTY_BUFFER = Buffer.allocUnsafe(0); - -class ChunkStream { - - private _chunks: Buffer[]; - private _totalLength: number; - - public get byteLength() { - return this._totalLength; - } - - constructor() { - this._chunks = []; - this._totalLength = 0; - } - - public acceptChunk(buff: Buffer) { - this._chunks.push(buff); - this._totalLength += buff.byteLength; - } - - public read(byteCount: number): Buffer { - if (byteCount === 0) { - return EMPTY_BUFFER; - } - - if (byteCount > this._totalLength) { - throw new Error(`Cannot read so many bytes!`); - } - - if (this._chunks[0].byteLength === byteCount) { - // super fast path, precisely first chunk must be returned - const result = 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; - return result; - } - - let result = Buffer.allocUnsafe(byteCount); - let resultOffset = 0; - while (byteCount > 0) { - const chunk = this._chunks[0]; - if (chunk.byteLength > byteCount) { - // this chunk will survive - this._chunks[0] = chunk.slice(byteCount); - - chunk.copy(result, resultOffset, 0, byteCount); - resultOffset += byteCount; - this._totalLength -= byteCount; - byteCount -= byteCount; - } else { - // this chunk will be entirely read - this._chunks.shift(); - - chunk.copy(result, resultOffset, 0, chunk.byteLength); - resultOffset += chunk.byteLength; - this._totalLength -= chunk.byteLength; - byteCount -= chunk.byteLength; - } - } - return result; - } -} - -const enum ProtocolMessageType { - None = 0, - Regular = 1, - Control = 2, - Ack = 3, - KeepAlive = 4 -} - -function ProtocolMessageTypeToString(type: ProtocolMessageType): string { - switch (type) { - case ProtocolMessageType.None: return 'None'; - case ProtocolMessageType.Regular: return 'Regular'; - case ProtocolMessageType.Control: return 'Control'; - case ProtocolMessageType.Ack: return 'Ack'; - case ProtocolMessageType.KeepAlive: return 'KeepAlive'; - } -} - -export const enum ProtocolConstants { - HeaderLength = 13, - /** - * Send an Acknowledge message at most 2 seconds later... - */ - AcknowledgeTime = 2000, // 2 seconds - /** - * If there is a message that has been unacknowledged for 10 seconds, consider the connection closed... - */ - AcknowledgeTimeoutTime = 10000, // 10 seconds - /** - * Send at least a message every 30s for keep alive reasons. - */ - KeepAliveTime = 30000, // 30 seconds - /** - * If there is no message received for 60 seconds, consider the connection closed... - */ - KeepAliveTimeoutTime = 60000, // 60 seconds - /** - * If there is no reconnection within this time-frame, consider the connection permanently closed... - */ - ReconnectionGraceTime = 60 * 60 * 1000, // 1hr -} - -class ProtocolMessage { - - public writtenTime: number; - - constructor( - public readonly type: ProtocolMessageType, - public readonly id: number, - public readonly ack: number, - public readonly data: Buffer - ) { - this.writtenTime = 0; - } - - public get size(): number { - return this.data.byteLength; - } -} - -class ProtocolReader { - - private readonly _socket: Socket; - private _isDisposed: boolean; - private readonly _incomingData: ChunkStream; - private readonly _socketDataListener: (data: Buffer) => void; - public lastReadTime: number; - - private readonly _onMessage = new Emitter(); - public readonly onMessage: Event = this._onMessage.event; - - private readonly _state = { - readHead: true, - readLen: ProtocolConstants.HeaderLength, - messageType: ProtocolMessageType.None, - id: 0, - ack: 0 - }; - - constructor(socket: Socket) { - this._socket = socket; - this._isDisposed = false; - this._incomingData = new ChunkStream(); - this._socketDataListener = (data: Buffer) => this.acceptChunk(data); - this._socket.on('data', this._socketDataListener); - this.lastReadTime = Date.now(); - } - - public acceptChunk(data: Buffer | null): void { - if (!data || data.byteLength === 0) { - return; - } - - this.lastReadTime = Date.now(); - - this._incomingData.acceptChunk(data); - - while (this._incomingData.byteLength >= this._state.readLen) { - - const buff = this._incomingData.read(this._state.readLen); - - if (this._state.readHead) { - // buff is the header - - // save new state => next time will read the body - this._state.readHead = false; - this._state.readLen = buff.readUInt32BE(9, true); - this._state.messageType = buff.readUInt8(0, true); - this._state.id = buff.readUInt32BE(1, true); - this._state.ack = buff.readUInt32BE(5, true); - } else { - // buff is the body - const messageType = this._state.messageType; - const id = this._state.id; - const ack = this._state.ack; - - // save new state => next time will read the header - this._state.readHead = true; - this._state.readLen = ProtocolConstants.HeaderLength; - this._state.messageType = ProtocolMessageType.None; - this._state.id = 0; - this._state.ack = 0; - - this._onMessage.fire(new ProtocolMessage(messageType, id, ack, buff)); - - if (this._isDisposed) { - // check if an event listener lead to our disposal - break; - } - } - } - } - - public readEntireBuffer(): Buffer { - return this._incomingData.read(this._incomingData.byteLength); - } - - public dispose(): void { - this._isDisposed = true; - this._socket.removeListener('data', this._socketDataListener); - } -} - -class ProtocolWriter { - - private _isDisposed: boolean; - private readonly _socket: Socket; - private readonly _logFile: number; - private _data: Buffer[]; - private _totalLength; - public lastWriteTime: number; - - constructor(socket: Socket, logFile: number) { - this._isDisposed = false; - this._socket = socket; - this._logFile = logFile; - this._data = []; - this._totalLength = 0; - this.lastWriteTime = 0; - } - - public dispose(): void { - this.flush(); - this._isDisposed = true; - } - - public flush(): void { - // flush - this._writeNow(); - } - - public write(msg: ProtocolMessage) { - if (this._isDisposed) { - console.warn(`Cannot write message in a disposed ProtocolWriter`); - console.warn(msg); - return; - } - if (this._logFile) { - log(this._logFile, `send-${ProtocolMessageTypeToString(msg.type)}-${msg.id}-${msg.ack}-`, msg.data); - } - msg.writtenTime = Date.now(); - this.lastWriteTime = Date.now(); - const header = Buffer.allocUnsafe(ProtocolConstants.HeaderLength); - header.writeUInt8(msg.type, 0, true); - header.writeUInt32BE(msg.id, 1, true); - header.writeUInt32BE(msg.ack, 5, true); - header.writeUInt32BE(msg.data.length, 9, true); - this._writeSoon(header, msg.data); - } - - private _bufferAdd(head: Buffer, body: Buffer): boolean { - const wasEmpty = this._totalLength === 0; - this._data.push(head, body); - this._totalLength += head.length + body.length; - return wasEmpty; - } - - private _bufferTake(): Buffer { - const ret = Buffer.concat(this._data, this._totalLength); - this._data.length = 0; - this._totalLength = 0; - return ret; - } - - private _writeSoon(header: Buffer, data: Buffer): void { - if (this._bufferAdd(header, data)) { - setImmediate(() => { - this._writeNow(); - }); - } - } - - private _writeNow(): void { - if (this._totalLength === 0) { - return; - } - // return early if socket has been destroyed in the meantime - if (this._socket.destroyed) { - return; - } - // we ignore the returned value from `write` because we would have to cached the data - // anyways and nodejs is already doing that for us: - // > https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback - // > However, the false return value is only advisory and the writable stream will unconditionally - // > accept and buffer chunk even if it has not not been allowed to drain. - this._socket.write(this._bufferTake()); - } -} - -/** - * A message has the following format: - * ``` - * /-------------------------------|------\ - * | HEADER | | - * |-------------------------------| DATA | - * | TYPE | ID | ACK | DATA_LENGTH | | - * \-------------------------------|------/ - * ``` - * The header is 9 bytes and consists of: - * - TYPE is 1 byte (ProtocolMessageType) - the message type - * - ID is 4 bytes (u32be) - the message id (can be 0 to indicate to be ignored) - * - ACK is 4 bytes (u32be) - the acknowledged message id (can be 0 to indicate to be ignored) - * - DATA_LENGTH is 4 bytes (u32be) - the length in bytes of DATA - * - * Only Regular messages are counted, other messages are not counted, nor acknowledged. - */ -export class Protocol implements IDisposable, IMessagePassingProtocol { - - private _socket: Socket; - private _socketWriter: ProtocolWriter; - private _socketReader: ProtocolReader; - - private _socketCloseListener: () => void; - - private _onMessage = new Emitter(); - readonly onMessage: Event = this._onMessage.event; - - private _onClose = new Emitter(); - readonly onClose: Event = this._onClose.event; - - constructor(socket: Socket) { - this._socket = socket; - this._socketWriter = new ProtocolWriter(this._socket, 0); - this._socketReader = new ProtocolReader(this._socket); - - this._socketReader.onMessage((msg) => { - if (msg.type === ProtocolMessageType.Regular) { - this._onMessage.fire(msg.data); - } - }); - - this._socketCloseListener = () => { - this._onClose.fire(); - }; - this._socket.once('close', this._socketCloseListener); - } - - dispose(): void { - this._socketWriter.dispose(); - this._socketReader.dispose(); - this._socket.removeListener('close', this._socketCloseListener); - } - - getSocket(): Socket { - return this._socket; - } - - send(buffer: Buffer): void { - this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.Regular, 0, 0, buffer)); - } -} - export class Server extends IPCServer { private static toClientConnectionEvent(server: NetServer): Event { const onConnection = Event.fromNodeEventEmitter(server, 'connection'); return Event.map(onConnection, socket => ({ - protocol: new Protocol(socket), + protocol: new Protocol(new NodeSocket(socket)), onDidClientDisconnect: Event.once(Event.fromNodeEventEmitter(socket, 'close')) })); } @@ -422,26 +98,6 @@ export class Server extends IPCServer { } } -export class Client extends IPCClient { - - static fromSocket(socket: Socket, id: TContext): Client { - return new Client(new Protocol(socket), id); - } - - get onClose(): Event { return this.protocol.onClose; } - - constructor(private protocol: Protocol | PersistentProtocol, id: TContext) { - super(protocol, id); - } - - dispose(): void { - super.dispose(); - const socket = this.protocol.getSocket(); - this.protocol.dispose(); - socket.end(); - } -} - export function serve(port: number): Promise; export function serve(namedPipe: string): Promise; export function serve(hook: any): Promise { @@ -463,440 +119,9 @@ export function connect(hook: any, clientId: string): Promise { return new Promise((c, e) => { const socket = createConnection(hook, () => { socket.removeListener('error', e); - c(Client.fromSocket(socket, clientId)); + c(Client.fromSocket(new NodeSocket(socket), clientId)); }); socket.once('error', e); }); } - -/** - * Will ensure no messages are lost if there are no event listeners. - */ -function createBufferedEvent(source: Event): Event { - let emitter: Emitter; - let hasListeners = false; - let isDeliveringMessages = false; - let bufferedMessages: T[] = []; - - const deliverMessages = () => { - if (isDeliveringMessages) { - return; - } - isDeliveringMessages = true; - while (hasListeners && bufferedMessages.length > 0) { - emitter.fire(bufferedMessages.shift()!); - } - isDeliveringMessages = false; - }; - - source((e: T) => { - bufferedMessages.push(e); - deliverMessages(); - }); - - emitter = new Emitter({ - onFirstListenerAdd: () => { - hasListeners = true; - // it is important to deliver these messages after this call, but before - // other messages have a chance to be received (to guarantee in order delivery) - // that's why we're using here nextTick and not other types of timeouts - process.nextTick(deliverMessages); - }, - onLastListenerRemove: () => { - hasListeners = false; - } - }); - - return emitter.event; -} - -class QueueElement { - public readonly data: T; - public next: QueueElement | null; - - constructor(data: T) { - this.data = data; - this.next = null; - } -} - -class Queue { - - private _first: QueueElement | null; - private _last: QueueElement | null; - - constructor() { - this._first = null; - this._last = null; - } - - public peek(): T | null { - if (!this._first) { - return null; - } - return this._first.data; - } - - public toArray(): T[] { - let result: T[] = [], resultLen = 0; - let it = this._first; - while (it) { - result[resultLen++] = it.data; - it = it.next; - } - return result; - } - - public pop(): void { - if (!this._first) { - return; - } - if (this._first === this._last) { - this._first = null; - this._last = null; - return; - } - this._first = this._first.next; - } - - public push(item: T): void { - const element = new QueueElement(item); - if (!this._first) { - this._first = element; - this._last = element; - return; - } - this._last!.next = element; - this._last = element; - } -} - -/** - * Same as Protocol, but will actually track messages and acks. - * Moreover, it will ensure no messages are lost if there are no event listeners. - */ -export class PersistentProtocol { - - private _logFile: number; - private _isReconnecting: boolean; - - private _outgoingUnackMsg: Queue; - private _outgoingMsgId: number; - private _outgoingAckId: number; - private _outgoingAckTimeout: NodeJS.Timeout | null; - - private _incomingMsgId: number; - private _incomingAckId: number; - private _incomingMsgLastTime: number; - private _incomingAckTimeout: NodeJS.Timeout | null; - - private _outgoingKeepAliveTimeout: NodeJS.Timeout | null; - private _incomingKeepAliveTimeout: NodeJS.Timeout | null; - - private _socket: Socket; - private _socketWriter: ProtocolWriter; - private _socketReader: ProtocolReader; - private _socketReaderListener: IDisposable; - - private readonly _socketCloseListener: () => void; - private readonly _socketEndListener: () => void; - private readonly _socketErrorListener: (err: any) => void; - - private _onControlMessage = new Emitter(); - readonly onControlMessage: Event = createBufferedEvent(this._onControlMessage.event); - - private _onMessage = new Emitter(); - readonly onMessage: Event = createBufferedEvent(this._onMessage.event); - - private _onClose = new Emitter(); - readonly onClose: Event = createBufferedEvent(this._onClose.event); - - private _onSocketClose = new Emitter(); - readonly onSocketClose: Event = createBufferedEvent(this._onSocketClose.event); - - private _onSocketTimeout = new Emitter(); - readonly onSocketTimeout: Event = createBufferedEvent(this._onSocketTimeout.event); - - public get unacknowledgedCount(): number { - return this._outgoingMsgId - this._outgoingAckId; - } - - constructor(socket: Socket, initialChunk: Buffer | null = null, logFileName: string | null = null) { - this._logFile = 0; - this._isReconnecting = false; - if (logFileName) { - console.log(`PersistentProtocol log file: ${logFileName}`); - this._logFile = fs.openSync(logFileName, 'a'); - } - this._outgoingUnackMsg = new Queue(); - this._outgoingMsgId = 0; - this._outgoingAckId = 0; - this._outgoingAckTimeout = null; - - this._incomingMsgId = 0; - this._incomingAckId = 0; - this._incomingMsgLastTime = 0; - this._incomingAckTimeout = null; - - this._outgoingKeepAliveTimeout = null; - this._incomingKeepAliveTimeout = null; - - this._socketCloseListener = () => { - console.log(`socket triggered close event!`); - this._onSocketClose.fire(); - }; - this._socketEndListener = () => { - // received FIN - this._onClose.fire(); - }; - this._socketErrorListener = (err) => { - console.log(`socket had an error: `, err); - }; - - this._socket = socket; - this._socketWriter = new ProtocolWriter(this._socket, this._logFile); - this._socketReader = new ProtocolReader(this._socket); - this._socketReaderListener = this._socketReader.onMessage(msg => this._receiveMessage(msg)); - this._socket.on('close', this._socketCloseListener); - this._socket.on('end', this._socketEndListener); - this._socket.on('error', this._socketErrorListener); - if (initialChunk) { - this._socketReader.acceptChunk(initialChunk); - } - - this._sendKeepAliveCheck(); - this._recvKeepAliveCheck(); - } - - dispose(): void { - if (this._outgoingAckTimeout) { - clearTimeout(this._outgoingAckTimeout); - this._outgoingAckTimeout = null; - } - if (this._incomingAckTimeout) { - clearTimeout(this._incomingAckTimeout); - this._incomingAckTimeout = null; - } - if (this._outgoingKeepAliveTimeout) { - clearTimeout(this._outgoingKeepAliveTimeout); - this._outgoingKeepAliveTimeout = null; - } - if (this._incomingKeepAliveTimeout) { - clearTimeout(this._incomingKeepAliveTimeout); - this._incomingKeepAliveTimeout = null; - } - if (this._logFile) { - fs.closeSync(this._logFile); - } - this._socketWriter.dispose(); - this._socketReader.dispose(); - this._socketReaderListener.dispose(); - this._socket.removeListener('close', this._socketCloseListener); - this._socket.removeListener('end', this._socketEndListener); - this._socket.removeListener('error', this._socketErrorListener); - } - - private _sendKeepAliveCheck(): void { - if (this._outgoingKeepAliveTimeout) { - // there will be a check in the near future - return; - } - - const timeSinceLastOutgoingMsg = Date.now() - this._socketWriter.lastWriteTime; - if (timeSinceLastOutgoingMsg >= ProtocolConstants.KeepAliveTime) { - // sufficient time has passed since last message was written, - // and no message from our side needed to be sent in the meantime, - // so we will send a message containing only a keep alive. - const msg = new ProtocolMessage(ProtocolMessageType.KeepAlive, 0, 0, EMPTY_BUFFER); - this._socketWriter.write(msg); - this._sendKeepAliveCheck(); - return; - } - - this._outgoingKeepAliveTimeout = setTimeout(() => { - this._outgoingKeepAliveTimeout = null; - this._sendKeepAliveCheck(); - }, ProtocolConstants.KeepAliveTime - timeSinceLastOutgoingMsg + 5); - } - - private _recvKeepAliveCheck(): void { - if (this._incomingKeepAliveTimeout) { - // there will be a check in the near future - return; - } - - const timeSinceLastIncomingMsg = Date.now() - this._socketReader.lastReadTime; - if (timeSinceLastIncomingMsg >= ProtocolConstants.KeepAliveTimeoutTime) { - // Trash the socket - this._onSocketTimeout.fire(undefined); - return; - } - - this._incomingKeepAliveTimeout = setTimeout(() => { - this._incomingKeepAliveTimeout = null; - this._recvKeepAliveCheck(); - }, ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg + 5); - } - - public getSocket(): Socket { - return this._socket; - } - - public beginAcceptReconnection(socket: Socket, initialDataChunk: Buffer | null): void { - this._isReconnecting = true; - - this._socketWriter.dispose(); - this._socketReader.dispose(); - this._socketReaderListener.dispose(); - this._socket.removeListener('close', this._socketCloseListener); - this._socket.removeListener('end', this._socketEndListener); - this._socket.removeListener('error', this._socketErrorListener); - - this._socket = socket; - - this._socketWriter = new ProtocolWriter(this._socket, this._logFile); - this._socketReader = new ProtocolReader(this._socket); - this._socketReaderListener = this._socketReader.onMessage(msg => this._receiveMessage(msg)); - this._socketReader.acceptChunk(initialDataChunk); - this._socket.on('close', this._socketCloseListener); - this._socket.on('end', this._socketEndListener); - this._socket.on('error', this._socketErrorListener); - } - - public endAcceptReconnection(): void { - this._isReconnecting = false; - - // Send again all unacknowledged messages - const toSend = this._outgoingUnackMsg.toArray(); - for (let i = 0, len = toSend.length; i < len; i++) { - this._socketWriter.write(toSend[i]); - } - this._recvAckCheck(); - - this._sendKeepAliveCheck(); - this._recvKeepAliveCheck(); - } - - private _receiveMessage(msg: ProtocolMessage): void { - if (this._logFile) { - log(this._logFile, `recv-${ProtocolMessageTypeToString(msg.type)}-${msg.id}-${msg.ack}-`, msg.data); - } - if (msg.ack > this._outgoingAckId) { - this._outgoingAckId = msg.ack; - do { - const first = this._outgoingUnackMsg.peek(); - if (first && first.id <= msg.ack) { - // this message has been confirmed, remove it - this._outgoingUnackMsg.pop(); - } else { - break; - } - } while (true); - } - - if (msg.type === ProtocolMessageType.Regular) { - if (msg.id > this._incomingMsgId) { - if (msg.id !== this._incomingMsgId + 1) { - console.error(`PROTOCOL CORRUPTION, LAST SAW MSG ${this._incomingMsgId} AND HAVE NOW RECEIVED MSG ${msg.id}`); - } - this._incomingMsgId = msg.id; - this._incomingMsgLastTime = Date.now(); - this._sendAckCheck(); - this._onMessage.fire(msg.data); - } - } else if (msg.type === ProtocolMessageType.Control) { - this._onControlMessage.fire(msg.data); - } - } - - readEntireBuffer(): Buffer { - return this._socketReader.readEntireBuffer(); - } - - flush(): void { - this._socketWriter.flush(); - } - - send(buffer: Buffer): void { - const myId = ++this._outgoingMsgId; - this._incomingAckId = this._incomingMsgId; - const msg = new ProtocolMessage(ProtocolMessageType.Regular, myId, this._incomingAckId, buffer); - this._outgoingUnackMsg.push(msg); - if (!this._isReconnecting) { - this._socketWriter.write(msg); - this._recvAckCheck(); - } - } - - /** - * Send a message which will not be part of the regular acknowledge flow. - * Use this for early control messages which are repeated in case of reconnection. - */ - sendControl(buffer: Buffer): void { - const msg = new ProtocolMessage(ProtocolMessageType.Control, 0, 0, buffer); - this._socketWriter.write(msg); - } - - private _sendAckCheck(): void { - if (this._incomingMsgId <= this._incomingAckId) { - // nothink to acknowledge - return; - } - - if (this._incomingAckTimeout) { - // there will be a check in the near future - return; - } - - const timeSinceLastIncomingMsg = Date.now() - this._incomingMsgLastTime; - if (timeSinceLastIncomingMsg >= ProtocolConstants.AcknowledgeTime) { - // sufficient time has passed since this message has been received, - // and no message from our side needed to be sent in the meantime, - // so we will send a message containing only an ack. - this._sendAck(); - return; - } - - this._incomingAckTimeout = setTimeout(() => { - this._incomingAckTimeout = null; - this._sendAckCheck(); - }, ProtocolConstants.AcknowledgeTime - timeSinceLastIncomingMsg + 5); - } - - private _recvAckCheck(): void { - if (this._outgoingMsgId <= this._outgoingAckId) { - // everything has been acknowledged - return; - } - - if (this._outgoingAckTimeout) { - // there will be a check in the near future - return; - } - - const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek()!; - const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime; - if (timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.AcknowledgeTimeoutTime) { - // Trash the socket - this._onSocketTimeout.fire(undefined); - return; - } - - this._outgoingAckTimeout = setTimeout(() => { - this._outgoingAckTimeout = null; - this._recvAckCheck(); - }, ProtocolConstants.AcknowledgeTimeoutTime - timeSinceOldestUnacknowledgedMsg + 5); - } - - private _sendAck(): void { - if (this._incomingMsgId <= this._incomingAckId) { - // nothink to acknowledge - return; - } - - this._incomingAckId = this._incomingMsgId; - const msg = new ProtocolMessage(ProtocolMessageType.Ack, 0, this._incomingAckId, EMPTY_BUFFER); - this._socketWriter.write(msg); - } -} diff --git a/src/vs/base/parts/ipc/node/ipc.ts b/src/vs/base/parts/ipc/node/ipc.ts deleted file mode 100644 index dcf7d07058..0000000000 --- a/src/vs/base/parts/ipc/node/ipc.ts +++ /dev/null @@ -1,733 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter, Relay } from 'vs/base/common/event'; -import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import * as errors from 'vs/base/common/errors'; -import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; - -export const enum RequestType { - Promise = 100, - PromiseCancel = 101, - EventListen = 102, - EventDispose = 103 -} - -type IRawPromiseRequest = { type: RequestType.Promise; id: number; channelName: string; name: string; arg: any; }; -type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel, id: number }; -type IRawEventListenRequest = { type: RequestType.EventListen; id: number; channelName: string; name: string; arg: any; }; -type IRawEventDisposeRequest = { type: RequestType.EventDispose, id: number }; -type IRawRequest = IRawPromiseRequest | IRawPromiseCancelRequest | IRawEventListenRequest | IRawEventDisposeRequest; - -export const enum ResponseType { - Initialize = 200, - PromiseSuccess = 201, - PromiseError = 202, - PromiseErrorObj = 203, - EventFire = 204 -} - -type IRawInitializeResponse = { type: ResponseType.Initialize }; -type IRawPromiseSuccessResponse = { type: ResponseType.PromiseSuccess; id: number; data: any }; -type IRawPromiseErrorResponse = { type: ResponseType.PromiseError; id: number; data: { message: string, name: string, stack: string[] | undefined } }; -type IRawPromiseErrorObjResponse = { type: ResponseType.PromiseErrorObj; id: number; data: any }; -type IRawEventFireResponse = { type: ResponseType.EventFire; id: number; data: any }; -type IRawResponse = IRawInitializeResponse | IRawPromiseSuccessResponse | IRawPromiseErrorResponse | IRawPromiseErrorObjResponse | IRawEventFireResponse; - -interface IHandler { - (response: IRawResponse): void; -} - -export interface IMessagePassingProtocol { - send(buffer: Buffer): void; - onMessage: Event; -} - -enum State { - Uninitialized, - Idle -} - -/** - * An `IChannelServer` hosts a collection of channels. You are - * able to register channels onto it, provided a channel name. - */ -export interface IChannelServer { - registerChannel(channelName: string, channel: IServerChannel): void; -} - -/** - * An `IChannelClient` has access to a collection of channels. You - * are able to get those channels, given their channel name. - */ -export interface IChannelClient { - getChannel(channelName: string): T; -} - -export interface Client { - readonly ctx: TContext; -} - -export interface IConnectionHub { - readonly connections: Connection[]; - readonly onDidChangeConnections: Event>; -} - -/** - * An `IClientRouter` is responsible for routing calls to specific - * channels, in scenarios in which there are multiple possible - * channels (each from a separate client) to pick from. - */ -export interface IClientRouter { - routeCall(hub: IConnectionHub, command: string, arg?: any, cancellationToken?: CancellationToken): Promise>; - routeEvent(hub: IConnectionHub, event: string, arg?: any): Promise>; -} - -/** - * Similar to the `IChannelClient`, you can get channels from this - * collection of channels. The difference being that in the - * `IRoutingChannelClient`, there are multiple clients providing - * the same channel. You'll need to pass in an `IClientRouter` in - * order to pick the right one. - */ -export interface IRoutingChannelClient { - getChannel(channelName: string, router: IClientRouter): T; -} - -interface IReader { - read(bytes: number): Buffer; -} - -interface IWriter { - write(buffer: Buffer): void; -} - -class BufferReader implements IReader { - - private pos = 0; - - constructor(private buffer: Buffer) { } - - read(bytes: number): Buffer { - const result = this.buffer.slice(this.pos, this.pos + bytes); - this.pos += result.length; - return result; - } -} - -class BufferWriter implements IWriter { - - private buffers: Buffer[] = []; - - get buffer(): Buffer { - return Buffer.concat(this.buffers); - } - - write(buffer: Buffer): void { - this.buffers.push(buffer); - } -} - -enum DataType { - Undefined = 0, - String = 1, - Buffer = 2, - Array = 3, - Object = 4 -} - -function createSizeBuffer(size: number): Buffer { - const result = Buffer.allocUnsafe(4); - result.writeUInt32BE(size, 0); - return result; -} - -function readSizeBuffer(reader: IReader): number { - return reader.read(4).readUInt32BE(0); -} - -const BufferPresets = { - Undefined: Buffer.alloc(1, DataType.Undefined), - String: Buffer.alloc(1, DataType.String), - Buffer: Buffer.alloc(1, DataType.Buffer), - Array: Buffer.alloc(1, DataType.Array), - Object: Buffer.alloc(1, DataType.Object) -}; - -function serialize(writer: IWriter, data: any): void { - if (typeof data === 'undefined') { - writer.write(BufferPresets.Undefined); - } else if (typeof data === 'string') { - const buffer = Buffer.from(data); - writer.write(BufferPresets.String); - writer.write(createSizeBuffer(buffer.length)); - writer.write(buffer); - } else if (Buffer.isBuffer(data)) { - writer.write(BufferPresets.Buffer); - writer.write(createSizeBuffer(data.length)); - writer.write(data); - } else if (Array.isArray(data)) { - writer.write(BufferPresets.Array); - writer.write(createSizeBuffer(data.length)); - - for (const el of data) { - serialize(writer, el); - } - } else { - const buffer = Buffer.from(JSON.stringify(data)); - writer.write(BufferPresets.Object); - writer.write(createSizeBuffer(buffer.length)); - writer.write(buffer); - } -} - -function deserialize(reader: IReader): any { - const type = reader.read(1).readUInt8(0); - - switch (type) { - case DataType.Undefined: return undefined; - case DataType.String: return reader.read(readSizeBuffer(reader)).toString(); - case DataType.Buffer: return reader.read(readSizeBuffer(reader)); - case DataType.Array: { - const length = readSizeBuffer(reader); - const result: any[] = []; - - for (let i = 0; i < length; i++) { - result.push(deserialize(reader)); - } - - return result; - } - case DataType.Object: return JSON.parse(reader.read(readSizeBuffer(reader)).toString()); - } -} - -export class ChannelServer implements IChannelServer, IDisposable { - - private channels = new Map>(); - private activeRequests = new Map(); - private protocolListener: IDisposable | null; - - constructor(private protocol: IMessagePassingProtocol, private ctx: TContext) { - this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg)); - this.sendResponse({ type: ResponseType.Initialize }); - } - - registerChannel(channelName: string, channel: IServerChannel): void { - this.channels.set(channelName, channel); - } - - private sendResponse(response: IRawResponse): void { - switch (response.type) { - case ResponseType.Initialize: - return this.send([response.type]); - - case ResponseType.PromiseSuccess: - case ResponseType.PromiseError: - case ResponseType.EventFire: - case ResponseType.PromiseErrorObj: - return this.send([response.type, response.id], response.data); - } - } - - private send(header: any, body: any = undefined): void { - const writer = new BufferWriter(); - serialize(writer, header); - serialize(writer, body); - this.sendBuffer(writer.buffer); - } - - private sendBuffer(message: Buffer): void { - try { - this.protocol.send(message); - } catch (err) { - // noop - } - } - - private onRawMessage(message: Buffer): void { - const reader = new BufferReader(message); - const header = deserialize(reader); - const body = deserialize(reader); - const type = header[0] as RequestType; - - switch (type) { - case RequestType.Promise: - return this.onPromise({ type, id: header[1], channelName: header[2], name: header[3], arg: body }); - case RequestType.EventListen: - return this.onEventListen({ type, id: header[1], channelName: header[2], name: header[3], arg: body }); - case RequestType.PromiseCancel: - return this.disposeActiveRequest({ type, id: header[1] }); - case RequestType.EventDispose: - return this.disposeActiveRequest({ type, id: header[1] }); - } - } - - private onPromise(request: IRawPromiseRequest): void { - const channel = this.channels.get(request.channelName); - if (!channel) { - throw new Error('Unknown channel'); - } - const cancellationTokenSource = new CancellationTokenSource(); - let promise: Promise; - - try { - promise = channel.call(this.ctx, request.name, request.arg, cancellationTokenSource.token); - } catch (err) { - promise = Promise.reject(err); - } - - const id = request.id; - - promise.then(data => { - this.sendResponse({ id, data, type: ResponseType.PromiseSuccess }); - this.activeRequests.delete(request.id); - }, err => { - if (err instanceof Error) { - this.sendResponse({ - id, data: { - message: err.message, - name: err.name, - stack: err.stack ? (err.stack.split ? err.stack.split('\n') : err.stack) : undefined - }, type: ResponseType.PromiseError - }); - } else { - this.sendResponse({ id, data: err, type: ResponseType.PromiseErrorObj }); - } - - this.activeRequests.delete(request.id); - }); - - const disposable = toDisposable(() => cancellationTokenSource.cancel()); - this.activeRequests.set(request.id, disposable); - } - - private onEventListen(request: IRawEventListenRequest): void { - const channel = this.channels.get(request.channelName); - if (!channel) { - throw new Error('Unknown channel'); - } - - const id = request.id; - const event = channel.listen(this.ctx, request.name, request.arg); - const disposable = event(data => this.sendResponse({ id, data, type: ResponseType.EventFire })); - - this.activeRequests.set(request.id, disposable); - } - - private disposeActiveRequest(request: IRawRequest): void { - const disposable = this.activeRequests.get(request.id); - - if (disposable) { - disposable.dispose(); - this.activeRequests.delete(request.id); - } - } - - public dispose(): void { - if (this.protocolListener) { - this.protocolListener.dispose(); - this.protocolListener = null; - } - this.activeRequests.forEach(d => d.dispose()); - this.activeRequests.clear(); - } -} - -export class ChannelClient implements IChannelClient, IDisposable { - - private state: State = State.Uninitialized; - private activeRequests = new Set(); - private handlers = new Map(); - private lastRequestId: number = 0; - private protocolListener: IDisposable | null; - - private _onDidInitialize = new Emitter(); - readonly onDidInitialize = this._onDidInitialize.event; - - constructor(private protocol: IMessagePassingProtocol) { - this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg)); - } - - getChannel(channelName: string): T { - const that = this; - - return { - call(command: string, arg?: any, cancellationToken?: CancellationToken) { - return that.requestPromise(channelName, command, arg, cancellationToken); - }, - listen(event: string, arg: any) { - return that.requestEvent(channelName, event, arg); - } - } as T; - } - - private requestPromise(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise { - const id = this.lastRequestId++; - const type = RequestType.Promise; - const request: IRawRequest = { id, type, channelName, name, arg }; - - if (cancellationToken.isCancellationRequested) { - return Promise.reject(errors.canceled()); - } - - let disposable: IDisposable; - - const result = new Promise((c, e) => { - if (cancellationToken.isCancellationRequested) { - return e(errors.canceled()); - } - - let uninitializedPromise: CancelablePromise | null = createCancelablePromise(_ => this.whenInitialized()); - uninitializedPromise.then(() => { - uninitializedPromise = null; - - const handler: IHandler = response => { - switch (response.type) { - case ResponseType.PromiseSuccess: - this.handlers.delete(id); - c(response.data); - break; - - case ResponseType.PromiseError: - this.handlers.delete(id); - const error = new Error(response.data.message); - (error).stack = response.data.stack; - error.name = response.data.name; - e(error); - break; - - case ResponseType.PromiseErrorObj: - this.handlers.delete(id); - e(response.data); - break; - } - }; - - this.handlers.set(id, handler); - this.sendRequest(request); - }); - - const cancel = () => { - if (uninitializedPromise) { - uninitializedPromise.cancel(); - uninitializedPromise = null; - } else { - this.sendRequest({ id, type: RequestType.PromiseCancel }); - } - - e(errors.canceled()); - }; - - const cancellationTokenListener = cancellationToken.onCancellationRequested(cancel); - disposable = combinedDisposable([toDisposable(cancel), cancellationTokenListener]); - this.activeRequests.add(disposable); - }); - - return result.finally(() => this.activeRequests.delete(disposable)); - } - - private requestEvent(channelName: string, name: string, arg?: any): Event { - const id = this.lastRequestId++; - const type = RequestType.EventListen; - const request: IRawRequest = { id, type, channelName, name, arg }; - - let uninitializedPromise: CancelablePromise | null = null; - - const emitter = new Emitter({ - onFirstListenerAdd: () => { - uninitializedPromise = createCancelablePromise(_ => this.whenInitialized()); - uninitializedPromise.then(() => { - uninitializedPromise = null; - this.activeRequests.add(emitter); - this.sendRequest(request); - }); - }, - onLastListenerRemove: () => { - if (uninitializedPromise) { - uninitializedPromise.cancel(); - uninitializedPromise = null; - } else { - this.activeRequests.delete(emitter); - this.sendRequest({ id, type: RequestType.EventDispose }); - } - } - }); - - const handler: IHandler = (res: IRawEventFireResponse) => emitter.fire(res.data); - this.handlers.set(id, handler); - - return emitter.event; - } - - private sendRequest(request: IRawRequest): void { - switch (request.type) { - case RequestType.Promise: - case RequestType.EventListen: - return this.send([request.type, request.id, request.channelName, request.name], request.arg); - - case RequestType.PromiseCancel: - case RequestType.EventDispose: - return this.send([request.type, request.id]); - } - } - - private send(header: any, body: any = undefined): void { - const writer = new BufferWriter(); - serialize(writer, header); - serialize(writer, body); - this.sendBuffer(writer.buffer); - } - - private sendBuffer(message: Buffer): void { - try { - this.protocol.send(message); - } catch (err) { - // noop - } - } - - private onBuffer(message: Buffer): void { - const reader = new BufferReader(message); - const header = deserialize(reader); - const body = deserialize(reader); - const type: ResponseType = header[0]; - - switch (type) { - case ResponseType.Initialize: - return this.onResponse({ type: header[0] }); - - case ResponseType.PromiseSuccess: - case ResponseType.PromiseError: - case ResponseType.EventFire: - case ResponseType.PromiseErrorObj: - return this.onResponse({ type: header[0], id: header[1], data: body }); - } - } - - private onResponse(response: IRawResponse): void { - if (response.type === ResponseType.Initialize) { - this.state = State.Idle; - this._onDidInitialize.fire(); - return; - } - - const handler = this.handlers.get(response.id); - - if (handler) { - handler(response); - } - } - - private whenInitialized(): Promise { - if (this.state === State.Idle) { - return Promise.resolve(); - } else { - return Event.toPromise(this.onDidInitialize); - } - } - - dispose(): void { - if (this.protocolListener) { - this.protocolListener.dispose(); - this.protocolListener = null; - } - this.activeRequests.forEach(p => p.dispose()); - this.activeRequests.clear(); - } -} - -export interface ClientConnectionEvent { - protocol: IMessagePassingProtocol; - onDidClientDisconnect: Event; -} - -interface Connection extends Client { - readonly channelClient: ChannelClient; -} - -/** - * An `IPCServer` is both a channel server and a routing channel - * client. - * - * As the owner of a protocol, you should extend both this - * and the `IPCClient` classes to get IPC implementations - * for your protocol. - */ -export class IPCServer implements IChannelServer, IRoutingChannelClient, IConnectionHub, IDisposable { - - private channels = new Map>(); - private _connections = new Set>(); - - private _onDidChangeConnections = new Emitter>(); - readonly onDidChangeConnections: Event> = this._onDidChangeConnections.event; - - get connections(): Connection[] { - const result: Connection[] = []; - this._connections.forEach(ctx => result.push(ctx)); - return result; - } - - constructor(onDidClientConnect: Event) { - onDidClientConnect(({ protocol, onDidClientDisconnect }) => { - const onFirstMessage = Event.once(protocol.onMessage); - - onFirstMessage(msg => { - const reader = new BufferReader(msg); - const ctx = deserialize(reader) as TContext; - - const channelServer = new ChannelServer(protocol, ctx); - const channelClient = new ChannelClient(protocol); - - this.channels.forEach((channel, name) => channelServer.registerChannel(name, channel)); - - const connection: Connection = { channelClient, ctx }; - this._connections.add(connection); - this._onDidChangeConnections.fire(connection); - - onDidClientDisconnect(() => { - channelServer.dispose(); - channelClient.dispose(); - this._connections.delete(connection); - }); - }); - }); - } - - getChannel(channelName: string, router: IClientRouter): T { - const that = this; - - return { - call(command: string, arg?: any, cancellationToken?: CancellationToken) { - const channelPromise = router.routeCall(that, command, arg) - .then(connection => (connection as Connection).channelClient.getChannel(channelName)); - - return getDelayedChannel(channelPromise) - .call(command, arg, cancellationToken); - }, - listen(event: string, arg: any) { - const channelPromise = router.routeEvent(that, event, arg) - .then(connection => (connection as Connection).channelClient.getChannel(channelName)); - - return getDelayedChannel(channelPromise) - .listen(event, arg); - } - } as T; - } - - registerChannel(channelName: string, channel: IServerChannel): void { - this.channels.set(channelName, channel); - } - - dispose(): void { - this.channels.clear(); - this._connections.clear(); - this._onDidChangeConnections.dispose(); - } -} - -/** - * An `IPCClient` is both a channel client and a channel server. - * - * As the owner of a protocol, you should extend both this - * and the `IPCClient` classes to get IPC implementations - * for your protocol. - */ -export class IPCClient implements IChannelClient, IChannelServer, IDisposable { - - private channelClient: ChannelClient; - private channelServer: ChannelServer; - - constructor(protocol: IMessagePassingProtocol, ctx: TContext) { - const writer = new BufferWriter(); - serialize(writer, ctx); - protocol.send(writer.buffer); - - this.channelClient = new ChannelClient(protocol); - this.channelServer = new ChannelServer(protocol, ctx); - } - - getChannel(channelName: string): T { - return this.channelClient.getChannel(channelName) as T; - } - - registerChannel(channelName: string, channel: IServerChannel): void { - this.channelServer.registerChannel(channelName, channel); - } - - dispose(): void { - this.channelClient.dispose(); - this.channelServer.dispose(); - } -} - -export function getDelayedChannel(promise: Promise): T { - return { - call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise { - return promise.then(c => c.call(command, arg, cancellationToken)); - }, - - listen(event: string, arg?: any): Event { - const relay = new Relay(); - promise.then(c => relay.input = c.listen(event, arg)); - return relay.event; - } - } as T; -} - -export function getNextTickChannel(channel: T): T { - let didTick = false; - - return { - call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise { - if (didTick) { - return channel.call(command, arg, cancellationToken); - } - - return timeout(0) - .then(() => didTick = true) - .then(() => channel.call(command, arg, cancellationToken)); - }, - listen(event: string, arg?: any): Event { - if (didTick) { - return channel.listen(event, arg); - } - - const relay = new Relay(); - - timeout(0) - .then(() => didTick = true) - .then(() => relay.input = channel.listen(event, arg)); - - return relay.event; - } - } as T; -} - -export class StaticRouter implements IClientRouter { - - constructor(private fn: (ctx: TContext) => boolean | Promise) { } - - routeCall(hub: IConnectionHub): Promise> { - return this.route(hub); - } - - routeEvent(hub: IConnectionHub): Promise> { - return this.route(hub); - } - - private async route(hub: IConnectionHub): Promise> { - for (const connection of hub.connections) { - if (await Promise.resolve(this.fn(connection.ctx))) { - return Promise.resolve(connection); - } - } - - await Event.toPromise(hub.onDidChangeConnections); - return await this.route(hub); - } -} diff --git a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts index 19c99ddc28..295f61af5a 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts @@ -6,12 +6,14 @@ import * as assert from 'assert'; import { Socket } from 'net'; import { EventEmitter } from 'events'; -import { Protocol, PersistentProtocol } from 'vs/base/parts/ipc/node/ipc.net'; +import { Protocol, PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { VSBuffer } from 'vs/base/common/buffer'; class MessageStream { - private _currentComplete: ((data: Buffer) => void) | null; - private _messages: Buffer[]; + private _currentComplete: ((data: VSBuffer) => void) | null; + private _messages: VSBuffer[]; constructor(x: Protocol | PersistentProtocol) { this._currentComplete = null; @@ -36,8 +38,8 @@ class MessageStream { complete(msg); } - public waitForOne(): Promise { - return new Promise((complete) => { + public waitForOne(): Promise { + return new Promise((complete) => { this._currentComplete = complete; this._trigger(); }); @@ -53,6 +55,9 @@ class EtherStream extends EventEmitter { } write(data: Buffer, cb?: Function): boolean { + if (!Buffer.isBuffer(data)) { + throw new Error(`Invalid data`); + } this._ether.write(this._name, data); return true; } @@ -122,26 +127,26 @@ suite('IPC, Socket Protocol', () => { test('read/write', async () => { - const a = new Protocol(ether.a); - const b = new Protocol(ether.b); + const a = new Protocol(new NodeSocket(ether.a)); + const b = new Protocol(new NodeSocket(ether.b)); const bMessages = new MessageStream(b); - a.send(Buffer.from('foobarfarboo')); + a.send(VSBuffer.fromString('foobarfarboo')); const msg1 = await bMessages.waitForOne(); assert.equal(msg1.toString(), 'foobarfarboo'); - const buffer = Buffer.allocUnsafe(1); - buffer.writeInt8(123, 0); + const buffer = VSBuffer.alloc(1); + buffer.writeUint8(123, 0); a.send(buffer); const msg2 = await bMessages.waitForOne(); - assert.equal(msg2.readInt8(0), 123); + assert.equal(msg2.readUint8(0), 123); }); test('read/write, object data', async () => { - const a = new Protocol(ether.a); - const b = new Protocol(ether.b); + const a = new Protocol(new NodeSocket(ether.a)); + const b = new Protocol(new NodeSocket(ether.b)); const bMessages = new MessageStream(b); const data = { @@ -151,7 +156,7 @@ suite('IPC, Socket Protocol', () => { data: 'Hello World'.split('') }; - a.send(Buffer.from(JSON.stringify(data))); + a.send(VSBuffer.fromString(JSON.stringify(data))); const msg = await bMessages.waitForOne(); assert.deepEqual(JSON.parse(msg.toString()), data); }); @@ -166,20 +171,20 @@ suite('PersistentProtocol reconnection', () => { }); test('acks get piggybacked with messages', async () => { - const a = new PersistentProtocol(ether.a); + const a = new PersistentProtocol(new NodeSocket(ether.a)); const aMessages = new MessageStream(a); - const b = new PersistentProtocol(ether.b); + const b = new PersistentProtocol(new NodeSocket(ether.b)); const bMessages = new MessageStream(b); - a.send(Buffer.from('a1')); + a.send(VSBuffer.fromString('a1')); assert.equal(a.unacknowledgedCount, 1); assert.equal(b.unacknowledgedCount, 0); - a.send(Buffer.from('a2')); + a.send(VSBuffer.fromString('a2')); assert.equal(a.unacknowledgedCount, 2); assert.equal(b.unacknowledgedCount, 0); - a.send(Buffer.from('a3')); + a.send(VSBuffer.fromString('a3')); assert.equal(a.unacknowledgedCount, 3); assert.equal(b.unacknowledgedCount, 0); @@ -198,7 +203,7 @@ suite('PersistentProtocol reconnection', () => { assert.equal(a.unacknowledgedCount, 3); assert.equal(b.unacknowledgedCount, 0); - b.send(Buffer.from('b1')); + b.send(VSBuffer.fromString('b1')); assert.equal(a.unacknowledgedCount, 3); assert.equal(b.unacknowledgedCount, 1); @@ -207,7 +212,7 @@ suite('PersistentProtocol reconnection', () => { assert.equal(a.unacknowledgedCount, 0); assert.equal(b.unacknowledgedCount, 1); - a.send(Buffer.from('a4')); + a.send(VSBuffer.fromString('a4')); assert.equal(a.unacknowledgedCount, 1); assert.equal(b.unacknowledgedCount, 1); diff --git a/src/vs/base/parts/ipc/test/node/ipc.test.ts b/src/vs/base/parts/ipc/test/node/ipc.test.ts index 1880182472..caa949d831 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.test.ts @@ -4,19 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient } from 'vs/base/parts/ipc/node/ipc'; +import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient } from 'vs/base/parts/ipc/common/ipc'; import { Emitter, Event } from 'vs/base/common/event'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { timeout } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; class QueueProtocol implements IMessagePassingProtocol { private buffering = true; - private buffers: Buffer[] = []; + private buffers: VSBuffer[] = []; - private _onMessage = new Emitter({ + private _onMessage = new Emitter({ onFirstListenerDidAdd: () => { for (const buffer of this.buffers) { this._onMessage.fire(buffer); @@ -33,11 +33,11 @@ class QueueProtocol implements IMessagePassingProtocol { readonly onMessage = this._onMessage.event; other: QueueProtocol; - send(buffer: Buffer): void { + send(buffer: VSBuffer): void { this.other.receive(buffer); } - protected receive(buffer: Buffer): void { + protected receive(buffer: VSBuffer): void { if (this.buffering) { this.buffers.push(buffer); } else { @@ -196,10 +196,10 @@ suite('Base IPC', function () { test('createProtocolPair', async function () { const [clientProtocol, serverProtocol] = createProtocolPair(); - const b1 = Buffer.alloc(0); + const b1 = VSBuffer.alloc(0); clientProtocol.send(b1); - const b3 = Buffer.alloc(0); + const b3 = VSBuffer.alloc(0); serverProtocol.send(b3); const b2 = await Event.toPromise(serverProtocol.onMessage); diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 5a1c47538c..0b049d6559 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -531,4 +531,30 @@ suite('Async', () => { assert.notEqual(r1Queue, r1Queue2); // previous one got disposed after finishing }); }); + + test('retry - success case', async () => { + let counter = 0; + + const res = await async.retry(() => { + counter++; + if (counter < 2) { + return Promise.reject(new Error('fail')); + } + + return Promise.resolve(true); + }, 10, 3); + + assert.equal(res, true); + }); + + test('retry - error case', async () => { + let expectedError = new Error('fail'); + try { + await async.retry(() => { + return Promise.reject(expectedError); + }, 10, 3); + } catch (error) { + assert.equal(error, error); + } + }); }); diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 867f536038..15c8081f92 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -7,7 +7,7 @@ import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matc function filterOk(filter: IFilter, word: string, wordToMatchAgainst: string, highlights?: { start: number; end: number; }[]) { let r = filter(word, wordToMatchAgainst); - assert(r); + assert(r, `${word} didn't match ${wordToMatchAgainst}`); if (highlights) { assert.deepEqual(r, highlights); } @@ -202,6 +202,17 @@ suite('Filters', () => { assert.ok(matchesWords('gipu', 'Category: Git: Pull', true) === null); assert.deepEqual(matchesWords('pu', 'Category: Git: Pull', true), [{ start: 15, end: 17 }]); + + filterOk(matchesWords, 'bar', 'foo-bar'); + filterOk(matchesWords, 'bar test', 'foo-bar test'); + filterOk(matchesWords, 'fbt', 'foo-bar test'); + filterOk(matchesWords, 'bar test', 'foo-bar (test)'); + filterOk(matchesWords, 'foo bar', 'foo (bar)'); + + filterNotOk(matchesWords, 'bar est', 'foo-bar test'); + filterNotOk(matchesWords, 'fo ar', 'foo-bar test'); + filterNotOk(matchesWords, 'for', 'foo-bar test'); + filterNotOk(matchesWords, 'foo bar', 'foo-bar'); }); function assertMatches(pattern: string, word: string, decoratedWord: string | undefined, filter: FuzzyScorer, opts: { patternPos?: number, wordPos?: number, firstMatchCanBeWeak?: boolean } = {}) { diff --git a/src/vs/base/test/node/extfs/extfs.test.ts b/src/vs/base/test/node/extfs/extfs.test.ts index 74d0adbb71..07d331c0e6 100644 --- a/src/vs/base/test/node/extfs/extfs.test.ts +++ b/src/vs/base/test/node/extfs/extfs.test.ts @@ -246,41 +246,40 @@ suite('Extfs', () => { }); test('writeFileAndFlush (string)', 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'); + const smallData = 'Hello World'; + const bigData = (new Array(100 * 1024)).join('Large String\n'); - mkdirp(newDir, 493, error => { - if (error) { - return done(error); - } + testWriteFileAndFlush(smallData, smallData, bigData, bigData, done); + }); - assert.ok(fs.existsSync(newDir)); + test('writeFileAndFlush (Buffer)', function (done) { + const smallData = 'Hello World'; + const bigData = (new Array(100 * 1024)).join('Large String\n'); - extfs.writeFileAndFlush(testFile, 'Hello World', null!, error => { - if (error) { - return done(error); - } + testWriteFileAndFlush(Buffer.from(smallData), smallData, Buffer.from(bigData), bigData, done); + }); - assert.equal(fs.readFileSync(testFile), 'Hello World'); + test('writeFileAndFlush (UInt8Array)', function (done) { + const smallData = 'Hello World'; + const bigData = (new Array(100 * 1024)).join('Large String\n'); - const largeString = (new Array(100 * 1024)).join('Large String\n'); - - extfs.writeFileAndFlush(testFile, largeString, null!, error => { - if (error) { - return done(error); - } - - assert.equal(fs.readFileSync(testFile), largeString); - - extfs.del(parentDir, os.tmpdir(), done, ignore); - }); - }); - }); + 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); @@ -293,27 +292,25 @@ suite('Extfs', () => { assert.ok(fs.existsSync(newDir)); - extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null!, error => { + extfs.writeFileAndFlush(testFile, smallData, null!, error => { if (error) { return done(error); } - assert.equal(fs.readFileSync(testFile), 'Hello World'); + assert.equal(fs.readFileSync(testFile), smallDataValue); - const largeString = (new Array(100 * 1024)).join('Large String\n'); - - extfs.writeFileAndFlush(testFile, toReadable(largeString), null!, error => { + extfs.writeFileAndFlush(testFile, bigData, null!, error => { if (error) { return done(error); } - assert.equal(fs.readFileSync(testFile), largeString); + assert.equal(fs.readFileSync(testFile), bigDataValue); extfs.del(parentDir, os.tmpdir(), done, ignore); }); }); }); - }); + } test('writeFileAndFlush (file stream)', function (done) { const id = uuid.generateUuid(); diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 21a3bb0548..44c58a46c6 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -16,7 +16,7 @@ import * as os from 'os'; import { debounce } from 'vs/base/common/decorators'; import * as platform from 'vs/base/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; -import { getDelayedChannel } from 'vs/base/parts/ipc/node/ipc'; +import { getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IWindowConfiguration, IWindowsService } from 'vs/platform/windows/common/windows'; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index f29135a6d0..a01c89b315 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -41,14 +41,13 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { DownloadService } from 'vs/platform/download/node/downloadService'; import { IDownloadService } from 'vs/platform/download/common/download'; -import { StaticRouter } from 'vs/base/parts/ipc/node/ipc'; +import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner'; import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner'; import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; export interface ISharedProcessConfiguration { readonly machineId: string; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index dfe7da8c46..e1a930b7f6 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -14,7 +14,8 @@ import { getShellEnvironment } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/node/updateIpc'; import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main'; -import { Server, connect, Client } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client } from 'vs/base/parts/ipc/common/ipc.net'; +import { Server, connect } from 'vs/base/parts/ipc/node/ipc.net'; import { SharedProcess } from 'vs/code/electron-main/sharedProcess'; import { LaunchService, LaunchChannel, ILaunchService } from 'vs/platform/launch/electron-main/launchService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -31,7 +32,7 @@ import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { getDelayedChannel, StaticRouter } from 'vs/base/parts/ipc/node/ipc'; +import { getDelayedChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import product from 'vs/platform/product/node/product'; import pkg from 'vs/platform/product/node/package'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; @@ -55,7 +56,7 @@ import { LogLevelSetterChannel } from 'vs/platform/log/node/logIpc'; import * as errors 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 } from 'vs/platform/remote/node/remoteAgentConnection'; +import { connectRemoteAgentManagement, ManagementPersistentConnection, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; import { IMenubarService } from 'vs/platform/menubar/common/menubar'; import { MenubarService } from 'vs/platform/menubar/electron-main/menubarService'; import { MenubarChannel } from 'vs/platform/menubar/node/menubarIpc'; @@ -67,7 +68,7 @@ import { homedir } from 'os'; import { join, sep } from 'vs/base/common/path'; import { localize } from 'vs/nls'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; -import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/remote/node/remoteAgentFileSystemChannel'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/remote/common/remoteAgentFileSystemChannel'; import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap'; import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService'; @@ -79,6 +80,7 @@ import { HistoryMainService } from 'vs/platform/history/electron-main/historyMai 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'; export class CodeApplication extends Disposable { @@ -575,16 +577,43 @@ export class CodeApplication extends Disposable { const hasFolderURIs = hasArgs(args['folder-uri']); const hasFileURIs = hasArgs(args['file-uri']); const noRecentEntry = args['skip-add-to-recently-opened'] === true; + const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { - return this.windowsMainService.open({ context, cli: args, forceNewWindow: true, forceEmpty: true, noRecentEntry, initialStartup: true }); // new window if "-n" was used without paths + // new window if "-n" was used without paths + return this.windowsMainService.open({ + context, + cli: args, + forceNewWindow: true, + forceEmpty: true, + noRecentEntry, + waitMarkerFileURI, + initialStartup: true + }); } if (macOpenFiles && macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { - return this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, urisToOpen: macOpenFiles.map(file => ({ uri: URI.file(file) })), noRecentEntry, initialStartup: true }); // mac: open-file event received on startup + // mac: open-file event received on startup + return this.windowsMainService.open({ + context: OpenContext.DOCK, + cli: args, + urisToOpen: macOpenFiles.map(file => ({ uri: URI.file(file) })), + noRecentEntry, + waitMarkerFileURI, + initialStartup: true + }); } - return this.windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), diffMode: args.diff, noRecentEntry, initialStartup: true }); // default: read paths from cli + // default: read paths from cli + return this.windowsMainService.open({ + context, + cli: args, + forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), + diffMode: args.diff, + noRecentEntry, + waitMarkerFileURI, + initialStartup: true + }); } private afterWindowOpen(accessor: ServicesAccessor): void { @@ -656,7 +685,17 @@ export class CodeApplication extends Disposable { constructor(authority: string, host: string, port: number) { this._authority = authority; - this._connection = connectRemoteAgentManagement(authority, host, port, `main`, isBuilt); + const options: IConnectionOptions = { + isBuilt: isBuilt, + commit: product.commit, + webSocketFactory: nodeWebSocketFactory, + addressProvider: { + getAddress: () => { + return Promise.resolve({ host, port }); + } + } + }; + this._connection = connectRemoteAgentManagement(options, authority, `main`); this._disposeRunner = new RunOnceScheduler(() => this.dispose(), 5000); } diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 65bfdbf5c3..b56771c5fc 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -8,8 +8,8 @@ import { app, dialog } from 'electron'; import { assign } from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import product from 'vs/platform/product/node/product'; -import { parseMainProcessArgv, createWaitMarkerFile } from 'vs/platform/environment/node/argvHelper'; -import { addArg } from 'vs/platform/environment/node/argv'; +import { parseMainProcessArgv } from 'vs/platform/environment/node/argvHelper'; +import { addArg, createWaitMarkerFile } from 'vs/platform/environment/node/argv'; import { mkdirp } from 'vs/base/node/pfs'; import { validatePaths } from 'vs/code/node/paths'; import { LifecycleService, ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; @@ -142,7 +142,10 @@ function setupIPC(accessor: ServicesAccessor): Promise { if (environmentService.args.status) { return service.getMainProcessInfo().then(info => { return instantiationService.invokeFunction(accessor => { - return accessor.get(IDiagnosticsService).printDiagnostics(info).then(() => Promise.reject(new ExpectedError())); + return accessor.get(IDiagnosticsService).getDiagnostics(info).then(diagnostics => { + console.log(diagnostics); + return Promise.reject(new ExpectedError()); + }); }); }); } @@ -212,7 +215,7 @@ function showStartupWarningDialog(message: string, detail: string): void { }); } -function handleStartupDataDirError(environmentService: IEnvironmentService, error): void { +function handleStartupDataDirError(environmentService: IEnvironmentService, error: NodeJS.ErrnoException): void { if (error.code === 'EACCES' || error.code === 'EPERM') { showStartupWarningDialog( localize('startupDataDirError', "Unable to write program user data."), @@ -357,20 +360,13 @@ function main(): void { // Note: we are not doing this if the wait marker has been already // added as argument. This can happen if Code was started from CLI. if (args.wait && !args.waitMarkerFilePath) { - createWaitMarkerFile(args.verbose).then(waitMarkerFilePath => { - if (waitMarkerFilePath) { - addArg(process.argv, '--waitMarkerFilePath', waitMarkerFilePath); - args.waitMarkerFilePath = waitMarkerFilePath; - } - - startup(args); - }); - } - - // Otherwise just startup normally - else { - startup(args); + const waitMarkerFilePath = createWaitMarkerFile(args.verbose); + if (waitMarkerFilePath) { + addArg(process.argv, '--waitMarkerFilePath', waitMarkerFilePath); + args.waitMarkerFilePath = waitMarkerFilePath; + } } + startup(args); } main(); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 13207f5fc6..82642d4bf8 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -317,9 +317,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details: any, cb: any) => { this.marketplaceHeadersPromise.then(headers => { const requestHeaders = objects.assign(details.requestHeaders, headers); - if (!this.configurationService.getValue('extensions.disableExperimentalAzureSearch')) { - requestHeaders['Cookie'] = `${requestHeaders['Cookie'] ? requestHeaders['Cookie'] + ';' : ''}EnableExternalSearchForVSCode=true`; - } cb({ cancel: false, requestHeaders }); }); }); diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 94fee5ccc1..9197c41bb3 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -421,8 +421,8 @@ export class WindowsManager implements IWindowsMainService { } // When run with --wait, make sure we keep the paths to wait for - if (fileInputs && openConfig.cli.wait && openConfig.cli.waitMarkerFilePath) { - fileInputs.filesToWait = { paths: [...fileInputs.filesToDiff, ...fileInputs.filesToOpen, ...fileInputs.filesToCreate], waitMarkerFileUri: URI.file(openConfig.cli.waitMarkerFilePath) }; + if (fileInputs && openConfig.waitMarkerFileURI) { + fileInputs.filesToWait = { paths: [...fileInputs.filesToDiff, ...fileInputs.filesToOpen, ...fileInputs.filesToCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; } // @@ -506,8 +506,9 @@ export class WindowsManager implements IWindowsMainService { // If we got started with --wait from the CLI, we need to signal to the outside when the window // used for the edit operation is closed or loaded to a different folder so that the waiting // process can continue. We do this by deleting the waitMarkerFilePath. - if (openConfig.context === OpenContext.CLI && openConfig.cli.wait && openConfig.cli.waitMarkerFilePath && usedWindows.length === 1 && usedWindows[0]) { - this.waitForWindowCloseOrLoad(usedWindows[0].id).then(() => fs.unlink(openConfig.cli.waitMarkerFilePath!, _error => undefined)); + const waitMarkerFileURI = openConfig.waitMarkerFileURI; + if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { + this.waitForWindowCloseOrLoad(usedWindows[0].id).then(() => fs.unlink(waitMarkerFileURI.fsPath, _error => undefined)); } return usedWindows; @@ -1216,7 +1217,16 @@ export class WindowsManager implements IWindowsMainService { } // Open it - this.open({ context: openConfig.context, cli: openConfig.cli, forceNewWindow: true, forceEmpty: !cliArgs.length && !folderUris.length && !fileUris.length, userEnv: openConfig.userEnv, noRecentEntry: true }); + const openArgs: IOpenConfiguration = { + context: openConfig.context, + cli: openConfig.cli, + forceNewWindow: true, + forceEmpty: !cliArgs.length && !folderUris.length && !fileUris.length, + userEnv: openConfig.userEnv, + noRecentEntry: true, + waitMarkerFileURI: openConfig.waitMarkerFileURI + }; + this.open(openArgs); } private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { @@ -1557,7 +1567,8 @@ export class WindowsManager implements IWindowsMainService { if (cli && (cli.remote !== remote)) { cli = { ...cli, remote }; } - return this.open({ context, cli, forceNewWindow: true, forceEmpty: true }); + const forceNewWindow = !(options && options.reuseWindow); + return this.open({ context, cli, forceNewWindow, forceEmpty: true }); } openNewTabbedWindow(context: OpenContext): ICodeWindow[] { diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 35df10f5e4..1cb0c26e96 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -5,8 +5,8 @@ import { spawn, ChildProcess } from 'child_process'; import { assign } from 'vs/base/common/objects'; -import { buildHelpMessage, buildVersionMessage, addArg } from 'vs/platform/environment/node/argv'; -import { parseCLIProcessArgv, createWaitMarkerFile } from 'vs/platform/environment/node/argvHelper'; +import { buildHelpMessage, buildVersionMessage, addArg, createWaitMarkerFile } from 'vs/platform/environment/node/argv'; +import { parseCLIProcessArgv } from 'vs/platform/environment/node/argvHelper'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; import product from 'vs/platform/product/node/product'; import pkg from 'vs/platform/product/node/package'; @@ -229,7 +229,7 @@ export async function main(argv: string[]): Promise { // is closed and then exit the waiting process. let waitMarkerFilePath: string | undefined; if (args.wait) { - waitMarkerFilePath = await createWaitMarkerFile(verbose); + waitMarkerFilePath = createWaitMarkerFile(verbose); if (waitMarkerFilePath) { addArg(argv, '--waitMarkerFilePath', waitMarkerFilePath); } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 7b775c9f96..0b5d8065f3 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1567,9 +1567,9 @@ export interface ITokenizationRegistry { /** * Get the tokenization support for a language. - * Returns null if not found. + * Returns `null` if not found. */ - get(language: string): ITokenizationSupport; + get(language: string): ITokenizationSupport | null; /** * Get the promise of a tokenization support for a language. diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index 42599182c6..9d83292d32 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -57,7 +57,7 @@ export class RichEditSupport { public readonly indentationRules: IndentationRule | undefined; public readonly foldingRules: FoldingRules; - constructor(languageIdentifier: LanguageIdentifier, previous: RichEditSupport, rawConf: LanguageConfiguration) { + constructor(languageIdentifier: LanguageIdentifier, previous: RichEditSupport | undefined, rawConf: LanguageConfiguration) { this._languageIdentifier = languageIdentifier; this._brackets = null; @@ -175,34 +175,30 @@ export class LanguageConfigurationChangeEvent { export class LanguageConfigurationRegistryImpl { - private readonly _entries: RichEditSupport[]; + private readonly _entries = new Map(); private readonly _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; - constructor() { - this._entries = []; - } - public register(languageIdentifier: LanguageIdentifier, configuration: LanguageConfiguration): IDisposable { let previous = this._getRichEditSupport(languageIdentifier.id); let current = new RichEditSupport(languageIdentifier, previous, configuration); - this._entries[languageIdentifier.id] = current; + this._entries.set(languageIdentifier.id, current); this._onDidChange.fire({ languageIdentifier }); return toDisposable(() => { - if (this._entries[languageIdentifier.id] === current) { - this._entries[languageIdentifier.id] = previous; + if (this._entries.get(languageIdentifier.id) === current) { + this._entries.set(languageIdentifier.id, previous); this._onDidChange.fire({ languageIdentifier }); } }); } - private _getRichEditSupport(languageId: LanguageId): RichEditSupport { - return this._entries[languageId] || null; + private _getRichEditSupport(languageId: LanguageId): RichEditSupport | undefined { + return this._entries.get(languageId); } public getIndentationRules(languageId: LanguageId) { - let value = this._entries[languageId]; + const value = this._entries.get(languageId); if (!value) { return null; diff --git a/src/vs/editor/common/modes/tokenizationRegistry.ts b/src/vs/editor/common/modes/tokenizationRegistry.ts index c3045a1a44..77b64dd63b 100644 --- a/src/vs/editor/common/modes/tokenizationRegistry.ts +++ b/src/vs/editor/common/modes/tokenizationRegistry.ts @@ -7,11 +7,13 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ColorId, ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent } from 'vs/editor/common/modes'; +import { withUndefinedAsNull } from 'vs/base/common/types'; +import { keys } from 'vs/base/common/map'; export class TokenizationRegistryImpl implements ITokenizationRegistry { - private readonly _map: { [language: string]: ITokenizationSupport }; - private readonly _promises: { [language: string]: Thenable }; + private readonly _map = new Map(); + private readonly _promises = new Map>(); private readonly _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; @@ -19,8 +21,6 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry { private _colorMap: Color[] | null; constructor() { - this._map = Object.create(null); - this._promises = Object.create(null); this._colorMap = null; } @@ -32,13 +32,13 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry { } public register(language: string, support: ITokenizationSupport) { - this._map[language] = support; + this._map.set(language, support); this.fire([language]); return toDisposable(() => { - if (this._map[language] !== support) { + if (this._map.get(language) !== support) { return; } - delete this._map[language]; + this._map.delete(language); this.fire([language]); }); } @@ -48,13 +48,13 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry { let registration: IDisposable | null = null; let isDisposed: boolean = false; - this._promises[language] = supportPromise.then(support => { - delete this._promises[language]; + this._promises.set(language, supportPromise.then(support => { + this._promises.delete(language); if (isDisposed || !support) { return; } registration = this.register(language, support); - }); + })); return toDisposable(() => { isDisposed = true; @@ -69,21 +69,21 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry { if (support) { return Promise.resolve(support); } - const promise = this._promises[language]; + const promise = this._promises.get(language); if (promise) { - return promise.then(_ => this.get(language)); + return promise.then(_ => this.get(language)!); } return null; } - public get(language: string): ITokenizationSupport { - return (this._map[language] || null); + public get(language: string): ITokenizationSupport | null { + return withUndefinedAsNull(this._map.get(language)); } public setColorMap(colorMap: Color[]): void { this._colorMap = colorMap; this._onDidChange.fire({ - changedLanguages: Object.keys(this._map), + changedLanguages: keys(this._map), changedColorMap: true }); } diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts new file mode 100644 index 0000000000..f0c107c0cb --- /dev/null +++ b/src/vs/editor/common/standaloneStrings.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; + +export namespace AccessibilityHelpNLS { + export const noSelection = nls.localize("noSelection", "No selection"); + export const singleSelectionRange = nls.localize("singleSelectionRange", "Line {0}, Column {1} ({2} selected)"); + export const singleSelection = nls.localize("singleSelection", "Line {0}, Column {1}"); + export const multiSelectionRange = nls.localize("multiSelectionRange", "{0} selections ({1} characters selected)"); + export const multiSelection = nls.localize("multiSelection", "{0} selections"); + export const emergencyConfOn = nls.localize("emergencyConfOn", "Now changing the setting `accessibilitySupport` to 'on'."); + export const openingDocs = nls.localize("openingDocs", "Now opening the Editor Accessibility documentation page."); + export const readonlyDiffEditor = nls.localize("readonlyDiffEditor", " in a read-only pane of a diff editor."); + export const editableDiffEditor = nls.localize("editableDiffEditor", " in a pane of a diff editor."); + export const readonlyEditor = nls.localize("readonlyEditor", " in a read-only code editor"); + export const editableEditor = nls.localize("editableEditor", " in a code editor"); + export const changeConfigToOnMac = nls.localize("changeConfigToOnMac", "To configure the editor to be optimized for usage with a Screen Reader press Command+E now."); + export const changeConfigToOnWinLinux = nls.localize("changeConfigToOnWinLinux", "To configure the editor to be optimized for usage with a Screen Reader press Control+E now."); + export const auto_on = nls.localize("auto_on", "The editor is configured to be optimized for usage with a Screen Reader."); + export const auto_off = nls.localize("auto_off", "The editor is configured to never be optimized for usage with a Screen Reader, which is not the case at this time."); + export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior by pressing {0}."); + export const tabFocusModeOnMsgNoKb = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding."); + export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}."); + export const tabFocusModeOffMsgNoKb = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding."); + export const openDocMac = nls.localize("openDocMac", "Press Command+H now to open a browser window with more information related to editor accessibility."); + export const openDocWinLinux = nls.localize("openDocWinLinux", "Press Control+H now to open a browser window with more information related to editor accessibility."); + export const outroMsg = nls.localize("outroMsg", "You can dismiss this tooltip and return to the editor by pressing Escape or Shift+Escape."); + export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help"); +} + +export namespace InspectTokensNLS { + export const inspectTokensAction = nls.localize('inspectTokens', "Developer: Inspect Tokens"); +} + +export namespace GoToLineNLS { + export const gotoLineLabelValidLineAndColumn = nls.localize('gotoLineLabelValidLineAndColumn', "Go to line {0} and character {1}"); + export const gotoLineLabelValidLine = nls.localize('gotoLineLabelValidLine', "Go to line {0}"); + export const gotoLineLabelEmptyWithLineLimit = nls.localize('gotoLineLabelEmptyWithLineLimit', "Type a line number between 1 and {0} to navigate to"); + export const gotoLineLabelEmptyWithLineAndColumnLimit = nls.localize('gotoLineLabelEmptyWithLineAndColumnLimit', "Type a character between 1 and {0} to navigate to"); + export const gotoLineAriaLabel = nls.localize('gotoLineAriaLabel', "Current Line: {0}. Go to line {1}."); + export const gotoLineActionInput = nls.localize('gotoLineActionInput', "Type a line number, followed by an optional colon and a character number to navigate to"); + export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line..."); +} + +export namespace QuickCommandNLS { + export const ariaLabelEntryWithKey = nls.localize('ariaLabelEntryWithKey', "{0}, {1}, commands"); + export const ariaLabelEntry = nls.localize('ariaLabelEntry', "{0}, commands"); + export const quickCommandActionInput = nls.localize('quickCommandActionInput', "Type the name of an action you want to execute"); + export const quickCommandActionLabel = nls.localize('quickCommandActionLabel', "Command Palette"); +} + +export namespace QuickOutlineNLS { + export const entryAriaLabel = nls.localize('entryAriaLabel', "{0}, symbols"); + export const quickOutlineActionInput = nls.localize('quickOutlineActionInput', "Type the name of an identifier you wish to navigate to"); + export const quickOutlineActionLabel = nls.localize('quickOutlineActionLabel', "Go to Symbol..."); + export const _symbols_ = nls.localize('symbols', "symbols ({0})"); + export const _modules_ = nls.localize('modules', "modules ({0})"); + export const _class_ = nls.localize('class', "classes ({0})"); + export const _interface_ = nls.localize('interface', "interfaces ({0})"); + export const _method_ = nls.localize('method', "methods ({0})"); + export const _function_ = nls.localize('function', "functions ({0})"); + export const _property_ = nls.localize('property', "properties ({0})"); + export const _variable_ = nls.localize('variable', "variables ({0})"); + export const _variable2_ = nls.localize('variable2', "variables ({0})"); + export const _constructor_ = nls.localize('_constructor', "constructors ({0})"); + export const _call_ = nls.localize('call', "calls ({0})"); +} + +export namespace StandaloneCodeEditorNLS { + export const editorViewAccessibleLabel = nls.localize('editorViewAccessibleLabel', "Editor content"); + export const accessibilityHelpMessageIE = nls.localize('accessibilityHelpMessageIE', "Press Ctrl+F1 for Accessibility Options."); + export const accessibilityHelpMessage = nls.localize('accessibilityHelpMessage', "Press Alt+F1 for Accessibility Options."); +} + +export namespace ToggleHighContrastNLS { + export const toggleHighContrast = nls.localize('toggleHighContrast', "Toggle High Contrast Theme"); +} + +export namespace SimpleServicesNLS { + export const bulkEditServiceSummary = nls.localize('bulkEditServiceSummary', "Made {0} edits in {1} files"); +} diff --git a/src/vs/editor/contrib/folding/indentRangeProvider.ts b/src/vs/editor/contrib/folding/indentRangeProvider.ts index 7b8b2c0102..7f8e600f6e 100644 --- a/src/vs/editor/contrib/folding/indentRangeProvider.ts +++ b/src/vs/editor/contrib/folding/indentRangeProvider.ts @@ -18,8 +18,6 @@ export const ID_INDENT_PROVIDER = 'indent'; export class IndentRangeProvider implements RangeProvider { readonly id = ID_INDENT_PROVIDER; - readonly decorations; - constructor(private readonly editorModel: ITextModel) { } diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 06f0b0fb97..4583524002 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -22,6 +22,9 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit'; import * as nls from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; +import { ILabelService } from 'vs/platform/label/common/label'; export function alertFormattingEdits(edits: ISingleEditOperation[]): void { @@ -83,6 +86,32 @@ export function getRealAndSyntheticDocumentFormattersOrdered(model: ITextModel): return result; } +export async function formatDocumentRangeWithFirstProvider( + accessor: ServicesAccessor, + editorOrModel: ITextModel | IActiveCodeEditor, + range: Range, + token: CancellationToken +): Promise { + + const instaService = accessor.get(IInstantiationService); + const statusBarService = accessor.get(IStatusbarService); + const labelService = accessor.get(ILabelService); + + const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel; + const [best, ...rest] = DocumentRangeFormattingEditProviderRegistry.ordered(model); + if (!best) { + return false; + } + const ret = await instaService.invokeFunction(formatDocumentRangeWithProvider, best, editorOrModel, range, token); + if (rest.length > 0) { + statusBarService.setStatusMessage( + nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName), + 5 * 1000 + ); + } + return ret; +} + export async function formatDocumentRangeWithProvider( accessor: ServicesAccessor, provider: DocumentRangeFormattingEditProvider, @@ -152,6 +181,31 @@ export async function formatDocumentRangeWithProvider( return true; } +export async function formatDocumentWithFirstProvider( + accessor: ServicesAccessor, + editorOrModel: ITextModel | IActiveCodeEditor, + token: CancellationToken +): Promise { + + const instaService = accessor.get(IInstantiationService); + const statusBarService = accessor.get(IStatusbarService); + const labelService = accessor.get(ILabelService); + + const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel; + const [best, ...rest] = getRealAndSyntheticDocumentFormattersOrdered(model); + if (!best) { + return false; + } + const ret = await instaService.invokeFunction(formatDocumentWithProvider, best, editorOrModel, token); + if (rest.length > 0) { + statusBarService.setStatusMessage( + nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName), + 5 * 1000 + ); + } + return ret; +} + export async function formatDocumentWithProvider( accessor: ServicesAccessor, provider: DocumentFormattingEditProvider, diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 2ddb4f6f41..d16c9d4184 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -16,7 +16,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { getOnTypeFormattingEdits, formatDocumentWithProvider, formatDocumentRangeWithProvider, alertFormattingEdits, getRealAndSyntheticDocumentFormattersOrdered } from 'vs/editor/contrib/format/format'; +import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithFirstProvider, formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format'; import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit'; import * as nls from 'vs/nls'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -211,12 +211,7 @@ class FormatOnPaste implements editorCommon.IEditorContribution { if (this.editor.getSelections().length > 1) { return; } - const provider = DocumentRangeFormattingEditProviderRegistry.ordered(this.editor.getModel()); - if (provider.length !== 1) { - // print status in n>1 case? - return; - } - this._instantiationService.invokeFunction(formatDocumentRangeWithProvider, provider[0], this.editor, range, CancellationToken.None).catch(onUnexpectedError); + this._instantiationService.invokeFunction(formatDocumentRangeWithFirstProvider, this.editor, range, CancellationToken.None).catch(onUnexpectedError); } } @@ -227,7 +222,7 @@ class FormatDocumentAction extends EditorAction { id: 'editor.action.formatDocument', label: nls.localize('formatDocument.label', "Format Document"), alias: 'Format Document', - precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider, EditorContextKeys.hasMultipleDocumentFormattingProvider.toNegated()), + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider), kbOpts: { kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentFormattingProvider), primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F, @@ -243,14 +238,9 @@ class FormatDocumentAction extends EditorAction { } async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - if (!editor.hasModel()) { - return; - } - const instaService = accessor.get(IInstantiationService); - const model = editor.getModel(); - const [provider] = getRealAndSyntheticDocumentFormattersOrdered(model); - if (provider) { - await instaService.invokeFunction(formatDocumentWithProvider, provider, editor, CancellationToken.None); + if (editor.hasModel()) { + const instaService = accessor.get(IInstantiationService); + await instaService.invokeFunction(formatDocumentWithFirstProvider, editor, CancellationToken.None); } } } @@ -262,7 +252,7 @@ class FormatSelectionAction extends EditorAction { id: 'editor.action.formatSelection', label: nls.localize('formatSelection.label', "Format Selection"), alias: 'Format Code', - precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider, EditorContextKeys.hasMultipleDocumentSelectionFormattingProvider.toNegated()), + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider), kbOpts: { kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentSelectionFormattingProvider), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F), @@ -281,14 +271,12 @@ class FormatSelectionAction extends EditorAction { return; } const instaService = accessor.get(IInstantiationService); - const [best] = DocumentRangeFormattingEditProviderRegistry.ordered(editor.getModel()); - if (best) { - let range: Range = editor.getSelection(); - if (range.isEmpty()) { - range = new Range(range.startLineNumber, 1, range.startLineNumber, editor.getModel().getLineMaxColumn(range.startLineNumber)); - } - await instaService.invokeFunction(formatDocumentRangeWithProvider, best, editor, range, CancellationToken.None); + const model = editor.getModel(); + let range: Range = editor.getSelection(); + if (range.isEmpty()) { + range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)); } + await instaService.invokeFunction(formatDocumentRangeWithFirstProvider, editor, range, CancellationToken.None); } } diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 35b42a39b4..927abc1576 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -41,6 +41,7 @@ import { Action } from 'vs/base/common/actions'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; import { IModeService } from 'vs/editor/common/services/modeService'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; const $ = dom.$; @@ -385,10 +386,10 @@ export class ModesContentHoverWidget extends ContentHoverWidget { model.guessColorPresentation(color, originalText); const updateEditorModel = () => { - let textEdits; - let newRange; + let textEdits: IIdentifiedSingleEditOperation[]; + let newRange: Range; if (model.presentation.textEdit) { - textEdits = [model.presentation.textEdit]; + textEdits = [model.presentation.textEdit as IIdentifiedSingleEditOperation]; newRange = new Range( model.presentation.textEdit.range.startLineNumber, model.presentation.textEdit.range.startColumn, @@ -405,7 +406,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._editor.executeEdits('colorpicker', textEdits); if (model.presentation.additionalTextEdits) { - textEdits = [...model.presentation.additionalTextEdits]; + textEdits = [...model.presentation.additionalTextEdits as IIdentifiedSingleEditOperation[]]; this._editor.executeEdits('colorpicker', textEdits); this.hide(); } diff --git a/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts b/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts index 77cf881912..935a9c68f1 100644 --- a/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/peekViewWidget.ts @@ -18,7 +18,38 @@ import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeE import { IOptions, IStyles, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import * as nls from 'vs/nls'; import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + + +export const IPeekViewService = createDecorator('IPeekViewService'); +export interface IPeekViewService { + _serviceBrand: any; + addExclusiveWidget(editor: ICodeEditor, widget: PeekViewWidget): void; +} + +registerSingleton(IPeekViewService, class implements IPeekViewService { + _serviceBrand: any; + + private _widgets = new Map(); + + addExclusiveWidget(editor: ICodeEditor, widget: PeekViewWidget): void { + const existing = this._widgets.get(editor); + if (existing) { + existing.listener.dispose(); + existing.widget.dispose(); + } + const remove = () => { + const data = this._widgets.get(editor); + if (data && data.widget === widget) { + data.listener.dispose(); + this._widgets.delete(editor); + } + }; + this._widgets.set(editor, { widget, listener: widget.onDidClose(remove) }); + } +}); export namespace PeekContext { export const inPeekEditor = new RawContextKey('inReferenceSearchEditor', true); diff --git a/src/vs/editor/contrib/referenceSearch/referencesController.ts b/src/vs/editor/contrib/referenceSearch/referencesController.ts index b30ee91abc..88029247b3 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesController.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesController.ts @@ -102,6 +102,7 @@ export abstract class ReferencesController implements editorCommon.IEditorContri this._widget = this._instantiationService.createInstance(ReferenceWidget, this._editor, this._defaultTreeKeyboardSupport, data); this._widget.setTitle(nls.localize('labelLoading', "Loading...")); this._widget.show(range); + this._disposables.push(this._widget.onDidClose(() => { modelPromise.cancel(); if (this._widget) { diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index 782a401e6e..4220e0ece2 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { ISashEvent, IVerticalSashLayoutProvider, Sash } from 'vs/base/browser/ui/sash/sash'; +import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; @@ -29,12 +29,14 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { activeContrastBorder, contrastBorder, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { PeekViewWidget } from './peekViewWidget'; +import { PeekViewWidget, IPeekViewService } from './peekViewWidget'; import { FileReferences, OneReference, ReferencesModel } from './referencesModel'; import { ITreeRenderer, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IAsyncDataTreeOptions } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { FuzzyScore } from 'vs/base/common/filters'; +import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; + class DecorationsManager implements IDisposable { @@ -155,72 +157,6 @@ class DecorationsManager implements IDisposable { } } -class VSash { - - private _disposables: IDisposable[] = []; - private _sash: Sash; - private _ratio: number; - private _height: number; - private _width: number; - private _onDidChangePercentages = new Emitter(); - - constructor(container: HTMLElement, ratio: number) { - this._ratio = ratio; - this._sash = new Sash(container, { - getVerticalSashLeft: () => this._width * this._ratio, - getVerticalSashHeight: () => this._height - }); - - // compute the current widget clientX postion since - // the sash works with clientX when dragging - let clientX: number; - this._disposables.push(this._sash.onDidStart((e: ISashEvent) => { - clientX = e.startX - (this._width * this.ratio); - })); - - this._disposables.push(this._sash.onDidChange((e: ISashEvent) => { - // compute the new position of the sash and from that - // compute the new ratio that we are using - let newLeft = e.currentX - clientX; - if (newLeft > 20 && newLeft + 20 < this._width) { - this._ratio = newLeft / this._width; - this._sash.layout(); - this._onDidChangePercentages.fire(this); - } - })); - } - - dispose() { - this._sash.dispose(); - this._onDidChangePercentages.dispose(); - dispose(this._disposables); - } - - get onDidChangePercentages() { - return this._onDidChangePercentages.event; - } - - set width(value: number) { - this._width = value; - this._sash.layout(); - } - - set height(value: number) { - this._height = value; - this._sash.layout(); - } - - get percentages() { - let left = 100 * this._ratio; - let right = 100 - left; - return [`${left}%`, `${right}%`]; - } - - get ratio() { - return this._ratio; - } -} - export interface LayoutData { ratio: number; heightInLines: number; @@ -248,14 +184,14 @@ export class ReferenceWidget extends PeekViewWidget { private _tree: WorkbenchAsyncDataTree; private _treeContainer: HTMLElement; - private _sash: VSash; + // private _sash: VSash; + private _splitView: SplitView; private _preview: ICodeEditor; private _previewModelReference: IReference; private _previewNotAvailableMessage: TextModel; private _previewContainer: HTMLElement; private _messageContainer: HTMLElement; - private height: number | undefined; - private width: number | undefined; + private _dim: dom.Dimension = { height: 0, width: 0 }; constructor( editor: ICodeEditor, @@ -264,15 +200,25 @@ export class ReferenceWidget extends PeekViewWidget { @IThemeService themeService: IThemeService, @ITextModelService private readonly _textModelResolverService: ITextModelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IPeekViewService private readonly _peekViewService: IPeekViewService, @ILabelService private readonly _uriLabel: ILabelService ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }); this._applyTheme(themeService.getTheme()); this._callOnDispose.push(themeService.onThemeChange(this._applyTheme.bind(this))); + this._peekViewService.addExclusiveWidget(editor, this); this.create(); } + dispose(): void { + this.setModel(undefined); + this._callOnDispose = dispose(this._callOnDispose); + dispose(this._preview, this._previewNotAvailableMessage, this._tree, this._previewModelReference); + this._splitView.dispose(); + super.dispose(); + } + private _applyTheme(theme: ITheme) { const borderColor = theme.getColor(peekViewBorder) || Color.transparent; this.style({ @@ -284,13 +230,6 @@ export class ReferenceWidget extends PeekViewWidget { }); } - public dispose(): void { - this.setModel(undefined); - this._callOnDispose = dispose(this._callOnDispose); - dispose(this._preview, this._previewNotAvailableMessage, this._tree, this._sash, this._previewModelReference); - super.dispose(); - } - get onDidSelectReference(): Event { return this._onDidSelectReference.event; } @@ -321,6 +260,8 @@ export class ReferenceWidget extends PeekViewWidget { this._messageContainer = dom.append(containerElement, dom.$('div.messages')); dom.hide(this._messageContainer); + this._splitView = new SplitView(containerElement, { orientation: Orientation.HORIZONTAL }); + // editor this._previewContainer = dom.append(containerElement, dom.$('div.preview.inline')); let options: IEditorOptions = { @@ -342,25 +283,8 @@ export class ReferenceWidget extends PeekViewWidget { dom.hide(this._previewContainer); this._previewNotAvailableMessage = TextModel.createFromString(nls.localize('missingPreviewMessage', "no preview available")); - // sash - this._sash = new VSash(containerElement, this.layoutData.ratio || 0.8); - this._sash.onDidChangePercentages(() => { - let [left, right] = this._sash.percentages; - this._previewContainer.style.width = left; - this._treeContainer.style.width = right; - this._preview.layout(); - this._tree.layout(this.height, this.width && this.width * (1 - this._sash.ratio)); - this.layoutData.ratio = this._sash.ratio; - }); - // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); - - const renderers = [ - this._instantiationService.createInstance(FileReferencesRenderer), - this._instantiationService.createInstance(OneReferenceRenderer), - ]; - const treeOptions: IAsyncDataTreeOptions = { ariaLabel: nls.localize('treeAriaLabel', "References"), keyboardSupport: this._defaultTreeKeyboardSupport, @@ -368,20 +292,48 @@ export class ReferenceWidget extends PeekViewWidget { keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider), identityProvider: new IdentityProvider() }; - - const treeDataSource = this._instantiationService.createInstance(DataSource); - this._tree = this._instantiationService.createInstance, ITreeRenderer[], IAsyncDataSource, IAsyncDataTreeOptions, WorkbenchAsyncDataTree>( WorkbenchAsyncDataTree, this._treeContainer, new Delegate(), - renderers, - treeDataSource, + [ + this._instantiationService.createInstance(FileReferencesRenderer), + this._instantiationService.createInstance(OneReferenceRenderer), + ], + this._instantiationService.createInstance(DataSource), treeOptions ); - ctxReferenceWidgetSearchTreeFocused.bindTo(this._tree.contextKeyService); + // split stuff + this._splitView.addView({ + onDidChange: Event.None, + element: this._previewContainer, + minimumSize: 200, + maximumSize: Number.MAX_VALUE, + layout: (width) => { + this._preview.layout({ height: this._dim.height, width }); + } + }, Sizing.Distribute); + + this._splitView.addView({ + onDidChange: Event.None, + element: this._treeContainer, + minimumSize: 100, + maximumSize: Number.MAX_VALUE, + layout: (width) => { + this._treeContainer.style.height = `${this._dim.height}px`; + this._treeContainer.style.width = `${width}px`; + this._tree.layout(this._dim.height, width); + } + }, Sizing.Distribute); + + this._splitView.onDidSashChange(() => { + if (this._dim.width) { + this.layoutData.ratio = this._splitView.getViewSize(0) / this._dim.width; + } + }, undefined, this._disposables); + // listen on selection and focus let onEvent = (element: any, kind: 'show' | 'goto' | 'side') => { if (element instanceof OneReference) { @@ -428,36 +380,18 @@ export class ReferenceWidget extends PeekViewWidget { dom.hide(this._treeContainer); } - protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void { - super._doLayoutBody(heightInPixel, widthInPixel); - - this.height = heightInPixel; - this.width = widthInPixel; - - const height = heightInPixel + 'px'; - this._sash.height = heightInPixel; - this._sash.width = widthInPixel; - - // set height/width - const [left, right] = this._sash.percentages; - this._previewContainer.style.height = height; - this._previewContainer.style.width = left; - this._treeContainer.style.height = height; - this._treeContainer.style.width = right; - // forward - this._tree.layout(heightInPixel, widthInPixel * (1 - this._sash.ratio)); - this._preview.layout(); - - // store layout data - this.layoutData = { - heightInLines: this._viewZone ? this._viewZone.heightInLines : 0, - ratio: this._sash.ratio - }; + protected _onWidth(width: number) { + if (this._dim) { + this._doLayoutBody(this._dim.height, width); + } } - public _onWidth(widthInPixel: number): void { - this._sash.width = widthInPixel; - this._preview.layout(); + protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void { + super._doLayoutBody(heightInPixel, widthInPixel); + this._dim = { height: heightInPixel, width: widthInPixel }; + this.layoutData.heightInLines = this._viewZone ? this._viewZone.heightInLines : this.layoutData.heightInLines; + this._splitView.layout(widthInPixel); + this._splitView.resizeView(0, widthInPixel * this.layoutData.ratio); } public setSelection(selection: OneReference): Promise { @@ -522,8 +456,7 @@ export class ReferenceWidget extends PeekViewWidget { dom.addClass(this.container, 'results-loaded'); dom.show(this._treeContainer); dom.show(this._previewContainer); - this._preview.layout(); - this._tree.layout(); + this._splitView.layout(this._dim.width); this.focus(); // pick input and a reference to begin with diff --git a/src/vs/editor/contrib/rename/renameInputField.ts b/src/vs/editor/contrib/rename/renameInputField.ts index 2c238faec9..79961cb674 100644 --- a/src/vs/editor/contrib/rename/renameInputField.ts +++ b/src/vs/editor/contrib/rename/renameInputField.ts @@ -116,7 +116,7 @@ export class RenameInputField implements IContentWidget, IDisposable { } private _currentAcceptInput: (() => void) | null = null; - private _currentCancelInput: ((focusEditor) => void) | null = null; + private _currentCancelInput: ((focusEditor: boolean) => void) | null = null; public acceptInput(): void { if (this._currentAcceptInput) { @@ -144,7 +144,7 @@ export class RenameInputField implements IContentWidget, IDisposable { this._hide(); }; - return new Promise(resolve => { + return new Promise(resolve => { this._currentCancelInput = (focusEditor) => { this._currentAcceptInput = null; diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 78bdecb595..6d03ec42c2 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -41,3 +41,7 @@ import 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode'; import 'vs/editor/contrib/wordHighlighter/wordHighlighter'; import 'vs/editor/contrib/wordOperations/wordOperations'; import 'vs/editor/contrib/wordPartOperations/wordPartOperations'; + +// Load up these strings even in VSCode, even if they are not used +// in order to get them translated +import 'vs/editor/common/standaloneStrings'; diff --git a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts index d62da6226c..ee0a123da6 100644 --- a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts +++ b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./accessibilityHelp'; -import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; @@ -31,6 +30,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { contrastBorder, editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; +import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; const CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE = new RawContextKey('accessibilityHelpWidgetVisible', false); @@ -72,31 +72,26 @@ class AccessibilityHelpController extends Disposable } } -const nlsNoSelection = nls.localize("noSelection", "No selection"); -const nlsSingleSelectionRange = nls.localize("singleSelectionRange", "Line {0}, Column {1} ({2} selected)"); -const nlsSingleSelection = nls.localize("singleSelection", "Line {0}, Column {1}"); -const nlsMultiSelectionRange = nls.localize("multiSelectionRange", "{0} selections ({1} characters selected)"); -const nlsMultiSelection = nls.localize("multiSelection", "{0} selections"); function getSelectionLabel(selections: Selection[] | null, charactersSelected: number): string { if (!selections || selections.length === 0) { - return nlsNoSelection; + return AccessibilityHelpNLS.noSelection; } if (selections.length === 1) { if (charactersSelected) { - return strings.format(nlsSingleSelectionRange, selections[0].positionLineNumber, selections[0].positionColumn, charactersSelected); + return strings.format(AccessibilityHelpNLS.singleSelectionRange, selections[0].positionLineNumber, selections[0].positionColumn, charactersSelected); } - return strings.format(nlsSingleSelection, selections[0].positionLineNumber, selections[0].positionColumn); + return strings.format(AccessibilityHelpNLS.singleSelection, selections[0].positionLineNumber, selections[0].positionColumn); } if (charactersSelected) { - return strings.format(nlsMultiSelectionRange, selections.length, charactersSelected); + return strings.format(AccessibilityHelpNLS.multiSelectionRange, selections.length, charactersSelected); } if (selections.length > 0) { - return strings.format(nlsMultiSelection, selections.length); + return strings.format(AccessibilityHelpNLS.multiSelection, selections.length); } return ''; @@ -151,7 +146,7 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { } if (e.equals(KeyMod.CtrlCmd | KeyCode.KEY_E)) { - alert(nls.localize("emergencyConfOn", "Now changing the setting `accessibilitySupport` to 'on'.")); + alert(AccessibilityHelpNLS.emergencyConfOn); this._editor.updateOptions({ accessibilitySupport: 'on' @@ -166,7 +161,7 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { } if (e.equals(KeyMod.CtrlCmd | KeyCode.KEY_H)) { - alert(nls.localize("openingDocs", "Now opening the Editor Accessibility documentation page.")); + alert(AccessibilityHelpNLS.openingDocs); let url = (this._editor.getRawConfiguration()).accessibilityHelpUrl; if (typeof url === 'undefined') { @@ -246,56 +241,52 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { if (opts.wrappingInfo.inDiffEditor) { if (opts.readOnly) { - text += nls.localize("readonlyDiffEditor", " in a read-only pane of a diff editor."); + text += AccessibilityHelpNLS.readonlyDiffEditor; } else { - text += nls.localize("editableDiffEditor", " in a pane of a diff editor."); + text += AccessibilityHelpNLS.editableDiffEditor; } } else { if (opts.readOnly) { - text += nls.localize("readonlyEditor", " in a read-only code editor"); + text += AccessibilityHelpNLS.readonlyEditor; } else { - text += nls.localize("editableEditor", " in a code editor"); + text += AccessibilityHelpNLS.editableEditor; } } const turnOnMessage = ( platform.isMacintosh - ? nls.localize("changeConfigToOnMac", "To configure the editor to be optimized for usage with a Screen Reader press Command+E now.") - : nls.localize("changeConfigToOnWinLinux", "To configure the editor to be optimized for usage with a Screen Reader press Control+E now.") + ? AccessibilityHelpNLS.changeConfigToOnMac + : AccessibilityHelpNLS.changeConfigToOnWinLinux ); switch (opts.accessibilitySupport) { case AccessibilitySupport.Unknown: text += '\n\n - ' + turnOnMessage; break; case AccessibilitySupport.Enabled: - text += '\n\n - ' + nls.localize("auto_on", "The editor is configured to be optimized for usage with a Screen Reader."); + text += '\n\n - ' + AccessibilityHelpNLS.auto_on; break; case AccessibilitySupport.Disabled: - text += '\n\n - ' + nls.localize("auto_off", "The editor is configured to never be optimized for usage with a Screen Reader, which is not the case at this time."); + text += '\n\n - ' + AccessibilityHelpNLS.auto_off; text += ' ' + turnOnMessage; break; } - const NLS_TAB_FOCUS_MODE_ON = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior by pressing {0}."); - const NLS_TAB_FOCUS_MODE_ON_NO_KB = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding."); - const NLS_TAB_FOCUS_MODE_OFF = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}."); - const NLS_TAB_FOCUS_MODE_OFF_NO_KB = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding."); if (opts.tabFocusMode) { - text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, NLS_TAB_FOCUS_MODE_ON, NLS_TAB_FOCUS_MODE_ON_NO_KB); + text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOnMsg, AccessibilityHelpNLS.tabFocusModeOnMsgNoKb); } else { - text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, NLS_TAB_FOCUS_MODE_OFF, NLS_TAB_FOCUS_MODE_OFF_NO_KB); + text += '\n\n - ' + this._descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOffMsg, AccessibilityHelpNLS.tabFocusModeOffMsgNoKb); } const openDocMessage = ( platform.isMacintosh - ? nls.localize("openDocMac", "Press Command+H now to open a browser window with more information related to editor accessibility.") - : nls.localize("openDocWinLinux", "Press Control+H now to open a browser window with more information related to editor accessibility.") + ? AccessibilityHelpNLS.openDocMac + : AccessibilityHelpNLS.openDocWinLinux ); text += '\n\n - ' + openDocMessage; - text += '\n\n' + nls.localize("outroMsg", "You can dismiss this tooltip and return to the editor by pressing Escape or Shift+Escape."); + text += '\n\n' + AccessibilityHelpNLS.outroMsg; this._contentDomNode.domNode.appendChild(renderFormattedText(text)); // Per https://www.w3.org/TR/wai-aria/roles#document, Authors SHOULD provide a title or label for documents @@ -337,7 +328,7 @@ class ShowAccessibilityHelpAction extends EditorAction { constructor() { super({ id: 'editor.action.showAccessibilityHelp', - label: nls.localize("ShowAccessibilityHelpAction", "Show Accessibility Help"), + label: AccessibilityHelpNLS.showAccessibilityHelpAction, alias: 'Show Accessibility Help', precondition: null, kbOpts: { diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index c6dcd2fd69..63331082a6 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./inspectTokens'; -import * as nls from 'vs/nls'; import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -21,6 +20,7 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; import { HIGH_CONTRAST, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { InspectTokensNLS } from 'vs/editor/common/standaloneStrings'; class InspectTokensController extends Disposable implements IEditorContribution { @@ -82,7 +82,7 @@ class InspectTokens extends EditorAction { constructor() { super({ id: 'editor.action.inspectTokens', - label: nls.localize('inspectTokens', "Developer: Inspect Tokens"), + label: InspectTokensNLS.inspectTokensAction, alias: 'Developer: Inspect Tokens', precondition: null }); diff --git a/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts b/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts index 46ed0fc48c..701ac49c8d 100644 --- a/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts +++ b/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./gotoLine'; -import * as nls from 'vs/nls'; +import * as strings from 'vs/base/common/strings'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { QuickOpenEntry, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; @@ -17,6 +17,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import { BaseEditorQuickOpenAction, IDecorator } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { GoToLineNLS } from 'vs/editor/common/standaloneStrings'; interface ParseResult { position: Position; @@ -62,14 +63,14 @@ export class GotoLineEntry extends QuickOpenEntry { if (isValid) { if (position.column && position.column > 1) { - label = nls.localize('gotoLineLabelValidLineAndColumn', "Go to line {0} and character {1}", position.lineNumber, position.column); + label = strings.format(GoToLineNLS.gotoLineLabelValidLineAndColumn, position.lineNumber, position.column); } else { - label = nls.localize('gotoLineLabelValidLine', "Go to line {0}", position.lineNumber, position.column); + label = strings.format(GoToLineNLS.gotoLineLabelValidLine, position.lineNumber); } } else if (position.lineNumber < 1 || position.lineNumber > (model ? model.getLineCount() : 0)) { - label = nls.localize('gotoLineLabelEmptyWithLineLimit', "Type a line number between 1 and {0} to navigate to", model ? model.getLineCount() : 0); + label = strings.format(GoToLineNLS.gotoLineLabelEmptyWithLineLimit, model ? model.getLineCount() : 0); } else { - label = nls.localize('gotoLineLabelEmptyWithLineAndColumnLimit', "Type a character between 1 and {0} to navigate to", model ? model.getLineMaxColumn(position.lineNumber) : 0); + label = strings.format(GoToLineNLS.gotoLineLabelEmptyWithLineAndColumnLimit, model ? model.getLineMaxColumn(position.lineNumber) : 0); } return { @@ -86,7 +87,7 @@ export class GotoLineEntry extends QuickOpenEntry { getAriaLabel(): string { const position = this.editor.getPosition(); const currentLine = position ? position.lineNumber : 0; - return nls.localize('gotoLineAriaLabel', "Current Line: {0}. Go to line {0}.", currentLine, this.parseResult.label); + return strings.format(GoToLineNLS.gotoLineAriaLabel, currentLine, this.parseResult.label); } run(mode: Mode, _context: IEntryRunContext): boolean { @@ -144,9 +145,9 @@ export class GotoLineEntry extends QuickOpenEntry { export class GotoLineAction extends BaseEditorQuickOpenAction { constructor() { - super(nls.localize('gotoLineActionInput', "Type a line number, followed by an optional colon and a character number to navigate to"), { + super(GoToLineNLS.gotoLineActionInput, { id: 'editor.action.gotoLine', - label: nls.localize('GotoLineAction.label', "Go to Line..."), + label: GoToLineNLS.gotoLineActionLabel, alias: 'Go to Line...', precondition: null, kbOpts: { diff --git a/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts b/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts index 15f3ea345e..2279303383 100644 --- a/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts +++ b/src/vs/editor/standalone/browser/quickOpen/quickCommand.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import * as strings from 'vs/base/common/strings'; import * as browser from 'vs/base/browser/browser'; import { onUnexpectedError } from 'vs/base/common/errors'; import { matchesFuzzy } from 'vs/base/common/filters'; @@ -17,6 +17,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { BaseEditorQuickOpenAction } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { QuickCommandNLS } from 'vs/editor/common/standaloneStrings'; export class EditorActionCommandEntry extends QuickOpenEntryGroup { private readonly key: string; @@ -40,10 +41,10 @@ export class EditorActionCommandEntry extends QuickOpenEntryGroup { public getAriaLabel(): string { if (this.keyAriaLabel) { - return nls.localize('ariaLabelEntryWithKey', "{0}, {1}, commands", this.getLabel(), this.keyAriaLabel); + return strings.format(QuickCommandNLS.ariaLabelEntryWithKey, this.getLabel(), this.keyAriaLabel); } - return nls.localize('ariaLabelEntry', "{0}, commands", this.getLabel()); + return strings.format(QuickCommandNLS.ariaLabelEntry, this.getLabel()); } public getGroupLabel(): string { @@ -77,9 +78,9 @@ export class EditorActionCommandEntry extends QuickOpenEntryGroup { export class QuickCommandAction extends BaseEditorQuickOpenAction { constructor() { - super(nls.localize('quickCommandActionInput', "Type the name of an action you want to execute"), { + super(QuickCommandNLS.quickCommandActionInput, { id: 'editor.action.quickCommand', - label: nls.localize('QuickCommandAction.label', "Command Palette"), + label: QuickCommandNLS.quickCommandActionLabel, alias: 'Command Palette', precondition: null, kbOpts: { diff --git a/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts b/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts index 8cda0eb843..f8e059ab08 100644 --- a/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts +++ b/src/vs/editor/standalone/browser/quickOpen/quickOutline.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./quickOutline'; -import * as nls from 'vs/nls'; import { CancellationToken } from 'vs/base/common/cancellation'; import { matchesFuzzy } from 'vs/base/common/filters'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -20,6 +19,7 @@ import { DocumentSymbol, DocumentSymbolProviderRegistry, symbolKindToCssClass } import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; import { BaseEditorQuickOpenAction, IDecorator } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { QuickOutlineNLS } from 'vs/editor/common/standaloneStrings'; let SCOPE_PREFIX = ':'; @@ -48,7 +48,7 @@ export class SymbolEntry extends QuickOpenEntryGroup { } public getAriaLabel(): string { - return nls.localize('entryAriaLabel', "{0}, symbols", this.name); + return strings.format(QuickOutlineNLS.entryAriaLabel, this.name); } public getIcon(): string { @@ -111,9 +111,9 @@ export class SymbolEntry extends QuickOpenEntryGroup { export class QuickOutlineAction extends BaseEditorQuickOpenAction { constructor() { - super(nls.localize('quickOutlineActionInput', "Type the name of an identifier you wish to navigate to"), { + super(QuickOutlineNLS.quickOutlineActionInput, { id: 'editor.action.quickOutline', - label: nls.localize('QuickOutlineAction.label', "Go to Symbol..."), + label: QuickOutlineNLS.quickOutlineActionLabel, alias: 'Go to Symbol...', precondition: EditorContextKeys.hasDocumentSymbolProvider, kbOpts: { @@ -249,7 +249,7 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction { // Mark first entry as outline else if (results.length > 0) { - results[0].setGroupLabel(nls.localize('symbols', "symbols ({0})", results.length)); + results[0].setGroupLabel(strings.format(QuickOutlineNLS._symbols_, results.length)); } return results; @@ -257,16 +257,16 @@ export class QuickOutlineAction extends BaseEditorQuickOpenAction { private typeToLabel(type: string, count: number): string { switch (type) { - case 'module': return nls.localize('modules', "modules ({0})", count); - case 'class': return nls.localize('class', "classes ({0})", count); - case 'interface': return nls.localize('interface', "interfaces ({0})", count); - case 'method': return nls.localize('method', "methods ({0})", count); - case 'function': return nls.localize('function', "functions ({0})", count); - case 'property': return nls.localize('property', "properties ({0})", count); - case 'variable': return nls.localize('variable', "variables ({0})", count); - case 'var': return nls.localize('variable2', "variables ({0})", count); - case 'constructor': return nls.localize('_constructor', "constructors ({0})", count); - case 'call': return nls.localize('call', "calls ({0})", count); + case 'module': return strings.format(QuickOutlineNLS._modules_, count); + case 'class': return strings.format(QuickOutlineNLS._class_, count); + case 'interface': return strings.format(QuickOutlineNLS._interface_, count); + case 'method': return strings.format(QuickOutlineNLS._method_, count); + case 'function': return strings.format(QuickOutlineNLS._function_, count); + case 'property': return strings.format(QuickOutlineNLS._property_, count); + case 'variable': return strings.format(QuickOutlineNLS._variable_, count); + case 'var': return strings.format(QuickOutlineNLS._variable2_, count); + case 'constructor': return strings.format(QuickOutlineNLS._constructor_, count); + case 'call': return strings.format(QuickOutlineNLS._call_, count); } return type; diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index d0c3c5fa7d..561422bd94 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; +import * as strings from 'vs/base/common/strings'; import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Emitter, Event } from 'vs/base/common/event'; @@ -43,6 +43,7 @@ import { ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/ import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILayoutService, IDimension } from 'vs/platform/layout/browser/layoutService'; +import { SimpleServicesNLS } from 'vs/editor/common/standaloneStrings'; export class SimpleModel implements IResolvedTextEditorModel { @@ -612,7 +613,7 @@ export class SimpleBulkEditService implements IBulkEditService { return Promise.resolve({ selection: undefined, - ariaSummary: localize('summary', 'Made {0} edits in {1} files', totalEdits, totalFiles) + ariaSummary: strings.format(SimpleServicesNLS.bulkEditServiceSummary, totalEdits, totalFiles) }); } } diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index f8c458eb0e..9082b724ec 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { Disposable, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -29,6 +28,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { StandaloneCodeEditorNLS } from 'vs/editor/common/standaloneStrings'; /** * Description of an action contribution @@ -168,11 +168,11 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon @IAccessibilityService accessibilityService: IAccessibilityService ) { options = options || {}; - options.ariaLabel = options.ariaLabel || nls.localize('editorViewAccessibleLabel', "Editor content"); + options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel; options.ariaLabel = options.ariaLabel + ';' + ( browser.isIE - ? nls.localize('accessibilityHelpMessageIE', "Press Ctrl+F1 for Accessibility Options.") - : nls.localize('accessibilityHelpMessage', "Press Alt+F1 for Accessibility Options.") + ? StandaloneCodeEditorNLS.accessibilityHelpMessageIE + : StandaloneCodeEditorNLS.accessibilityHelpMessage ); super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); diff --git a/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts b/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts index 9d6d9fbbc7..8064f324c6 100644 --- a/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts +++ b/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; +import { ToggleHighContrastNLS } from 'vs/editor/common/standaloneStrings'; class ToggleHighContrast extends EditorAction { @@ -15,7 +15,7 @@ class ToggleHighContrast extends EditorAction { constructor() { super({ id: 'editor.action.toggleHighContrast', - label: nls.localize('toggleHighContrast', "Toggle High Contrast Theme"), + label: ToggleHighContrastNLS.toggleHighContrast, alias: 'Toggle High Contrast Theme', precondition: null }); diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index afa227a26e..ddae0c77b2 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -18,7 +18,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { test('TextToHtmlTokenizer 1', () => { let mode = new Mode(); - let support = TokenizationRegistry.get(mode.getId()); + let support = TokenizationRegistry.get(mode.getId())!; let actual = tokenizeToString('.abc..def...gh', support); let expected = [ @@ -38,7 +38,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { test('TextToHtmlTokenizer 2', () => { let mode = new Mode(); - let support = TokenizationRegistry.get(mode.getId()); + let support = TokenizationRegistry.get(mode.getId())!; let actual = tokenizeToString('.abc..def...gh\n.abc..def...gh', support); let expected1 = [ diff --git a/src/vs/platform/actions/browser/menuItemActionItem.ts b/src/vs/platform/actions/browser/menuItemActionItem.ts index 9e47d188b9..6f2d8cac33 100644 --- a/src/vs/platform/actions/browser/menuItemActionItem.ts +++ b/src/vs/platform/actions/browser/menuItemActionItem.ts @@ -90,10 +90,10 @@ export function fillInActionBarActions(menu: IMenu, options: IMenuActionOptions } // {{SQL CARBON EDIT}} add export modifier -export function fillInActions(groups: [string, Array][], target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, getAlternativeActions, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { +export function fillInActions(groups: [string, Array][], target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { for (let tuple of groups) { let [group, actions] = tuple; - if (getAlternativeActions) { + if (useAlternativeActions) { actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index ec9613caf5..a18e94b144 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -65,7 +65,7 @@ export const enum MenuId { DebugConsoleContext, DebugVariablesContext, DebugWatchContext, - DebugToolbar, + DebugToolBar, EditorContext, EditorTitle, EditorTitleContext, diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index c6eb1d723a..0d71cbe5ce 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -29,6 +29,8 @@ export interface IConfigurationOverrides { export const enum ConfigurationTarget { USER = 1, + USER_LOCAL, + USER_REMOTE, WORKSPACE, WORKSPACE_FOLDER, DEFAULT, @@ -37,6 +39,8 @@ export const enum ConfigurationTarget { export function ConfigurationTargetToString(configurationTarget: ConfigurationTarget) { switch (configurationTarget) { case ConfigurationTarget.USER: return 'USER'; + case ConfigurationTarget.USER_LOCAL: return 'USER_LOCAL'; + case ConfigurationTarget.USER_REMOTE: return 'USER_REMOTE'; case ConfigurationTarget.WORKSPACE: return 'WORKSPACE'; case ConfigurationTarget.WORKSPACE_FOLDER: return 'WORKSPACE_FOLDER'; case ConfigurationTarget.DEFAULT: return 'DEFAULT'; @@ -88,6 +92,8 @@ export interface IConfigurationService { inspect(key: string, overrides?: IConfigurationOverrides): { default: T, user: T, + userLocal?: T, + userRemote?: T, workspace?: T, workspaceFolder?: T, memory?: T, diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 732cfcd0fc..12c68325ee 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -36,6 +36,10 @@ export class ConfigurationModel implements IConfigurationModel { return this.checkAndFreeze(this._keys); } + isEmpty(): boolean { + return this._keys.length === 0 && Object.keys(this._contents).length === 0 && this._overrides.length === 0; + } + getValue(section: string | undefined): V { return section ? getConfigurationValue(this.contents, section) : this.contents; } @@ -284,7 +288,8 @@ export class Configuration { constructor( private _defaultConfiguration: ConfigurationModel, - private _userConfiguration: ConfigurationModel, + private _localUserConfiguration: ConfigurationModel, + private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(), private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), private _folderConfigurations: ResourceMap = new ResourceMap(), private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(), @@ -323,6 +328,8 @@ export class Configuration { inspect(key: string, overrides: IConfigurationOverrides, workspace: Workspace | undefined): { default: C, user: C, + userLocal?: C, + userRemote?: C, workspace?: C, workspaceFolder?: C memory?: C @@ -333,7 +340,9 @@ export class Configuration { const memoryConfigurationModel = overrides.resource ? this._memoryConfigurationByResource.get(overrides.resource) || this._memoryConfiguration : this._memoryConfiguration; return { default: overrides.overrideIdentifier ? this._defaultConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._defaultConfiguration.freeze().getValue(key), - user: overrides.overrideIdentifier ? this._userConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._userConfiguration.freeze().getValue(key), + user: overrides.overrideIdentifier ? this.userConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.userConfiguration.freeze().getValue(key), + userLocal: overrides.overrideIdentifier ? this.localUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.localUserConfiguration.freeze().getValue(key), + userRemote: overrides.overrideIdentifier ? this.remoteUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.remoteUserConfiguration.freeze().getValue(key), workspace: workspace ? overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._workspaceConfiguration.freeze().getValue(key) : undefined, //Check on workspace exists or not because _workspaceConfiguration is never null workspaceFolder: folderConfigurationModel ? overrides.overrideIdentifier ? folderConfigurationModel.freeze().override(overrides.overrideIdentifier).getValue(key) : folderConfigurationModel.freeze().getValue(key) : undefined, memory: overrides.overrideIdentifier ? memoryConfigurationModel.override(overrides.overrideIdentifier).getValue(key) : memoryConfigurationModel.getValue(key), @@ -350,7 +359,7 @@ export class Configuration { const folderConfigurationModel = this.getFolderConfigurationModelForResource(undefined, workspace); return { default: this._defaultConfiguration.freeze().keys, - user: this._userConfiguration.freeze().keys, + user: this.userConfiguration.freeze().keys, workspace: this._workspaceConfiguration.freeze().keys, workspaceFolder: folderConfigurationModel ? folderConfigurationModel.freeze().keys : [] }; @@ -362,8 +371,16 @@ export class Configuration { this._foldersConsolidatedConfigurations.clear(); } - updateUserConfiguration(userConfiguration: ConfigurationModel): void { - this._userConfiguration = userConfiguration; + updateLocalUserConfiguration(localUserConfiguration: ConfigurationModel): void { + this._localUserConfiguration = localUserConfiguration; + this._userConfiguration = null; + this._workspaceConsolidatedConfiguration = null; + this._foldersConsolidatedConfigurations.clear(); + } + + updateRemoteUserConfiguration(remoteUserConfiguration: ConfigurationModel): void { + this._remoteUserConfiguration = remoteUserConfiguration; + this._userConfiguration = null; this._workspaceConsolidatedConfiguration = null; this._foldersConsolidatedConfigurations.clear(); } @@ -380,7 +397,7 @@ export class Configuration { } deleteFolderConfiguration(resource: URI): void { - this.folders.delete(resource); + this.folderConfigurations.delete(resource); this._foldersConsolidatedConfigurations.delete(resource); } @@ -388,15 +405,30 @@ export class Configuration { return this._defaultConfiguration; } - get user(): ConfigurationModel { + private _userConfiguration: ConfigurationModel | null; + get userConfiguration(): ConfigurationModel { + if (!this._userConfiguration) { + this._userConfiguration = this._remoteUserConfiguration.isEmpty() ? this._localUserConfiguration : this._localUserConfiguration.merge(this._remoteUserConfiguration); + if (this._freeze) { + this._userConfiguration.freeze(); + } + } return this._userConfiguration; } - get workspace(): ConfigurationModel { + get localUserConfiguration(): ConfigurationModel { + return this._localUserConfiguration; + } + + get remoteUserConfiguration(): ConfigurationModel { + return this._remoteUserConfiguration; + } + + get workspaceConfiguration(): ConfigurationModel { return this._workspaceConfiguration; } - protected get folders(): ResourceMap { + protected get folderConfigurations(): ResourceMap { return this._folderConfigurations; } @@ -424,7 +456,7 @@ export class Configuration { private getWorkspaceConsolidatedConfiguration(): ConfigurationModel { if (!this._workspaceConsolidatedConfiguration) { - this._workspaceConsolidatedConfiguration = this._defaultConfiguration.merge(this._userConfiguration, this._workspaceConfiguration, this._memoryConfiguration); + this._workspaceConsolidatedConfiguration = this._defaultConfiguration.merge(this.userConfiguration, this._workspaceConfiguration, this._memoryConfiguration); if (this._freeze) { this._workspaceConfiguration = this._workspaceConfiguration.freeze(); } @@ -468,9 +500,9 @@ export class Configuration { keys: this._defaultConfiguration.keys }, user: { - contents: this._userConfiguration.contents, - overrides: this._userConfiguration.overrides, - keys: this._userConfiguration.keys + contents: this.userConfiguration.contents, + overrides: this.userConfiguration.overrides, + keys: this.userConfiguration.keys }, workspace: { contents: this._workspaceConfiguration.contents, @@ -489,7 +521,7 @@ export class Configuration { allKeys(workspace: Workspace | undefined): string[] { let keys = this.keys(workspace); let all = [...keys.default]; - const addKeys = (keys) => { + const addKeys = (keys: string[]) => { for (const key of keys) { if (all.indexOf(key) === -1) { all.push(key); @@ -498,8 +530,8 @@ export class Configuration { }; addKeys(keys.user); addKeys(keys.workspace); - for (const resource of this.folders.keys()) { - addKeys(this.folders.get(resource)!.keys); + for (const resource of this.folderConfigurations.keys()) { + addKeys(this.folderConfigurations.get(resource)!.keys); } return all; } diff --git a/src/vs/platform/configuration/node/configuration.ts b/src/vs/platform/configuration/node/configuration.ts index 5a62020f3e..96a4615a61 100644 --- a/src/vs/platform/configuration/node/configuration.ts +++ b/src/vs/platform/configuration/node/configuration.ts @@ -3,13 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } 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 UserConfiguration extends Disposable { +export class NodeBasedUserConfiguration extends Disposable { private userConfigModelWatcher: ConfigWatcher; private initializePromise: Promise; @@ -50,4 +54,53 @@ export class UserConfiguration 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.watchFileChanges(this.configurationResource); + this._register(toDisposable(() => this.fileService.unwatchFileChanges(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(); + } + } } \ No newline at end of file diff --git a/src/vs/platform/configuration/node/configurationService.ts b/src/vs/platform/configuration/node/configurationService.ts index ff86837d30..cccbdefcde 100644 --- a/src/vs/platform/configuration/node/configurationService.ts +++ b/src/vs/platform/configuration/node/configurationService.ts @@ -11,14 +11,14 @@ import { DefaultConfigurationModel, Configuration, ConfigurationChangeEvent, Con import { Event, Emitter } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { UserConfiguration } from 'vs/platform/configuration/node/configuration'; +import { NodeBasedUserConfiguration } from 'vs/platform/configuration/node/configuration'; export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable { _serviceBrand: any; private _configuration: Configuration; - private userConfiguration: UserConfiguration; + private userConfiguration: NodeBasedUserConfiguration; private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; @@ -28,7 +28,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe ) { super(); - this.userConfiguration = this._register(new UserConfiguration(environmentService.appSettingsPath)); + this.userConfiguration = this._register(new NodeBasedUserConfiguration(environmentService.appSettingsPath)); // Initialize const defaults = new DefaultConfigurationModel(); @@ -91,10 +91,10 @@ export class ConfigurationService extends Disposable implements IConfigurationSe } private onDidChangeUserConfiguration(userConfigurationModel: ConfigurationModel): void { - const { added, updated, removed } = compare(this._configuration.user, userConfigurationModel); + const { added, updated, removed } = compare(this._configuration.localUserConfiguration, userConfigurationModel); const changedKeys = [...added, ...updated, ...removed]; if (changedKeys.length) { - this._configuration.updateUserConfiguration(userConfigurationModel); + this._configuration.updateLocalUserConfiguration(userConfigurationModel); this.trigger(changedKeys, ConfigurationTarget.USER); } } @@ -113,7 +113,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe case ConfigurationTarget.DEFAULT: return this._configuration.defaults.contents; case ConfigurationTarget.USER: - return this._configuration.user.contents; + return this._configuration.localUserConfiguration.contents; } return {}; } diff --git a/src/vs/platform/configuration/test/common/testConfigurationService.ts b/src/vs/platform/configuration/test/common/testConfigurationService.ts index 26eb0ac1a3..3908ce5de1 100644 --- a/src/vs/platform/configuration/test/common/testConfigurationService.ts +++ b/src/vs/platform/configuration/test/common/testConfigurationService.ts @@ -56,6 +56,8 @@ export class TestConfigurationService implements IConfigurationService { public inspect(key: string, overrides?: IConfigurationOverrides): { default: T, user: T, + userLocal?: T, + userRemote?: T, workspace?: T, workspaceFolder?: T value: T, diff --git a/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts index bd5e909bcd..ade627654e 100644 --- a/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/electron-main/diagnosticsService.ts @@ -26,7 +26,7 @@ export interface IDiagnosticsService { formatEnvironment(info: IMainProcessInfo): string; getPerformanceInfo(info: IMainProcessInfo): Promise; getSystemInfo(info: IMainProcessInfo): SystemInfo; - printDiagnostics(info: IMainProcessInfo): Promise; + getDiagnostics(info: IMainProcessInfo): Promise; } export interface VersionInfo { @@ -156,28 +156,29 @@ export class DiagnosticsService implements IDiagnosticsService { return systemInfo; } - printDiagnostics(info: IMainProcessInfo): Promise { + getDiagnostics(info: IMainProcessInfo): Promise { + const output: string[] = []; return listProcesses(info.mainPID).then(rootProcess => { // Environment Info - console.log(''); - console.log(this.formatEnvironment(info)); + output.push(''); + output.push(this.formatEnvironment(info)); // Process List - console.log(''); - console.log(this.formatProcessList(info, rootProcess)); + output.push(''); + output.push(this.formatProcessList(info, rootProcess)); // Workspace Stats const workspaceStatPromises: Promise[] = []; if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) { - console.log(''); - console.log('Workspace Stats: '); + output.push(''); + output.push('Workspace Stats: '); info.windows.forEach(window => { if (window.folderURIs.length === 0) { return; } - console.log(`| Window (${window.title})`); + output.push(`| Window (${window.title})`); window.folderURIs.forEach(uriComponents => { const folderUri = URI.revive(uriComponents); @@ -188,22 +189,24 @@ export class DiagnosticsService implements IDiagnosticsService { if (stats.maxFilesReached) { countMessage = `more than ${countMessage}`; } - console.log(`| Folder (${basename(folder)}): ${countMessage}`); - console.log(this.formatWorkspaceStats(stats)); + output.push(`| Folder (${basename(folder)}): ${countMessage}`); + output.push(this.formatWorkspaceStats(stats)); }).catch(error => { - console.log(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`); + output.push(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`); })); } else { - console.log(`| Folder (${folderUri.toString()}): Workspace stats not available.`); + output.push(`| Folder (${folderUri.toString()}): Workspace stats not available.`); } }); }); } return Promise.all(workspaceStatPromises).then(() => { - console.log(''); - console.log(''); + output.push(''); + output.push(''); + + return output.join('\n'); }); }); } @@ -340,7 +343,7 @@ function asSortedItems(map: Map): WorkspaceStatItem[] { // const errors: ParseError[] = []; // const json = parse(contents.toString(), errors); // if (errors.length) { -// console.log(`Unable to parse ${launchConfig}`); +// output.push(`Unable to parse ${launchConfig}`); // return resolve([]); // } diff --git a/src/vs/platform/dialogs/browser/dialogService.ts b/src/vs/platform/dialogs/browser/dialogService.ts new file mode 100644 index 0000000000..c2fa9b5d34 --- /dev/null +++ b/src/vs/platform/dialogs/browser/dialogService.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 * as nls from 'vs/nls'; +import { IDialogService, IDialogOptions, IConfirmation, IConfirmationResult, DialogType } from 'vs/platform/dialogs/common/dialogs'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { ILogService } from 'vs/platform/log/common/log'; +import Severity from 'vs/base/common/severity'; +import { Dialog } from 'vs/base/browser/ui/dialog/dialog'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { attachDialogStyler } from 'vs/platform/theme/common/styler'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; + +export class DialogService implements IDialogService { + _serviceBrand: any; + + constructor( + @ILogService private readonly logService: ILogService, + @ILayoutService private readonly layoutService: ILayoutService, + @IThemeService private readonly themeService: IThemeService + ) { } + + async confirm(confirmation: IConfirmation): Promise { + this.logService.trace('DialogService#confirm', confirmation.message); + + const buttons: string[] = []; + if (confirmation.primaryButton) { + buttons.push(confirmation.primaryButton); + } else { + buttons.push(nls.localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes")); + } + + if (confirmation.secondaryButton) { + buttons.push(confirmation.secondaryButton); + } else if (typeof confirmation.secondaryButton === 'undefined') { + buttons.push(nls.localize('cancelButton', "Cancel")); + } + + const severity = this.getSeverity(confirmation.type || 'none'); + const result = await this.show(severity, confirmation.message, buttons, { cancelId: 1, detail: confirmation.detail }); + + return { confirmed: result === 0 }; + } + + private getSeverity(type: DialogType): Severity { + switch (type) { + case 'error': + return Severity.Error; + case 'warning': + return Severity.Warning; + case 'question': + case 'info': + return Severity.Info; + case 'none': + default: + return Severity.Ignore; + } + } + + private getDialogType(severity: Severity): DialogType { + return (severity === Severity.Info) ? 'question' : (severity === Severity.Error) ? 'error' : (severity === Severity.Warning) ? 'warning' : 'none'; + } + + + async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { + this.logService.trace('DialogService#show', message); + + const dialogDisposables: IDisposable[] = []; + const dialog = new Dialog( + this.layoutService.container, + message, + buttons, + { + detail: options ? options.detail : undefined, + cancelId: options ? options.cancelId : undefined, + type: this.getDialogType(severity) + }); + + dialogDisposables.push(dialog); + dialogDisposables.push(attachDialogStyler(dialog, this.themeService)); + + const choice = await dialog.show(); + dispose(dialogDisposables); + + return choice; + } +} + +registerSingleton(IDialogService, DialogService, true); diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index f75917be28..c19c583d31 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -11,9 +11,11 @@ import { localize } from 'vs/nls'; import { FileFilter } from 'vs/platform/windows/common/windows'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning'; + export interface IConfirmation { title?: string; - type?: 'none' | 'info' | 'error' | 'question' | 'warning'; + type?: DialogType; message: string; detail?: string; primaryButton?: string; diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index 7b37a90c0a..fab602c592 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -8,7 +8,7 @@ import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/node/ipc'; +import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { SimpleKeybinding, KeyCode } from 'vs/base/common/keyCodes'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; import { OS } from 'vs/base/common/platform'; diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts index cc1174c07e..f3efafb598 100644 --- a/src/vs/platform/driver/node/driver.ts +++ b/src/vs/platform/driver/node/driver.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { connect as connectNet, Client } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client } from 'vs/base/parts/ipc/common/ipc.net'; +import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 944d35a004..c8b906ee91 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -7,6 +7,8 @@ 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 { writeFileSync } from 'fs'; /** * This code is also used by standalone cli's. Avoid adding any other dependencies. @@ -263,3 +265,20 @@ export function addArg(argv: string[], ...args: string[]): string[] { return argv; } + +export function createWaitMarkerFile(verbose?: boolean): string | undefined { + const randomWaitMarkerPath = join(os.tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10)); + + try { + writeFileSync(randomWaitMarkerPath, ''); + if (verbose) { + console.log(`Marker file for --wait created: ${randomWaitMarkerPath}`); + } + return randomWaitMarkerPath; + } catch (err) { + if (verbose) { + console.error(`Failed to create marker file for --wait: ${err}`); + } + return undefined; + } +} diff --git a/src/vs/platform/environment/node/argvHelper.ts b/src/vs/platform/environment/node/argvHelper.ts index 20a0f94943..ca026ebe8e 100644 --- a/src/vs/platform/environment/node/argvHelper.ts +++ b/src/vs/platform/environment/node/argvHelper.ts @@ -4,14 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { tmpdir } from 'os'; import { firstIndex } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; import { ParsedArgs } from '../common/environment'; import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files'; import { parseArgs } from 'vs/platform/environment/node/argv'; -import { join } from 'vs/base/common/path'; -import { writeFile } from 'vs/base/node/pfs'; function validate(args: ParsedArgs): ParsedArgs { if (args.goto) { @@ -60,21 +57,3 @@ export function parseCLIProcessArgv(processArgv: string[]): ParsedArgs { return validate(parseArgs(args)); } - -export function createWaitMarkerFile(verbose?: boolean): Promise { - const randomWaitMarkerPath = join(tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10)); - - return writeFile(randomWaitMarkerPath, '').then(() => { - if (verbose) { - console.log(`Marker file for --wait created: ${randomWaitMarkerPath}`); - } - - return randomWaitMarkerPath; - }, error => { - if (verbose) { - console.error(`Failed to create marker file for --wait: ${error}`); - } - - return Promise.resolve(undefined); - }); -} diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 1fa830a770..ba2830b40c 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -56,6 +56,11 @@ export interface IFileService { */ canHandleResource(resource: URI): boolean; + /** + * Checks if the provider for the provided resource has the provided file system capability. + */ + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise; + //#endregion /** @@ -188,6 +193,7 @@ export interface FileOpenOptions { export interface FileDeleteOptions { recursive: boolean; + useTrash: boolean; } export enum FileType { @@ -215,7 +221,9 @@ export const enum FileSystemProviderCapabilities { FileFolderCopy = 1 << 3, PathCaseSensitive = 1 << 10, - Readonly = 1 << 11 + Readonly = 1 << 11, + + Trash = 1 << 12 } export interface IFileSystemProvider { @@ -243,6 +251,34 @@ export interface IFileSystemProvider { write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; } +export interface IFileSystemProviderWithFileReadWriteCapability extends IFileSystemProvider { + readFile(resource: URI): Promise; + writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise; +} + +export function hasReadWriteCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileReadWriteCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite); +} + +export interface IFileSystemProviderWithFileFolderCopyCapability extends IFileSystemProvider { + copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise; +} + +export function hasFileFolderCopyCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileFolderCopyCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy); +} + +export interface IFileSystemProviderWithOpenReadWriteCloseCapability extends IFileSystemProvider { + open(resource: URI, opts: FileOpenOptions): Promise; + close(fd: number): Promise; + read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; +} + +export function hasOpenReadWriteCloseCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithOpenReadWriteCloseCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose); +} + export enum FileSystemProviderErrorCode { FileExists = 'EntryExists', FileNotFound = 'EntryNotFound', @@ -1103,8 +1139,6 @@ export interface ILegacyFileService { createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise; - del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; - watchFileChanges(resource: URI): void; unwatchFileChanges(resource: URI): void; diff --git a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts index 1bf8d34da4..c1849e7091 100644 --- a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts +++ b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; -import { Client, connect } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client } from 'vs/base/parts/ipc/common/ipc.net'; +import { connect } from 'vs/base/parts/ipc/node/ipc.net'; import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getDelayedChannel } from 'vs/base/parts/ipc/node/ipc'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; export const ISharedProcessService = createDecorator('sharedProcessService'); diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index 04d6b3a007..37e81f559b 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -89,4 +89,5 @@ export interface IIssueService { _serviceBrand: any; openReporter(data: IssueReporterData): Promise; openProcessExplorer(data: ProcessExplorerData): Promise; + getSystemStatus(): Promise; } diff --git a/src/vs/platform/issue/electron-browser/issueService.ts b/src/vs/platform/issue/electron-browser/issueService.ts index 29526af74a..a70412d99d 100644 --- a/src/vs/platform/issue/electron-browser/issueService.ts +++ b/src/vs/platform/issue/electron-browser/issueService.ts @@ -25,4 +25,8 @@ export class IssueService implements IIssueService { openProcessExplorer(data: ProcessExplorerData): Promise { return this.channel.call('openProcessExplorer', data); } + + getSystemStatus(): Promise { + return this.channel.call('getSystemStatus'); + } } diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index ef6a65cd36..b7175ef599 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -214,6 +214,12 @@ export class IssueService implements IIssueService { }); } + public getSystemStatus(): Promise { + return this.launchService.getMainProcessInfo().then(info => { + return this.diagnosticsService.getDiagnostics(info); + }); + } + private getWindowPosition(parentWindow: BrowserWindow, defaultWidth: number, defaultHeight: number): IWindowState { // We want the new window to open on the same display that the parent is in let displayToUse: Electron.Display | undefined; diff --git a/src/vs/platform/issue/node/issueIpc.ts b/src/vs/platform/issue/node/issueIpc.ts index 5b5513c644..7f55676c33 100644 --- a/src/vs/platform/issue/node/issueIpc.ts +++ b/src/vs/platform/issue/node/issueIpc.ts @@ -21,6 +21,8 @@ export class IssueChannel implements IServerChannel { return this.service.openReporter(arg); case 'openProcessExplorer': return this.service.openProcessExplorer(arg); + case 'getSystemStatus': + return this.service.getSystemStatus(); } throw new Error(`Call not found: ${command}`); diff --git a/src/vs/platform/launch/electron-main/launchService.ts b/src/vs/platform/launch/electron-main/launchService.ts index 63d9450f92..7d3f2a0aa1 100644 --- a/src/vs/platform/launch/electron-main/launchService.ts +++ b/src/vs/platform/launch/electron-main/launchService.ts @@ -163,9 +163,11 @@ export class LaunchService implements ILaunchService { const context = !!userEnv['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; let usedWindows: ICodeWindow[] = []; + const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; + // Special case extension development if (!!args.extensionDevelopmentPath) { - this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, { context, cli: args, userEnv }); + this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, { context, cli: args, userEnv, waitMarkerFileURI }); } // Start without file/folder arguments @@ -199,7 +201,14 @@ export class LaunchService implements ILaunchService { } if (openNewWindow) { - usedWindows = this.windowsMainService.open({ context, cli: args, userEnv, forceNewWindow: true, forceEmpty: true }); + usedWindows = this.windowsMainService.open({ + context, + cli: args, + userEnv, + forceNewWindow: true, + forceEmpty: true, + waitMarkerFileURI + }); } else { usedWindows = [this.windowsMainService.focusLastActive(args, context)]; } @@ -216,7 +225,8 @@ export class LaunchService implements ILaunchService { forceReuseWindow: args['reuse-window'], diffMode: args.diff, addMode: args.add, - noRecentEntry: !!args['skip-add-to-recently-opened'] + noRecentEntry: !!args['skip-add-to-recently-opened'], + waitMarkerFileURI }); } @@ -232,10 +242,10 @@ export class LaunchService implements ILaunchService { // If the other instance is waiting to be killed, we hook up a window listener if one window // is being used and only then resolve the startup promise which will kill this second instance. // In addition, we poll for the wait marker file to be deleted to return. - if (args.wait && args.waitMarkerFilePath && usedWindows.length === 1 && usedWindows[0]) { + if (waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { return Promise.race([ this.windowsMainService.waitForWindowCloseOrLoad(usedWindows[0].id), - whenDeleted(args.waitMarkerFilePath) + whenDeleted(waitMarkerFileURI.fsPath) ]).then(() => undefined, () => undefined); } diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index e13a0febf0..b59202d4fa 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -195,6 +195,8 @@ export interface IQuickPick extends IQuickInput { valueSelection: Readonly<[number, number]> | undefined; validationMessage: string | undefined; + + inputHasFocus(): boolean; } export interface IInputBox extends IQuickInput { diff --git a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts new file mode 100644 index 0000000000..bddaea9c66 --- /dev/null +++ b/src/vs/platform/remote/browser/remoteAuthorityResolverService.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 { ResolvedAuthority, IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; + +export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverService { + + _serviceBrand: any; + + constructor() { + } + + resolveAuthority(authority: string): Promise { + if (authority.indexOf(':') >= 0) { + const pieces = authority.split(':'); + return Promise.resolve({ authority, host: pieces[0], port: parseInt(pieces[1], 10) }); + } + return Promise.resolve({ authority, host: authority, port: 80 }); + } + + setResolvedAuthority(resolvedAuthority: ResolvedAuthority) { + throw new Error(`Not implemented`); + } + + setResolvedAuthorityError(authority: string, err: any): void { + throw new Error(`Not implemented`); + } +} diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts new file mode 100644 index 0000000000..7a85b6a2d6 --- /dev/null +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Client, PersistentProtocol, ISocket } from 'vs/base/parts/ipc/common/ipc.net'; +import { generateUuid } from 'vs/base/common/uuid'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { Disposable } from 'vs/base/common/lifecycle'; + +export const enum ConnectionType { + Management = 1, + ExtensionHost = 2, + Tunnel = 3, +} + +interface ISimpleConnectionOptions { + isBuilt: boolean; + commit: string | undefined; + host: string; + port: number; + reconnectionToken: string; + reconnectionProtocol: PersistentProtocol | null; + webSocketFactory: IWebSocketFactory; +} + +export interface IConnectCallback { + (err: any | undefined, socket: ISocket | undefined): void; +} + +export interface IWebSocketFactory { + connect(host: string, port: number, query: string, callback: IConnectCallback): void; +} + +async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise { + throw new Error(`Not implemented`); +} + +interface IManagementConnectionResult { + protocol: PersistentProtocol; +} + +async function doConnectRemoteAgentManagement(options: ISimpleConnectionOptions): Promise { + const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.Management, undefined); + return new Promise((c, e) => { + const registration = protocol.onControlMessage(raw => { + registration.dispose(); + const msg = JSON.parse(raw.toString()); + const error = getErrorFromMessage(msg); + if (error) { + return e(error); + } + if (options.reconnectionProtocol) { + options.reconnectionProtocol.endAcceptReconnection(); + } + c({ protocol }); + }); + }); +} + +export interface IRemoteExtensionHostStartParams { + language: string; + debugId?: string; + break?: boolean; + port?: number | null; + updatePort?: boolean; +} + +interface IExtensionHostConnectionResult { + protocol: PersistentProtocol; + debugPort?: number; +} + +async function doConnectRemoteAgentExtensionHost(options: ISimpleConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise { + const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.ExtensionHost, startArguments); + return new Promise((c, e) => { + const registration = protocol.onControlMessage(raw => { + registration.dispose(); + const msg = JSON.parse(raw.toString()); + const error = getErrorFromMessage(msg); + if (error) { + return e(error); + } + const debugPort = msg && msg.debugPort; + if (options.reconnectionProtocol) { + options.reconnectionProtocol.endAcceptReconnection(); + } + c({ protocol, debugPort }); + }); + }); +} + +export interface ITunnelConnectionStartParams { + port: number; +} + +async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, startParams: ITunnelConnectionStartParams): Promise { + const protocol = await connectToRemoteExtensionHostAgent(options, ConnectionType.Tunnel, startParams); + return protocol; +} + +export interface IConnectionOptions { + isBuilt: boolean; + commit: string | undefined; + webSocketFactory: IWebSocketFactory; + addressProvider: IAddressProvider; +} + +async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise { + const { host, port } = await options.addressProvider.getAddress(); + return { + isBuilt: options.isBuilt, + commit: options.commit, + host: host, + port: port, + reconnectionToken: reconnectionToken, + reconnectionProtocol: reconnectionProtocol, + webSocketFactory: options.webSocketFactory, + }; +} + +export interface IAddress { + host: string; + port: number; +} + +export interface IAddressProvider { + getAddress(): Promise; +} + +export async function connectRemoteAgentManagement(options: IConnectionOptions, remoteAuthority: string, clientId: string): Promise { + const reconnectionToken = generateUuid(); + const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); + const { protocol } = await doConnectRemoteAgentManagement(simpleOptions); + return new ManagementPersistentConnection(options, remoteAuthority, clientId, reconnectionToken, protocol); +} + +export async function connectRemoteAgentExtensionHost(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise { + const reconnectionToken = generateUuid(); + const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); + const { protocol, debugPort } = await doConnectRemoteAgentExtensionHost(simpleOptions, startArguments); + return new ExtensionHostPersistentConnection(options, startArguments, reconnectionToken, protocol, debugPort); +} + +export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise { + const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null); + const protocol = await doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }); + return protocol; +} + +abstract class PersistentConnection extends Disposable { + + protected readonly _options: IConnectionOptions; + public readonly reconnectionToken: string; + public readonly protocol: PersistentProtocol; + + constructor(options: IConnectionOptions, reconnectionToken: string, protocol: PersistentProtocol) { + super(); + this._options = options; + this.reconnectionToken = reconnectionToken; + this.protocol = protocol; + } + + protected abstract _reconnect(options: ISimpleConnectionOptions): Promise; +} + +export class ManagementPersistentConnection extends PersistentConnection { + + public readonly client: Client; + + constructor(options: IConnectionOptions, remoteAuthority: string, clientId: string, reconnectionToken: string, protocol: PersistentProtocol) { + super(options, reconnectionToken, protocol); + this.client = this._register(new Client(protocol, { + remoteAuthority: remoteAuthority, + clientId: clientId + })); + } + + protected async _reconnect(options: ISimpleConnectionOptions): Promise { + await doConnectRemoteAgentManagement(options); + } +} + +export class ExtensionHostPersistentConnection extends PersistentConnection { + + private readonly _startArguments: IRemoteExtensionHostStartParams; + public readonly debugPort: number | undefined; + + constructor(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams, reconnectionToken: string, protocol: PersistentProtocol, debugPort: number | undefined) { + super(options, reconnectionToken, protocol); + this._startArguments = startArguments; + this.debugPort = debugPort; + } + + protected async _reconnect(options: ISimpleConnectionOptions): Promise { + await doConnectRemoteAgentExtensionHost(options, this._startArguments); + } +} + +function getErrorFromMessage(msg: any): Error | null { + if (msg && msg.type === 'error') { + const error = new Error(`Connection error: ${msg.reason}`); + (error).code = 'VSCODE_CONNECTION_ERROR'; + return error; + } + return null; +} diff --git a/src/vs/platform/remote/node/remoteAgentFileSystemChannel.ts b/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts similarity index 91% rename from src/vs/platform/remote/node/remoteAgentFileSystemChannel.ts rename to src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts index a175c2ec64..a917f5fb27 100644 --- a/src/vs/platform/remote/node/remoteAgentFileSystemChannel.ts +++ b/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts @@ -9,6 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, IFileChange, IFileSystemProvider, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { VSBuffer } from 'vs/base/common/buffer'; export const REMOTE_FILE_SYSTEM_CHANNEL_NAME = 'remotefilesystem'; @@ -71,20 +72,17 @@ export class RemoteExtensionsFileSystemProvider extends Disposable implements IF // --- forwarding calls - private static _asBuffer(data: Uint8Array): Buffer { - return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength); - } - stat(resource: URI): Promise { return this._channel.call('stat', [resource]); } - readFile(resource: URI): Promise { - return this._channel.call('readFile', [resource]); + async readFile(resource: URI): Promise { + const buff = await this._channel.call('readFile', [resource]); + return buff.buffer; } writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - const contents = RemoteExtensionsFileSystemProvider._asBuffer(content); + const contents = VSBuffer.wrap(content); return this._channel.call('writeFile', [resource, contents, opts]); } diff --git a/src/vs/platform/remote/node/nodeWebSocketFactory.ts b/src/vs/platform/remote/node/nodeWebSocketFactory.ts new file mode 100644 index 0000000000..dd69ea1a5e --- /dev/null +++ b/src/vs/platform/remote/node/nodeWebSocketFactory.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWebSocketFactory, IConnectCallback } from 'vs/platform/remote/common/remoteAgentConnection'; + +export const nodeWebSocketFactory = new class implements IWebSocketFactory { + connect(host: string, port: number, query: string, callback: IConnectCallback): void { + throw new Error(`Not implemented`); + } +}; diff --git a/src/vs/platform/remote/node/remoteAgentConnection.ts b/src/vs/platform/remote/node/remoteAgentConnection.ts deleted file mode 100644 index 91070c0c90..0000000000 --- a/src/vs/platform/remote/node/remoteAgentConnection.ts +++ /dev/null @@ -1,60 +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 { Client, PersistentProtocol } from 'vs/base/parts/ipc/node/ipc.net'; -import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { Disposable } from 'vs/base/common/lifecycle'; - -export interface IRemoteExtensionHostStartParams { - language: string; - debugId?: string; - break?: boolean; - port?: number | null; - updatePort?: boolean; -} - -export async function connectRemoteAgentManagement(remoteAuthority: string, host: string, port: number, clientId: string, isBuilt: boolean): Promise { - throw new Error(`Not implemented`); -} - -export async function connectRemoteAgentExtensionHost(host: string, port: number, startArguments: IRemoteExtensionHostStartParams, isBuilt: boolean): Promise { - throw new Error(`Not implemented`); -} - -abstract class PersistentConnection extends Disposable { - - public readonly reconnectionToken: string; - public readonly protocol: PersistentProtocol; - - constructor(reconnectionToken: string, protocol: PersistentProtocol) { - super(); - this.reconnectionToken = reconnectionToken; - this.protocol = protocol; - } -} - -export class ManagementPersistentConnection extends PersistentConnection { - - public readonly client: Client; - - constructor(remoteAuthority: string, host: string, port: number, clientId: string, isBuilt: boolean, reconnectionToken: string, protocol: PersistentProtocol) { - super(reconnectionToken, protocol); - - this.client = this._register(new Client(protocol, { - remoteAuthority: remoteAuthority, - clientId: clientId - })); - } -} - -export class ExtensionHostPersistentConnection extends PersistentConnection { - - public readonly debugPort: number | undefined; - - constructor(host: string, port: number, startArguments: IRemoteExtensionHostStartParams, isBuilt: boolean, reconnectionToken: string, protocol: PersistentProtocol, debugPort: number | undefined) { - super(reconnectionToken, protocol); - this.debugPort = debugPort; - } -} diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index fa8c473219..1d24f79e3a 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -259,7 +259,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic return import('vscode-sqlite3').then(sqlite3 => { return new Promise((resolve, reject) => { - const handleSuffixKey = (row, key: string, suffix: string) => { + const handleSuffixKey = (row: any, key: string, suffix: string) => { if (endsWith(key, suffix.toLowerCase())) { const value: string = row.value.toString('utf16le'); const normalizedKey = key.substring(0, key.length - suffix.length) + suffix; diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index d9607f5700..afcbdb361b 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -61,7 +61,7 @@ export interface IColorRegistry { /** * Register a color to the registry. */ - deregisterColor(id: string); + deregisterColor(id: string): void; /** * Get all color contributions diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index dcf70f5e48..4df22261fc 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground } from 'vs/platform/theme/common/colorRegistry'; +import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, editorWidgetBorder, inputValidationInfoForeground, inputValidationWarningForeground, inputValidationErrorForeground, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuBorder, menuSeparatorBackground, darken, listFilterWidgetOutline, listFilterWidgetNoMatchesOutline, listFilterWidgetBackground, editorWidgetBackground } from 'vs/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -329,3 +329,24 @@ export const defaultMenuStyles = { export function attachMenuStyler(widget: IThemable, themeService: IThemeService, style?: IMenuStyleOverrides): IDisposable { return attachStyler(themeService, { ...defaultMenuStyles, ...style }, widget); } + +export interface IDialogStyleOverrides extends IButtonStyleOverrides { + dialogForeground?: ColorIdentifier; + dialogBackground?: ColorIdentifier; + dialogShadow?: ColorIdentifier; +} + +export const defaultDialogStyles = { + dialogBackground: editorWidgetBackground, + dialogForeground: foreground, + dialogShadow: widgetShadow, + buttonForeground: buttonForeground, + buttonBackground: buttonBackground, + buttonHoverBackground: buttonHoverBackground, + buttonBorder: contrastBorder +}; + + +export function attachDialogStyler(widget: IThemable, themeService: IThemeService, style?: IDialogStyleOverrides): IDisposable { + return attachStyler(themeService, { ...defaultDialogStyles, ...style }, widget); +} diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index b29ef5a86d..c4a6f6d947 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -194,7 +194,7 @@ export class Win32UpdateService extends AbstractUpdateService { } private async cleanup(exceptVersion: string | null = null): Promise { - const filter = exceptVersion ? one => !(new RegExp(`${product.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true; + const filter = exceptVersion ? (one: string) => !(new RegExp(`${product.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true; const cachePath = await this.cachePath; const versions = await pfs.readdir(cachePath); diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index c3fc7db3b9..e1a7b18318 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -84,6 +84,7 @@ export interface SaveDialogOptions { export interface INewWindowOptions { remoteAuthority?: string; + reuseWindow?: boolean; } export interface IDevToolsOptions { @@ -149,9 +150,8 @@ export interface IWindowsService { toggleSharedProcess(): Promise; // Global methods - openWindow(windowId: number, uris: IURIToOpen[], options?: IOpenSettings): Promise; + openWindow(windowId: number, uris: IURIToOpen[], options: IOpenSettings): Promise; openNewWindow(options?: INewWindowOptions): Promise; - showWindow(windowId: number): Promise; getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]>; getWindowCount(): Promise; log(severity: string, ...messages: string[]): Promise; @@ -183,6 +183,7 @@ export interface IOpenSettings { diffMode?: boolean; addMode?: boolean; noRecentEntry?: boolean; + waitMarkerFileURI?: URI; args?: ParsedArgs; } @@ -228,7 +229,6 @@ export interface IWindowService { unmaximizeWindow(): Promise; minimizeWindow(): Promise; onWindowTitleDoubleClick(): Promise; - show(): Promise; showMessageBox(options: MessageBoxOptions): Promise; showSaveDialog(options: SaveDialogOptions): Promise; showOpenDialog(options: OpenDialogOptions): Promise; diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index cbc093540f..118656cde5 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -98,7 +98,7 @@ export class WindowService extends Disposable implements IWindowService { return this.windowsService.enterWorkspace(this.windowId, path); } - openWindow(uris: IURIToOpen[], options?: IOpenSettings): Promise { + openWindow(uris: IURIToOpen[], options: IOpenSettings = {}): Promise { if (!!this.configuration.remoteAuthority) { uris.forEach(u => u.label = u.label || this.getRecentLabel(u, !!(options && options.forceOpenWorkspaceAsFile))); } @@ -153,10 +153,6 @@ export class WindowService extends Disposable implements IWindowService { return this.windowsService.setDocumentEdited(this.windowId, flag); } - show(): Promise { - return this.windowsService.showWindow(this.windowId); - } - showMessageBox(options: Electron.MessageBoxOptions): Promise { return this.windowsService.showMessageBox(this.windowId, options); } diff --git a/src/vs/platform/windows/electron-browser/windowsService.ts b/src/vs/platform/windows/electron-browser/windowsService.ts index 7f5d5bf50b..f160cfddb5 100644 --- a/src/vs/platform/windows/electron-browser/windowsService.ts +++ b/src/vs/platform/windows/electron-browser/windowsService.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, CrashReporterStartOptions, IMessageBoxResult, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions, IDevToolsOptions, INewWindowOptions, IURIToOpen } from 'vs/platform/windows/common/windows'; +import { IWindowsService, INativeOpenDialogOptions, IEnterWorkspaceResult, CrashReporterStartOptions, IMessageBoxResult, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions, IDevToolsOptions, INewWindowOptions, IURIToOpen, IOpenSettings } from 'vs/platform/windows/common/windows'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IRecentlyOpened, IRecent, isRecentWorkspace } from 'vs/platform/history/common/history'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; @@ -186,7 +186,7 @@ export class WindowsService implements IWindowsService { return this.channel.call('toggleSharedProcess'); } - openWindow(windowId: number, uris: IURIToOpen[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean, args?: ParsedArgs }): Promise { + openWindow(windowId: number, uris: IURIToOpen[], options: IOpenSettings): Promise { return this.channel.call('openWindow', [windowId, uris, options]); } @@ -194,10 +194,6 @@ export class WindowsService implements IWindowsService { return this.channel.call('openNewWindow', options); } - showWindow(windowId: number): Promise { - return this.channel.call('showWindow', windowId); - } - getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { return this.channel.call<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]>('getWindows').then(result => { for (const win of result) { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 629ececbea..9c1e61ac0d 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -125,6 +125,7 @@ export interface IOpenConfiguration { readonly cli: ParsedArgs; readonly userEnv?: IProcessEnvironment; readonly urisToOpen?: IURIToOpen[]; + readonly waitMarkerFileURI?: URI; readonly preferNewWindow?: boolean; readonly forceNewWindow?: boolean; readonly forceNewTabbedWindow?: boolean; diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index dbed5a8a53..bf6047e619 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -218,7 +218,11 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable async focusWindow(windowId: number): Promise { this.logService.trace('windowsService#focusWindow', windowId); - return this.withWindow(windowId, codeWindow => codeWindow.win.focus()); + if (isMacintosh) { + return this.withWindow(windowId, codeWindow => codeWindow.win.show()); + } else { + return this.withWindow(windowId, codeWindow => codeWindow.win.focus()); + } } async closeWindow(windowId: number): Promise { @@ -273,7 +277,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable }); } - async openWindow(windowId: number, urisToOpen: IURIToOpen[], options: IOpenSettings = {}): Promise { + async openWindow(windowId: number, urisToOpen: IURIToOpen[], options: IOpenSettings): Promise { this.logService.trace('windowsService#openWindow'); if (!urisToOpen || !urisToOpen.length) { return undefined; @@ -289,7 +293,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable forceOpenWorkspaceAsFile: options.forceOpenWorkspaceAsFile, diffMode: options.diffMode, addMode: options.addMode, - noRecentEntry: options.noRecentEntry + noRecentEntry: options.noRecentEntry, + waitMarkerFileURI: options.waitMarkerFileURI }); } @@ -299,12 +304,6 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable this.windowsMainService.openNewWindow(OpenContext.API, options); } - async showWindow(windowId: number): Promise { - this.logService.trace('windowsService#showWindow', windowId); - - return this.withWindow(windowId, codeWindow => codeWindow.win.show()); - } - async getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { this.logService.trace('windowsService#getWindows'); @@ -466,4 +465,4 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable dispose(): void { this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} diff --git a/src/vs/platform/windows/node/windowsIpc.ts b/src/vs/platform/windows/node/windowsIpc.ts index 4dee713c20..8d2b312d5d 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 } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IURIToOpen, IOpenSettings } 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'; @@ -86,9 +86,14 @@ export class WindowsChannel implements IServerChannel { case 'minimizeWindow': return this.service.minimizeWindow(arg); case 'onWindowTitleDoubleClick': return this.service.onWindowTitleDoubleClick(arg); case 'setDocumentEdited': return this.service.setDocumentEdited(arg[0], arg[1]); - case 'openWindow': return this.service.openWindow(arg[0], arg[1] ? (arg[1]).map(r => { r.uri = URI.revive(r.uri); return r; }) : arg[1], arg[2]); + case 'openWindow': { + const urisToOpen: IURIToOpen[] = arg[1]; + const options: IOpenSettings = arg[2]; + urisToOpen.forEach(r => { r.uri = URI.revive(r.uri); return r; }); + options.waitMarkerFileURI = options.waitMarkerFileURI && URI.revive(options.waitMarkerFileURI); + return this.service.openWindow(arg[0], urisToOpen, options); + } case 'openNewWindow': return this.service.openNewWindow(arg); - case 'showWindow': return this.service.showWindow(arg); case 'getWindows': return this.service.getWindows(); case 'getWindowCount': return this.service.getWindowCount(); case 'relaunch': return this.service.relaunch(arg[0]); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ae4a415327..7cd5f2ea18 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -8828,9 +8828,10 @@ declare module 'vscode' { * Folder specific variables used in the configuration (e.g. '${workspaceFolder}') are resolved against the given folder. * @param folder The [workspace folder](#WorkspaceFolder) for looking up named configurations and resolving variables or `undefined` for a non-folder setup. * @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object. + * @param parent If specified the newly created debug session is registered as a "child" session of a "parent" debug session. * @return A thenable that resolves when debugging could be successfully started. */ - export function startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): Thenable; + export function startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, parentSession?: DebugSession): Thenable; /** * Add breakpoints. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9d6fd67156..66bebec815 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -36,12 +36,26 @@ declare module 'vscode' { export interface CallHierarchyItemProvider { + /** + * Given a document and position compute a call hierarchy item. This is justed as + * anchor for call hierarchy and then `resolveCallHierarchyItem` is being called. + */ provideCallHierarchyItem( document: TextDocument, postion: Position, token: CancellationToken ): ProviderResult; + /** + * Resolve a call hierarchy item, e.g. compute all calls from or to a function. + * The result is an array of item/location-tuples. The location in the returned tuples + * is always relative to the "caller" with the caller either being the provided item or + * the returned item. + * + * @param item A call hierarchy item previously returned from `provideCallHierarchyItem` or `resolveCallHierarchyItem` + * @param direction Resolve calls from a function or calls to a function + * @param token A cancellation token + */ resolveCallHierarchyItem( item: CallHierarchyItem, direction: CallHierarchyDirection, @@ -574,22 +588,6 @@ declare module 'vscode' { //#region André: debug - export namespace debug { - - /** - * Start debugging by using either a named launch or named compound configuration, - * or by directly passing a [DebugConfiguration](#DebugConfiguration). - * The named configurations are looked up in '.vscode/launch.json' found in the given folder. - * Before debugging starts, all unsaved files are saved and the launch configurations are brought up-to-date. - * Folder specific variables used in the configuration (e.g. '${workspaceFolder}') are resolved against the given folder. - * @param folder The [workspace folder](#WorkspaceFolder) for looking up named configurations and resolving variables or `undefined` for a non-folder setup. - * @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object. - * @param parent If specified the newly created debug session is registered as a "child" session of a "parent" debug session. - * @return A thenable that resolves when debugging could be successfully started. - */ - export function startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, parentSession?: DebugSession): Thenable; - } - // deprecated export interface DebugConfigurationProvider { diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts similarity index 99% rename from src/vs/workbench/api/electron-browser/mainThreadComments.ts rename to src/vs/workbench/api/browser/mainThreadComments.ts index 5e0c38d49b..36ec655075 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -11,14 +11,14 @@ import { keys } from 'vs/base/common/map'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape, CommentProviderFeatures } from '../common/extHost.protocol'; -import { ICommentService, ICommentInfo } from 'vs/workbench/contrib/comments/electron-browser/commentService'; -import { COMMENTS_PANEL_ID, CommentsPanel, COMMENTS_PANEL_TITLE } from 'vs/workbench/contrib/comments/electron-browser/commentsPanel'; +import { ICommentService, ICommentInfo } from 'vs/workbench/contrib/comments/browser/commentService'; +import { COMMENTS_PANEL_ID, CommentsPanel, COMMENTS_PANEL_TITLE } from 'vs/workbench/contrib/comments/browser/commentsPanel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { generateUuid } from 'vs/base/common/uuid'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ICommentsConfiguration } from 'vs/workbench/contrib/comments/electron-browser/comments.contribution'; +import { ICommentsConfiguration } from 'vs/workbench/contrib/comments/browser/comments.contribution'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel'; diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 91a18428e3..9dfb425e5d 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -230,7 +230,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb private getSession(sessionId: DebugSessionUUID | undefined): IDebugSession | undefined { if (sessionId) { - return this.debugService.getModel().getSessions(true).filter(s => s.getId() === sessionId).pop(); + return this.debugService.getModel().getSession(sessionId, true); } return undefined; } @@ -246,7 +246,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } public $customDebugAdapterRequest(sessionId: DebugSessionUUID, request: string, args: any): Promise { - const session = this.debugService.getModel().getSessions(true).filter(s => s.getId() === sessionId).pop(); + const session = this.debugService.getModel().getSession(sessionId, true); if (session) { return session.customRequest(request, args).then(response => { if (response && response.success) { diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 90ed1602b9..a136cf6e01 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -21,7 +21,7 @@ import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; -import { getRealAndSyntheticDocumentFormattersOrdered, formatDocumentWithProvider } from 'vs/editor/contrib/format/format'; +import { formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -256,24 +256,15 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { return new Promise((resolve, reject) => { const source = new CancellationTokenSource(); + const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', overrides); + const request = this._instantiationService.invokeFunction(formatDocumentWithFirstProvider, model, source.token); - const provider = getRealAndSyntheticDocumentFormattersOrdered(model); - if (provider.length !== 1) { - // print message for >1 case? - resolve(); + setTimeout(() => { + reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)); + source.cancel(); + }, timeout); - } else { - // having 1 formatter -> go for it - const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', overrides); - const request = this._instantiationService.invokeFunction(formatDocumentWithProvider, provider[0], model, source.token); - - setTimeout(() => { - reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)); - source.cancel(); - }, timeout); - - request.then(resolve, reject); - } + request.then(resolve, reject); }); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts similarity index 100% rename from src/vs/workbench/api/electron-browser/mainThreadTask.ts rename to src/vs/workbench/api/browser/mainThreadTask.ts diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 01799f5a0a..ce15477c6d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -513,7 +513,7 @@ export interface WebviewPanelShowOptions { export interface MainThreadWebviewsShape extends IDisposable { $createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void; - $createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: modes.IWebviewOptions, extensionLocation: UriComponents | undefined): void; + $createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: modes.IWebviewOptions, extensionId: ExtensionIdentifier | undefined, extensionLocation: UriComponents | undefined): void; $disposeWebview(handle: WebviewPanelHandle): void; $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void; $setTitle(handle: WebviewPanelHandle, value: string): void; diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 578d1ab4ea..7791d9ae48 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -34,7 +34,8 @@ 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 'debug/toolBar': return MenuId.DebugToolBar; case 'menuBar/file': return MenuId.MenubarFileMenu; case 'scm/title': return MenuId.SCMTitle; case 'scm/sourceControl': return MenuId.SCMSourceControl; @@ -53,6 +54,15 @@ namespace schema { return undefined; } + export function isProposedAPI(menuId: MenuId): boolean { + switch (menuId) { + case MenuId.StatusBarWindowIndicatorMenu: + case MenuId.MenubarFileMenu: + return true; + } + return false; + } + export function isValidMenuItems(menu: IUserFriendlyMenuItem[], collector: ExtensionMessageCollector): boolean { if (!Array.isArray(menu)) { collector.error(localize('requirearray', "menu items must be an array")); @@ -142,8 +152,8 @@ namespace schema { type: 'array', items: menuItem }, - 'debug/toolbar': { - description: localize('menus.debugToolbar', "The debug toolbar menu"), + 'debug/toolBar': { + description: localize('menus.debugToolBar', "The debug toolbar menu"), type: 'array', items: menuItem }, @@ -363,6 +373,11 @@ ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: schema.IUserFriendlyM return; } + if (schema.isProposedAPI(menu) && !extension.description.enableProposedApi) { + collector.error(localize('proposedAPI.invalid', "{0} is a proposed menu identifier and is only available when running out of dev or with the following command line switch: --enable-proposed-api {1}", entry.key, extension.description.identifier.value)); + return; + } + for (let item of entry.value) { let command = MenuRegistry.getCommand(item.command); let alt = item.alt && MenuRegistry.getCommand(item.alt); diff --git a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts index 047c53aad7..fbca87430c 100644 --- a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts @@ -51,8 +51,8 @@ import '../browser/mainThreadTreeViews'; import '../browser/mainThreadUrls'; import '../browser/mainThreadWindow'; import '../browser/mainThreadWorkspace'; -import './mainThreadComments'; -import './mainThreadTask'; +import '../browser/mainThreadComments'; +import '../browser/mainThreadTask'; import './mainThreadWebview'; import 'vs/workbench/api/node/apiCommands'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 9bc5f1dd00..2d705c1c48 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -94,7 +94,10 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn); } - const webview = this._webviewService.createWebview(this.getInternalWebviewId(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), URI.revive(extensionLocation), this.createWebviewEventDelegate(handle)); + const webview = this._webviewService.createWebview(this.getInternalWebviewId(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), { + location: URI.revive(extensionLocation), + id: extensionId + }, this.createWebviewEventDelegate(handle)); webview.state = { viewType: viewType, state: undefined @@ -111,7 +114,13 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extensionId.value }); } - $createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: IWebviewOptions, extensionLocation: UriComponents): void { + $createWebviewCodeInset( + handle: WebviewInsetHandle, + symbolId: string, + options: IWebviewOptions, + extensionId: ExtensionIdentifier, + extensionLocation: UriComponents + ): void { // todo@joh main is for the lack of a code-inset service // which we maybe wanna have... this is how it now works // 1) create webview element @@ -122,7 +131,10 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews WebviewElement, this._layoutService.getContainer(Parts.EDITOR_PART), { - extensionLocation: URI.revive(extensionLocation), + extension: { + location: URI.revive(extensionLocation), + id: extensionId + }, enableFindWidget: false, }, { diff --git a/src/vs/workbench/api/node/apiCommands.ts b/src/vs/workbench/api/node/apiCommands.ts index f5cbe5fd47..e579a7e7d6 100644 --- a/src/vs/workbench/api/node/apiCommands.ts +++ b/src/vs/workbench/api/node/apiCommands.ts @@ -13,6 +13,8 @@ import { EditorGroupLayout } from 'vs/workbench/services/editor/common/editorGro import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IWindowsService, IOpenSettings } from 'vs/platform/windows/common/windows'; import { IDownloadService } from 'vs/platform/download/common/download'; +import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IRecent } from 'vs/platform/history/common/history'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -34,7 +36,6 @@ function adjustHandler(handler: (executor: ICommandsExecutor, ...args: any[]) => interface IOpenFolderAPICommandOptions { forceNewWindow?: boolean; noRecentEntry?: boolean; - recentEntryLabel?: string; } export class OpenFolderAPICommand { @@ -50,7 +51,7 @@ export class OpenFolderAPICommand { } const options: IOpenSettings = { forceNewWindow: arg.forceNewWindow, noRecentEntry: arg.noRecentEntry }; uri = URI.revive(uri); - return executor.executeCommand('_files.windowOpen', [{ uri, label: arg.recentEntryLabel }], options); + return executor.executeCommand('_files.windowOpen', [{ uri }], options); } } CommandsRegistry.registerCommand({ @@ -60,12 +61,13 @@ CommandsRegistry.registerCommand({ description: 'Open a folder or workspace in the current window or new window depending on the newWindow argument. Note that opening in the same window will shutdown the current extension host process and start a new one on the given folder/workspace unless the newWindow parameter is set to true.', args: [ { name: 'uri', description: '(optional) Uri of the folder or workspace file to open. If not provided, a native dialog will ask the user for the folder', constraint: (value: any) => value === undefined || value instanceof URI }, - { name: 'options', description: '(optional) Options. Object with the following properties: `forceNewWindow `: Whether to open the folder/workspace in a new window or the same. Defaults to opening in the same window. `noRecentEntry`: Wheter the opened URI will appear in the \'Open Recent\' list. Defaults to true. `recentEntryLabel`: The label used for \'Open Recent\' list. Note, for backward compatibility, options can also be of type boolean, representing the `forceNewWindow` setting.', constraint: (value: any) => value === undefined || typeof value === 'object' || typeof value === 'boolean' } + { name: 'options', description: '(optional) Options. Object with the following properties: `forceNewWindow `: Whether to open the folder/workspace in a new window or the same. Defaults to opening in the same window. `noRecentEntry`: Wheter the opened URI will appear in the \'Open Recent\' list. Defaults to true. Note, for backward compatibility, options can also be of type boolean, representing the `forceNewWindow` setting.', constraint: (value: any) => value === undefined || typeof value === 'object' || typeof value === 'boolean' } ] } }); interface INewWindowAPICommandOptions { + reuseWindow?: boolean; } export class NewWindowAPICommand { @@ -141,6 +143,29 @@ export class RemoveFromRecentlyOpenedAPICommand { } CommandsRegistry.registerCommand(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute)); +interface RecentEntry { + uri: URI; + type: 'workspace' | 'folder' | 'file'; + label?: string; +} + +CommandsRegistry.registerCommand('_workbench.addToRecentlyOpened', async function (accessor: ServicesAccessor, recentEntry: RecentEntry) { + const windowsService = accessor.get(IWindowsService); + const workspacesService = accessor.get(IWorkspacesService); + let recent: IRecent | undefined = undefined; + const uri = recentEntry.uri; + const label = recentEntry.label; + if (recentEntry.type === 'workspace') { + const workspace = await workspacesService.getWorkspaceIdentifier(uri); + recent = { workspace, label }; + } else if (recentEntry.type === 'folder') { + recent = { folderUri: uri, label }; + } else { + recent = { fileUri: uri, label }; + } + return windowsService.addRecentlyOpened([recent]); +}); + export class SetEditorLayoutAPICommand { public static ID = 'vscode.setEditorLayout'; public static execute(executor: ICommandsExecutor, layout: EditorGroupLayout): Promise { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 46bb57d30b..0a2e6dd73e 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -624,14 +624,14 @@ export function createApiFactory( registerFileSystemProvider(scheme, provider, options) { return extHostFileSystem.registerFileSystemProvider(scheme, provider, options); }, - registerFileSearchProvider: proposedApiFunction(extension, (scheme, provider) => { + registerFileSearchProvider: proposedApiFunction(extension, (scheme: string, provider: vscode.FileSearchProvider) => { return extHostSearch.registerFileSearchProvider(scheme, provider); }), registerSearchProvider: proposedApiFunction(extension, () => { // Temp for live share in Insiders return { dispose: () => { } }; }), - registerTextSearchProvider: proposedApiFunction(extension, (scheme, provider) => { + registerTextSearchProvider: proposedApiFunction(extension, (scheme: string, provider: vscode.TextSearchProvider) => { return extHostSearch.registerTextSearchProvider(scheme, provider); }), registerDocumentCommentProvider: proposedApiFunction(extension, (provider: vscode.DocumentCommentProvider) => { @@ -646,10 +646,10 @@ export function createApiFactory( registerResourceLabelFormatter: proposedApiFunction(extension, (formatter: vscode.ResourceLabelFormatter) => { return extHostFileSystem.registerResourceLabelFormatter(formatter); }), - onDidRenameFile: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + onDidRenameFile: proposedApiFunction(extension, (listener: (e: vscode.FileRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { return extHostFileSystemEvent.onDidRenameFile(listener, thisArg, disposables); }), - onWillRenameFile: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + onWillRenameFile: proposedApiFunction(extension, (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { return extHostFileSystemEvent.getOnWillRenameFileEvent(extension)(listener, thisArg, disposables); }) }; diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index faa0748d6c..226399e169 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -7,10 +7,24 @@ 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 } from 'vs/platform/windows/common/windows'; +import { IURIToOpen, URIType, IOpenSettings } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; +export interface OpenCommandPipeArgs { + type: 'open'; + fileURIs?: string[]; + folderURIs: string[]; + forceNewWindow?: boolean; + diffMode?: boolean; + addMode?: boolean; + forceReuseWindow?: boolean; + waitMarkerFilePath?: string; +} + +export interface StatusPipeArgs { + type: 'status'; +} export class CLIServer { @@ -41,7 +55,7 @@ export class CLIServer { return this._ipcHandlePath; } - private collectURIToOpen(strs: string[], typeHint: URIType, result: IURIToOpen[]): void { + private collectURIToOpen(strs: string[] | undefined, typeHint: URIType, result: IURIToOpen[]): void { if (Array.isArray(strs)) { for (const s of strs) { try { @@ -58,11 +72,14 @@ export class CLIServer { req.setEncoding('utf8'); req.on('data', (d: string) => chunks.push(d)); req.on('end', () => { - const data = JSON.parse(chunks.join('')); + const data: OpenCommandPipeArgs | StatusPipeArgs | any = JSON.parse(chunks.join('')); switch (data.type) { case 'open': this.open(data, res); break; + case 'status': + this.getStatus(data, res); + break; default: res.writeHead(404); res.write(`Unkown message type: ${data.type}`, err => { @@ -76,8 +93,8 @@ export class CLIServer { }); } - private open(data: any, res: http.ServerResponse) { - let { fileURIs, folderURIs, forceNewWindow, diffMode, addMode, forceReuseWindow } = data; + 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); @@ -85,12 +102,31 @@ export class CLIServer { if (!forceReuseWindow && urisToOpen.some(o => o.typeHint === 'folder' || (o.typeHint === 'file' && hasWorkspaceFileExtension(o.uri.path)))) { forceNewWindow = true; } - this._commands.executeCommand('_files.windowOpen', urisToOpen, { forceNewWindow, diffMode, addMode, forceReuseWindow }); + const waitMarkerFileURI = waitMarkerFilePath ? URI.file(waitMarkerFilePath) : undefined; + const windowOpenArgs: IOpenSettings = { forceNewWindow, diffMode, addMode, forceReuseWindow, waitMarkerFileURI }; + this._commands.executeCommand('_files.windowOpen', urisToOpen, windowOpenArgs); } res.writeHead(200); res.end(); } + private async getStatus(data: StatusPipeArgs, res: http.ServerResponse) { + try { + const status = await this._commands.executeCommand('_issues.getSystemStatus'); + res.writeHead(200); + res.write(status); + res.end(); + } catch (err) { + res.writeHead(500); + res.write(String(err), err => { + if (err) { + console.error(err); + } + }); + res.end(); + } + } + dispose(): void { this._server.close(); diff --git a/src/vs/workbench/api/node/extHostConfiguration.ts b/src/vs/workbench/api/node/extHostConfiguration.ts index ec0bc0a589..5c77d994eb 100644 --- a/src/vs/workbench/api/node/extHostConfiguration.ts +++ b/src/vs/workbench/api/node/extHostConfiguration.ts @@ -263,7 +263,7 @@ export class ExtHostConfigProvider { result.set(URI.parse(key), ExtHostConfigProvider.parseConfigurationModel(data.folders[key])); return result; }, new ResourceMap()); - return new Configuration(defaultConfiguration, userConfiguration, workspaceConfiguration, folders, new ConfigurationModel(), new ResourceMap(), false); + return new Configuration(defaultConfiguration, userConfiguration, new ConfigurationModel(), workspaceConfiguration, folders, new ConfigurationModel(), new ResourceMap(), false); } private static parseConfigurationModel(model: IConfigurationModel): ConfigurationModel { diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 1251e2245d..5498a79aea 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -998,8 +998,8 @@ class MultiTracker implements vscode.DebugAdapterTracker { } interface IDapTransport { - start(cb: (msg: DebugProtocol.ProtocolMessage) => void, errorcb: (event: DebugProtocol.Event) => void); - send(message: DebugProtocol.ProtocolMessage); + start(cb: (msg: DebugProtocol.ProtocolMessage) => void, errorcb: (event: DebugProtocol.Event) => void): void; + send(message: DebugProtocol.ProtocolMessage): void; stop(): void; } diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 10ad9fe72c..5c968cc560 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -1211,7 +1211,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { const webviewHandle = Math.random(); const webview = new ExtHostWebview(webviewHandle, this._webviewProxy, { enableScripts: true }); return this._withAdapter(handle, CodeInsetAdapter, async (adapter, extension) => { - await this._webviewProxy.$createWebviewCodeInset(webviewHandle, symbol.id, { enableCommandUris: true, enableScripts: true }, extension ? extension.extensionLocation : undefined); + await this._webviewProxy.$createWebviewCodeInset(webviewHandle, symbol.id, { enableCommandUris: true, enableScripts: true }, extension ? extension.identifier : undefined, extension ? extension.extensionLocation : undefined); return adapter.resolveCodeInset(symbol, webview, token); }, symbol); } diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index dc4b634050..76cd1d4c5d 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -8,7 +8,7 @@ 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 { ILogService } from 'vs/platform/log/common/log'; -import { IFileQuery, IFolderQuery, IRawFileQuery, IRawQuery, IRawTextQuery, ISearchCompleteStats, ITextQuery, isSerializedFileMatch } from 'vs/workbench/services/search/common/search'; +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'; import { SearchService } from 'vs/workbench/services/search/node/rawSearchService'; import { RipgrepSearchProvider } from 'vs/workbench/services/search/node/ripgrepSearchProvider'; @@ -107,7 +107,7 @@ export class ExtHostSearch implements ExtHostSearchShape { } private doInternalFileSearch(handle: number, session: number, rawQuery: IFileQuery, token: CancellationToken): Promise { - const onResult = (ev) => { + const onResult = (ev: ISerializedSearchProgressItem) => { if (isSerializedFileMatch(ev)) { ev = [ev]; } diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 06d88947fc..3cc426f2a8 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -9,7 +9,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; import { IWindowConfiguration, IWindowService } 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 } from 'vs/workbench/common/contextkeys'; +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'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -90,6 +90,8 @@ export class WorkbenchContextKeysHandler extends Disposable { IsLinuxContext.bindTo(this.contextKeyService); IsWindowsContext.bindTo(this.contextKeyService); + IsRemoteContext.bindTo(this.contextKeyService).set(!!this.windowService.getConfiguration().remoteAuthority); + // macOS Native Tabs const windowConfig = this.configurationService.getValue(); HasMacNativeTabsContext.bindTo(this.contextKeyService).set(windowConfig && windowConfig.window && windowConfig.window.nativeTabs); diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index f07292e1e8..7036d2bb67 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -175,7 +175,7 @@ class EditorRegistry implements IEditorRegistry { getEditorInputs(): SyncDescriptor[] { const inputClasses: SyncDescriptor[] = []; for (const editor of this.editors) { - const editorInputDescriptors = editor[INPUT_DESCRIPTORS_PROPERTY]; + const editorInputDescriptors: SyncDescriptor[] = editor[INPUT_DESCRIPTORS_PROPERTY]; inputClasses.push(...editorInputDescriptors.map(descriptor => descriptor.ctor)); } diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts index 8242be5744..68cb6ee725 100644 --- a/src/vs/workbench/browser/nodeless.simpleservices.ts +++ b/src/vs/workbench/browser/nodeless.simpleservices.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { ITextSnapshot, IFileStat, IContent, IFileService, IResourceEncodings, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IStreamContent, IUpdateContentOptions, snapshotToString, ICreateFileOptions, IResourceEncoding, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { ITextSnapshot, IFileStat, IContent, IFileService, IResourceEncodings, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IStreamContent, IUpdateContentOptions, snapshotToString, ICreateFileOptions, IResourceEncoding, IFileStatWithMetadata, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { ITextBufferFactory } from 'vs/editor/common/model'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { keys, ResourceMap } from 'vs/base/common/map'; @@ -14,8 +14,7 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; // tslint:disable-next-line: import-patterns no-standalone-editor -import { SimpleConfigurationService as StandaloneEditorConfigurationService, SimpleDialogService as StandaloneEditorDialogService, StandaloneKeybindingService, SimpleResourcePropertiesService } from 'vs/editor/standalone/browser/simpleServices'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { SimpleConfigurationService as StandaloneEditorConfigurationService, StandaloneKeybindingService, SimpleResourcePropertiesService } from 'vs/editor/standalone/browser/simpleServices'; import { IDownloadService } from 'vs/platform/download/common/download'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEnvironmentService, IExtensionHostDebugParams, IDebugParams } from 'vs/platform/environment/common/environment'; @@ -48,7 +47,7 @@ import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/com import { ITextMateService, IGrammar as ITextMategrammar } from 'vs/workbench/services/textMate/common/textMateService'; import { LanguageId, TokenizationRegistry } from 'vs/editor/common/modes'; import { IUpdateService, State } from 'vs/platform/update/common/update'; -import { IWindowConfiguration, IPath, IPathsToWaitFor, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IURIToOpen, IMessageBoxResult, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowConfiguration, IPath, IPathsToWaitFor, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IURIToOpen, IMessageBoxResult, IWindowsService, IOpenSettings } from 'vs/platform/windows/common/windows'; import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData, isSingleFolderWorkspaceIdentifier, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { ExportData } from 'vs/base/common/performance'; @@ -202,9 +201,9 @@ registerSingleton(IConfigurationService, SimpleConfigurationService); //#region Dialog -export class SimpleDialogService extends StandaloneEditorDialogService { } +// export class SimpleDialogService extends StandaloneEditorDialogService { } -registerSingleton(IDialogService, SimpleDialogService, true); +// registerSingleton(IDialogService, SimpleDialogService, true); //#endregion @@ -655,6 +654,8 @@ export class SimpleRemoteAgentService implements IRemoteAgentService { } } +registerSingleton(IRemoteAgentService, SimpleRemoteAgentService); + //#endregion //#region Remote Authority Resolver @@ -797,6 +798,8 @@ export class SimpleRemoteFileService implements IFileService { canHandleResource(resource: URI): boolean { return resource.scheme === 'file'; } + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise { return Promise.resolve(false); } + del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean }): Promise { return Promise.resolve(); } watchFileChanges(_resource: URI): void { } @@ -1431,7 +1434,7 @@ export class SimpleWindowService implements IWindowService { return Promise.resolve(); } - openWindow(_uris: IURIToOpen[], _options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): Promise { + openWindow(_uris: IURIToOpen[], _options?: IOpenSettings): Promise { return Promise.resolve(); } @@ -1447,10 +1450,6 @@ export class SimpleWindowService implements IWindowService { return Promise.resolve(); } - show(): Promise { - return Promise.resolve(); - } - showMessageBox(_options: Electron.MessageBoxOptions): Promise { return Promise.resolve({ button: 0 }); } @@ -1606,7 +1605,7 @@ export class SimpleWindowsService implements IWindowsService { } // Global methods - openWindow(_windowId: number, _uris: IURIToOpen[], _options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): Promise { + openWindow(_windowId: number, _uris: IURIToOpen[], _options: IOpenSettings): Promise { return Promise.resolve(); } @@ -1614,10 +1613,6 @@ export class SimpleWindowsService implements IWindowsService { return Promise.resolve(); } - showWindow(_windowId: number): Promise { - return Promise.resolve(); - } - getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { return Promise.resolve([]); } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index ee6f825ca5..2309537731 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -66,6 +66,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { private cachedViewlets: ICachedViewlet[] = []; private compositeBar: CompositeBar; private compositeActions: { [compositeId: string]: { activityAction: ViewletActivityAction, pinnedAction: ToggleCompositePinnedAction } } = Object.create(null); + private readonly viewletDisposables: Map = new Map(); constructor( @IViewletService private readonly viewletService: IViewletService, @@ -121,7 +122,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Viewlet registration this._register(this.viewletService.onDidViewletRegister(viewlet => this.onDidRegisterViewlets([viewlet]))); - this._register(this.viewletService.onDidViewletDeregister(({ id }) => this.removeComposite(id, true))); + this._register(this.viewletService.onDidViewletDeregister(({ id }) => this.onDidDeregisterViewlet(id))); // Activate viewlet action on opening of a viewlet this._register(this.viewletService.onDidViewletOpen(viewlet => this.onDidViewletOpen(viewlet))); @@ -144,29 +145,9 @@ export class ActivitybarPart extends Part implements IActivityBarService { private onDidRegisterExtensions(): void { this.removeNotExistingComposites(); - for (const viewlet of this.viewletService.getViewlets()) { - this.enableCompositeActions(viewlet); - const viewContainer = this.getViewContainer(viewlet.id); - if (viewContainer && viewContainer.hideIfEmpty) { - const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); - if (viewDescriptors) { - this.onDidChangeActiveViews(viewlet, viewDescriptors); - viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(viewlet, viewDescriptors)); - } - } - } - this.saveCachedViewlets(); } - private onDidChangeActiveViews(viewlet: ViewletDescriptor, viewDescriptors: IViewDescriptorCollection): void { - if (viewDescriptors.activeViewDescriptors.length) { - this.compositeBar.addComposite(viewlet); - } else { - this.removeComposite(viewlet.id, true); - } - } - private onDidViewletOpen(viewlet: IViewlet): void { // Update the composite bar by adding @@ -321,6 +302,34 @@ export class ActivitybarPart extends Part implements IActivityBarService { } } } + for (const viewlet of viewlets) { + this.enableCompositeActions(viewlet); + const viewContainer = this.getViewContainer(viewlet.id); + if (viewContainer && viewContainer.hideIfEmpty) { + const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); + if (viewDescriptors) { + this.onDidChangeActiveViews(viewlet, viewDescriptors); + this.viewletDisposables.set(viewlet.id, viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(viewlet, viewDescriptors))); + } + } + } + } + + private onDidDeregisterViewlet(viewletId: string): void { + const disposable = this.viewletDisposables.get(viewletId); + if (disposable) { + disposable.dispose(); + } + this.viewletDisposables.delete(viewletId); + this.removeComposite(viewletId, true); + } + + private onDidChangeActiveViews(viewlet: ViewletDescriptor, viewDescriptors: IViewDescriptorCollection): void { + if (viewDescriptors.activeViewDescriptors.length) { + this.compositeBar.addComposite(viewlet); + } else { + this.removeComposite(viewlet.id, true); + } } private shouldBeHidden(viewletId: string, cachedViewlet: ICachedViewlet): boolean { diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 4d4281e5a5..c25e3c8e08 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -336,7 +336,12 @@ export class BreadcrumbsControl { editorViewState = undefined; } this._contextViewService.hideContextView(this); - this._revealInEditor(event, data.target, this._getEditorGroup(data.payload && data.payload.originalEvent), (data.payload && data.payload.originalEvent && data.payload.originalEvent.middleButton)); + + const group = (picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).metaKey) || (!picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).altKey) + ? SIDE_GROUP + : ACTIVE_GROUP; + + this._revealInEditor(event, data.target, group, (data.browserEvent as MouseEvent).button === 1); /* __GDPR__ "breadcrumbs/open" : { "type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } @@ -464,10 +469,10 @@ export class BreadcrumbsControl { } } - private _getEditorGroup(data: StandardMouseEvent | object): SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined { - if (data === BreadcrumbsControl.Payload_RevealAside || (data instanceof StandardMouseEvent && data.altKey)) { + private _getEditorGroup(data: object): SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined { + if (data === BreadcrumbsControl.Payload_RevealAside) { return SIDE_GROUP; - } else if (data === BreadcrumbsControl.Payload_Reveal || (data instanceof StandardMouseEvent && data.metaKey)) { + } else if (data === BreadcrumbsControl.Payload_Reveal) { return ACTIVE_GROUP; } else { return undefined; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index 1045f7ac34..f7e05094e6 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -205,7 +205,7 @@ export class EditorBreadcrumbsModel { let chain: Array = []; while (item) { chain.push(item); - let parent = item.parent; + let parent: any = item.parent; if (parent instanceof OutlineModel) { break; } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 3389e994b5..2c2ce90a26 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -48,6 +48,11 @@ interface ILayoutInfo { type Tree = WorkbenchDataTree | WorkbenchAsyncDataTree; +export interface SelectEvent { + target: any; + browserEvent: UIEvent; +} + export abstract class BreadcrumbsPicker { protected readonly _disposables = new Array(); @@ -58,11 +63,11 @@ export abstract class BreadcrumbsPicker { protected _fakeEvent = new UIEvent('fakeEvent'); protected _layoutInfo: ILayoutInfo; - private readonly _onDidPickElement = new Emitter<{ target: any, payload: any }>(); - readonly onDidPickElement: Event<{ target: any, payload: any }> = this._onDidPickElement.event; + private readonly _onDidPickElement = new Emitter(); + readonly onDidPickElement: Event = this._onDidPickElement.event; - private readonly _onDidFocusElement = new Emitter<{ target: any, payload: any }>(); - readonly onDidFocusElement: Event<{ target: any, payload: any }> = this._onDidFocusElement.event; + private readonly _onDidFocusElement = new Emitter(); + readonly onDidFocusElement: Event = this._onDidFocusElement.event; constructor( parent: HTMLElement, @@ -106,18 +111,18 @@ export abstract class BreadcrumbsPicker { this._disposables.push(this._tree.onDidChangeSelection(e => { if (e.browserEvent !== this._fakeEvent) { - const target = this._getTargetFromEvent(e.elements[0], e.browserEvent); + const target = this._getTargetFromEvent(e.elements[0]); if (target) { setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click - this._onDidPickElement.fire({ target, payload: undefined }); + this._onDidPickElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') }); }, 0); } } })); this._disposables.push(this._tree.onDidChangeFocus(e => { - const target = this._getTargetFromEvent(e.elements[0], e.browserEvent); + const target = this._getTargetFromEvent(e.elements[0]); if (target) { - this._onDidFocusElement.fire({ target, payload: undefined }); + this._onDidFocusElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') }); } })); this._disposables.push(this._tree.onDidChangeContentHeight(() => { @@ -155,9 +160,13 @@ export abstract class BreadcrumbsPicker { } + get useAltAsMultipleSelectionModifier() { + return this._tree.useAltAsMultipleSelectionModifier; + } + protected abstract _setInput(element: BreadcrumbElement): Promise; protected abstract _createTree(container: HTMLElement): Tree; - protected abstract _getTargetFromEvent(element: any, payload: UIEvent | undefined): any | undefined; + protected abstract _getTargetFromEvent(element: any): any | undefined; } //#region - Files @@ -425,7 +434,7 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { }); } - protected _getTargetFromEvent(element: any, _payload: any): any | undefined { + protected _getTargetFromEvent(element: any): any | undefined { // todo@joh if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) { return new FileElement((element as IFileStat).resource, FileKind.FILE); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 6fe7a7cd96..612eca7ad0 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -20,7 +20,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ClosePanelAction, TogglePanelPositionAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; +import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND, PANEL_INPUT_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem } from 'vs/workbench/browser/parts/compositeBar'; import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; @@ -484,8 +484,6 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { // Styling with Outline color (e.g. high contrast theme) const outline = theme.getColor(activeContrastBorder); if (outline) { - const outline = theme.getColor(activeContrastBorder); - collector.addRule(` .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item.checked .action-label, .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item .action-label:hover { @@ -502,6 +500,15 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { } `); } + + const inputBorder = theme.getColor(PANEL_INPUT_BORDER); + if (inputBorder) { + collector.addRule(` + .monaco-workbench .part.panel .monaco-inputbox { + border-color: ${inputBorder} + } + `); + } }); registerSingleton(IPanelService, PanelPart); \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index 0de179e7d4..3b48e59cc1 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -463,6 +463,10 @@ class QuickPick extends QuickInput implements IQuickPi this.update(); } + public inputHasFocus(): boolean { + return this.visible ? this.ui.inputBox.hasFocus() : false; + } + onDidChangeSelection = this.onDidChangeSelectionEmitter.event; onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts index 98ed7666db..acb98dfdb2 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts @@ -81,6 +81,10 @@ export class QuickInputBox { this.inputBox.setEnabled(enabled); } + hasFocus(): boolean { + return this.inputBox.hasFocus(); + } + setAttribute(name: string, value: string) { this.inputBox.inputElement.setAttribute(name, value); } diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 46f752af09..06307e451f 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -173,10 +173,10 @@ export class Workbench extends Layout { // Layout Service serviceCollection.set(IWorkbenchLayoutService, this); - // + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // NOTE: DO NOT ADD ANY OTHER SERVICE INTO THE COLLECTION HERE. - // INSTEAD, CONTRIBUTE IT VIA WORKBENCH.MAIN.TS - // + // CONTRIBUTE IT VIA WORKBENCH.MAIN.TS AND registerSingleton(). + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // All Contributed Services const contributedServices = getServices(); @@ -192,8 +192,8 @@ export class Workbench extends Layout { // TODO@Ben legacy file service const fileService = accessor.get(IFileService) as any; - if (typeof fileService.setImpl === 'function') { - fileService.setImpl(accessor.get(ILegacyFileService)); + if (typeof fileService.setLegacyService === 'function') { + fileService.setLegacyService(accessor.get(ILegacyFileService)); } // TODO@Sandeep debt around cyclic dependencies diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 368ae3dd23..98c48d19f5 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -9,6 +9,7 @@ import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform'; export const IsMacContext = new RawContextKey('isMac', isMacintosh); export const IsLinuxContext = new RawContextKey('isLinux', isLinux); export const IsWindowsContext = new RawContextKey('isWindows', isWindows); +export const IsRemoteContext = new RawContextKey('isRemote', false); export const HasMacNativeTabsContext = new RawContextKey('hasMacNativeTabs', false); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 8a3ae85ae2..6158ecf66f 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -388,11 +388,11 @@ export abstract class EditorInput extends Disposable implements IEditorInput { } /** - * Returns a descriptor suitable for telemetry events or null if none is available. + * Returns a descriptor suitable for telemetry events. * * Subclasses should extend if they can contribute. */ - getTelemetryDescriptor(): object { + getTelemetryDescriptor(): { [key: string]: any } { /* __GDPR__FRAGMENT__ "EditorTelemetryDescriptor" : { "typeId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 4f882763c6..e3a623f10f 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -235,6 +235,11 @@ export const PANEL_DRAG_AND_DROP_BACKGROUND = registerColor('panel.dropBackgroun hc: Color.white.transparent(0.12) }, nls.localize('panelDragAndDropBackground', "Drag and drop feedback color for the panel title items. The color should have transparency so that the panel entries can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal.")); +export const PANEL_INPUT_BORDER = registerColor('panelInput.border', { + dark: null, + light: Color.fromHex('#ddd'), + hc: null +}, nls.localize('panelInputBorder', "Input box border for inputs in the panel.")); // < --- Status --- > diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index 99f38f06f9..5ed540796b 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/callHierarchy'; -import { PeekViewWidget } from 'vs/editor/contrib/referenceSearch/peekViewWidget'; +import { PeekViewWidget, IPeekViewService } from 'vs/editor/contrib/referenceSearch/peekViewWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CallHierarchyProvider, CallHierarchyDirection, CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; @@ -106,6 +106,7 @@ export class CallHierarchyTreePeekWidget extends PeekViewWidget { private readonly _provider: CallHierarchyProvider, private _direction: CallHierarchyDirection, @IThemeService themeService: IThemeService, + @IPeekViewService private readonly _peekViewService: IPeekViewService, @IEditorService private readonly _editorService: IEditorService, @ITextModelService private readonly _textModelService: ITextModelService, @ILabelService private readonly _labelService: ILabelService, @@ -114,6 +115,7 @@ export class CallHierarchyTreePeekWidget extends PeekViewWidget { ) { super(editor, { showFrame: true, showArrow: true, isResizeable: true, isAccessible: true }); this.create(); + this._peekViewService.addExclusiveWidget(editor, this); this._applyTheme(themeService.getTheme()); themeService.onThemeChange(this._applyTheme, this, this._disposables); } diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css b/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css index 41b00fbf14..ad70f92f9f 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css @@ -32,10 +32,11 @@ background-position: left center; } -.monaco-workbench .call-hierarchy .monaco-split-view2.horizontal > .split-view-container > .split-view-view{ +.monaco-workbench .call-hierarchy .monaco-split-view2.horizontal > .split-view-container > .split-view-view { /* this is a little bizare.. */ height: unset; } -.monaco-workbench .call-hierarchy .tree{ + +.monaco-workbench .call-hierarchy .tree { height: 100%; } diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/commentGlyphWidget.ts rename to src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts similarity index 99% rename from src/vs/workbench/contrib/comments/electron-browser/commentNode.ts rename to src/vs/workbench/contrib/comments/browser/commentNode.ts index c037c40ffe..8e52603e45 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -19,8 +19,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ICommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService'; -import { SimpleCommentEditor } from 'vs/workbench/contrib/comments/electron-browser/simpleCommentEditor'; +import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; +import { SimpleCommentEditor } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { Selection } from 'vs/editor/common/core/selection'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts similarity index 98% rename from src/vs/workbench/contrib/comments/electron-browser/commentService.ts rename to src/vs/workbench/contrib/comments/browser/commentService.ts index 0a03972f90..b7aa643264 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -13,7 +13,7 @@ import { keys } from 'vs/base/common/map'; import { CancellationToken } from 'vs/base/common/cancellation'; import { assign } from 'vs/base/common/objects'; import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; -import { MainThreadCommentController } from 'vs/workbench/api/electron-browser/mainThreadComments'; +import { MainThreadCommentController } from 'vs/workbench/api/browser/mainThreadComments'; export const ICommentService = createDecorator('commentService'); @@ -65,8 +65,8 @@ export interface ICommentService { deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; getReactionGroup(owner: string): CommentReaction[] | undefined; toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise; - setActiveCommentThread(commentThread: CommentThread | null); - setInput(input: string); + setActiveCommentThread(commentThread: CommentThread | null): void; + setInput(input: string): void; } export class CommentService extends Disposable implements ICommentService { diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts similarity index 99% rename from src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts rename to src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 0b516a6477..732f1a3013 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -19,20 +19,20 @@ import { peekViewBorder } from 'vs/editor/contrib/referenceSearch/referencesWidg import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/electron-browser/commentGlyphWidget'; +import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; import { SimpleCommentEditor } from './simpleCommentEditor'; import { URI } from 'vs/base/common/uri'; import { transparent, editorForeground, textLinkActiveForeground, textLinkForeground, focusBorder, textBlockQuoteBackground, textBlockQuoteBorder, contrastBorder, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { ICommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService'; +import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; -import { CommentNode } from 'vs/workbench/contrib/comments/electron-browser/commentNode'; +import { CommentNode } from 'vs/workbench/contrib/comments/browser/commentNode'; import { ITextModel } from 'vs/editor/common/model'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { generateUuid } from 'vs/base/common/uuid'; diff --git a/src/vs/workbench/contrib/comments/electron-browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts similarity index 91% rename from src/vs/workbench/contrib/comments/electron-browser/comments.contribution.ts rename to src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 544a2996ee..8e2357a341 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -6,8 +6,8 @@ import * as nls from 'vs/nls'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; -import 'vs/workbench/contrib/comments/electron-browser/commentsEditorContribution'; -import { ICommentService, CommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService'; +import 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; +import { ICommentService, CommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; export interface ICommentsConfiguration { diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts similarity index 98% rename from src/vs/workbench/contrib/comments/electron-browser/commentsEditorContribution.ts rename to src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 278e82288f..7511f03fac 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -21,20 +21,19 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { CommentThreadCollapsibleState } from 'vs/workbench/api/node/extHostTypes'; -import { ReviewZoneWidget, COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/electron-browser/commentThreadWidget'; -import { ICommentService, ICommentInfo } from 'vs/workbench/contrib/comments/electron-browser/commentService'; +import { ReviewZoneWidget, COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/browser/commentThreadWidget'; +import { ICommentService, ICommentInfo } from 'vs/workbench/contrib/comments/browser/commentService'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelDecorationOptions } from 'vs/editor/common/model'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; -import { overviewRulerCommentingRangeForeground } from 'vs/workbench/contrib/comments/electron-browser/commentGlyphWidget'; +import { overviewRulerCommentingRangeForeground } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ctxCommentEditorFocused, SimpleCommentEditor } from 'vs/workbench/contrib/comments/electron-browser/simpleCommentEditor'; +import { ctxCommentEditorFocused, SimpleCommentEditor } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { onUnexpectedError } from 'vs/base/common/errors'; export const ctxCommentThreadVisible = new RawContextKey('commentThreadVisible', false); @@ -472,7 +471,7 @@ export class ReviewController implements IEditorContribution { endColumn: 0 }, reply: replyCommand, - collapsibleState: CommentThreadCollapsibleState.Expanded, + collapsibleState: modes.CommentThreadCollapsibleState.Expanded, }, pendingComment, draftMode); this.localToDispose.push(this._newCommentWidget!.onDidClose(e => { @@ -596,6 +595,7 @@ export class ReviewController implements IEditorContribution { const args = replyCommand.arguments || []; this._commandService.executeCommand(commandId, ...args); + this._addInProgress = false; } } else if (commentingRangesInfo.newCommentThreadCallback) { return commentingRangesInfo.newCommentThreadCallback(this.editor.getModel().uri, range) @@ -610,11 +610,13 @@ export class ReviewController implements IEditorContribution { } else { const commentInfo = this._commentInfos.filter(info => info.owner === ownerId); if (!commentInfo || !commentInfo.length) { + this._addInProgress = false; return Promise.resolve(); } const draftMode = commentInfo[0].draftMode; this.addComment(lineNumber, replyCommand, ownerId, extensionId, draftMode, null); + this._addInProgress = false; } return Promise.resolve(); diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentsPanel.ts b/src/vs/workbench/contrib/comments/browser/commentsPanel.ts similarity index 98% rename from src/vs/workbench/contrib/comments/electron-browser/commentsPanel.ts rename to src/vs/workbench/contrib/comments/browser/commentsPanel.ts index 77ac722f77..ebed730710 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentsPanel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsPanel.ts @@ -15,9 +15,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Panel } from 'vs/workbench/browser/panel'; import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; -import { ReviewController } from 'vs/workbench/contrib/comments/electron-browser/commentsEditorContribution'; -import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'vs/workbench/contrib/comments/electron-browser/commentsTreeViewer'; -import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/electron-browser/commentService'; +import { ReviewController } from 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; +import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/browser/commentService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/commentsTreeViewer.ts rename to src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/close.svg b/src/vs/workbench/contrib/comments/browser/media/close.svg similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/media/close.svg rename to src/vs/workbench/contrib/comments/browser/media/close.svg diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/comment.svg b/src/vs/workbench/contrib/comments/browser/media/comment.svg similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/media/comment.svg rename to src/vs/workbench/contrib/comments/browser/media/comment.svg diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/media/panel.css rename to src/vs/workbench/contrib/comments/browser/media/panel.css diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/reaction-dark.svg b/src/vs/workbench/contrib/comments/browser/media/reaction-dark.svg similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/media/reaction-dark.svg rename to src/vs/workbench/contrib/comments/browser/media/reaction-dark.svg diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/reaction-hc.svg b/src/vs/workbench/contrib/comments/browser/media/reaction-hc.svg similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/media/reaction-hc.svg rename to src/vs/workbench/contrib/comments/browser/media/reaction-hc.svg diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/reaction.svg b/src/vs/workbench/contrib/comments/browser/media/reaction.svg similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/media/reaction.svg rename to src/vs/workbench/contrib/comments/browser/media/reaction.svg diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/media/review.css rename to src/vs/workbench/contrib/comments/browser/media/review.css diff --git a/src/vs/workbench/contrib/comments/electron-browser/reactionsAction.ts b/src/vs/workbench/contrib/comments/browser/reactionsAction.ts similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/reactionsAction.ts rename to src/vs/workbench/contrib/comments/browser/reactionsAction.ts diff --git a/src/vs/workbench/contrib/comments/electron-browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts similarity index 100% rename from src/vs/workbench/contrib/comments/electron-browser/simpleCommentEditor.ts rename to src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 1f79074d1b..632563c33b 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -126,7 +126,7 @@ export interface IExpressionTemplateData { name: HTMLSpanElement; value: HTMLSpanElement; inputBoxContainer: HTMLElement; - enableInputBox(expression: IExpression, options: IInputBoxOptions); + enableInputBox(expression: IExpression, options: IInputBoxOptions): void; toDispose: IDisposable[]; label: HighlightedLabel; } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index ad5fd7ed9d..1d2adc8cb6 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -76,7 +76,7 @@ export class BreakpointsView extends ViewletPanel { ], { identityProvider: { getId: (element: IEnablement) => element.getId() }, multipleSelectionSupport: false, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: e => e } + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IEnablement) => e } }) as WorkbenchList; CONTEXT_BREAKPOINTS_FOCUSED.bindTo(this.list.contextKeyService); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index c8924c0618..6586c3a5be 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -109,7 +109,7 @@ export class CallStackView extends ViewletPanel { accessibilityProvider: new CallStackAccessibilityProvider(), ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"), identityProvider: { - getId: element => { + getId: (element: CallStackItem) => { if (typeof element === 'string') { return element; } @@ -117,11 +117,11 @@ export class CallStackView extends ViewletPanel { return `showMore ${element[0].getId()}`; } - return (element).getId(); + return element.getId(); } }, keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: e => { + getKeyboardNavigationLabel: (e: CallStackItem) => { if (isDebugSession(e)) { return e.getLabel(); } @@ -170,7 +170,7 @@ export class CallStackView extends ViewletPanel { focusStackFrame(undefined, undefined, element); } if (element instanceof ThreadAndSessionIds) { - const session = this.debugService.getModel().getSessions().filter(p => p.getId() === element.sessionId).pop(); + const session = this.debugService.getModel().getSession(element.sessionId); const thread = session && session.getThread(element.threadId); if (thread) { (thread).fetchCallStack() diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index f920ed3953..e2592e71f9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -125,7 +125,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: STEP_INTO_ID, - weight: KeybindingWeight.WorkbenchContrib, + weight: KeybindingWeight.WorkbenchContrib + 10, // Have a stronger weight to have priority over full screen when debugging primary: KeyCode.F11, when: CONTEXT_DEBUG_STATE.isEqualTo('stopped'), handler: (accessor: ServicesAccessor, _: string, thread: IThread | undefined) => { diff --git a/src/vs/workbench/contrib/debug/browser/debugToolbar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts similarity index 97% rename from src/vs/workbench/contrib/debug/browser/debugToolbar.ts rename to src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 6119f9de7d..63afde46e6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolbar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/debugToolbar'; +import 'vs/css!./media/debugToolBar'; import * as errors from 'vs/base/common/errors'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; @@ -46,14 +46,14 @@ export const debugToolBarBorder = registerColor('debugToolBar.border', { hc: null }, localize('debugToolBarBorder', "Debug toolbar border color.")); -export class DebugToolbar extends Themable implements IWorkbenchContribution { +export class DebugToolBar extends Themable implements IWorkbenchContribution { private $el: HTMLElement; private dragArea: HTMLElement; private actionBar: ActionBar; private activeActions: IAction[]; private updateScheduler: RunOnceScheduler; - private debugToolbarMenu: IMenu; + private debugToolBarMenu: IMenu; private isVisible: boolean; private isBuilt: boolean; @@ -81,8 +81,8 @@ export class DebugToolbar extends Themable implements IWorkbenchContribution { this.dragArea = dom.append(this.$el, dom.$('div.drag-area')); const actionBarContainer = dom.append(this.$el, dom.$('div.action-bar-container')); - this.debugToolbarMenu = menuService.createMenu(MenuId.DebugToolbar, contextKeyService); - this.toDispose.push(this.debugToolbarMenu); + this.debugToolBarMenu = menuService.createMenu(MenuId.DebugToolBar, contextKeyService); + this.toDispose.push(this.debugToolBarMenu); this.activeActions = []; this.actionBar = this._register(new ActionBar(actionBarContainer, { @@ -106,7 +106,7 @@ export class DebugToolbar extends Themable implements IWorkbenchContribution { return this.hide(); } - const actions = DebugToolbar.getActions(this.debugToolbarMenu, this.debugService, this.instantiationService); + const actions = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); if (!arrays.equals(actions, this.activeActions, (first, second) => first.id === second.id)) { this.actionBar.clear(); this.actionBar.push(actions, { icon: true, label: false }); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 3765e7f64b..20cf5a754e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -24,7 +24,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { memoize } from 'vs/base/common/decorators'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { DebugToolbar } from 'vs/workbench/contrib/debug/browser/debugToolbar'; +import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { IMenu, MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -40,7 +40,7 @@ export class DebugViewlet extends ViewContainerViewlet { private progressRunner: IProgressRunner; private breakpointView: ViewletPanel; private panelListeners = new Map(); - private debugToolbarMenu: IMenu; + private debugToolBarMenu: IMenu; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -63,6 +63,7 @@ export class DebugViewlet extends ViewContainerViewlet { super(VIEWLET_ID, `${VIEWLET_ID}.state`, false, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state))); + this._register(this.debugService.onDidNewSession(() => this.updateToolBar())); this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateTitleArea())); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('debug.toolBarLocation')) { @@ -109,11 +110,11 @@ export class DebugViewlet extends ViewContainerViewlet { return [this.startAction, this.configureAction, this.toggleReplAction]; } - if (!this.debugToolbarMenu) { - this.debugToolbarMenu = this.menuService.createMenu(MenuId.DebugToolbar, this.contextKeyService); - this.toDispose.push(this.debugToolbarMenu); + if (!this.debugToolBarMenu) { + this.debugToolBarMenu = this.menuService.createMenu(MenuId.DebugToolBar, this.contextKeyService); + this.toDispose.push(this.debugToolBarMenu); } - return DebugToolbar.getActions(this.debugToolbarMenu, this.debugService, this.instantiationService); + return DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); } get showInitialDebugActions(): boolean { @@ -160,6 +161,10 @@ export class DebugViewlet extends ViewContainerViewlet { this.progressRunner = this.progressService.show(true); } + this.updateToolBar(); + } + + private updateToolBar(): void { if (this.configurationService.getValue('debug').toolBarLocation === 'docked') { this.updateTitleArea(); } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 94f8261b56..2742fb34d5 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -424,10 +424,10 @@ export class LoadedScriptsView extends ViewletPanel { new LoadedScriptsDataSource(), { identityProvider: { - getId: element => (element).getId() + getId: (element: LoadedScriptsItem) => element.getId() }, keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: element => (element).getLabel() + getKeyboardNavigationLabel: (element: LoadedScriptsItem) => element.getLabel() }, filter: this.filter, accessibilityProvider: new LoadedSciptsAccessibilityProvider(), diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolbar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css similarity index 100% rename from src/vs/workbench/contrib/debug/browser/media/debugToolbar.css rename to src/vs/workbench/contrib/debug/browser/media/debugToolBar.css diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 6bb4d122a4..cb75672a40 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -373,9 +373,9 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati ], new ReplDataSource(), { ariaLabel: nls.localize('replAriaLabel', "Read Eval Print Loop Panel"), accessibilityProvider: new ReplAccessibilityProvider(), - identityProvider: { getId: element => (element).getId() }, + identityProvider: { getId: (element: IReplElement) => element.getId() }, mouseSupport: false, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: e => e }, + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e }, horizontalScrolling: false, setRowLineHeight: false, supportDynamicHeights: true @@ -452,7 +452,10 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati private onContextMenu(e: ITreeContextMenuEvent): void { const actions: IAction[] = []; actions.push(new Action('debug.replCopy', nls.localize('copy', "Copy"), undefined, true, () => { - this.clipboardService.writeText(window.getSelection().toString()); + const nativeSelection = window.getSelection(); + if (nativeSelection) { + this.clipboardService.writeText(nativeSelection.toString()); + } return Promise.resolve(); })); actions.push(new Action('workbench.debug.action.copyAll', nls.localize('copyAll', "Copy All"), undefined, true, () => { @@ -740,7 +743,8 @@ class ReplDelegate implements IListVirtualDelegate { } hasDynamicHeight?(element: IReplElement): boolean { - return true; + // Empty elements should not have dynamic height since they will be invisible + return element.toString().length > 0; } } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 09155a6270..b1da22b526 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -76,8 +76,8 @@ export class VariablesView extends ViewletPanel { new VariablesDataSource(), { ariaLabel: nls.localize('variablesAriaTreeLabel', "Debug Variables"), accessibilityProvider: new VariablesAccessibilityProvider(), - identityProvider: { getId: element => (element).getId() }, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: e => e } + identityProvider: { getId: (element: IExpression | IScope) => element.getId() }, + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression | IScope) => e } }) as WorkbenchAsyncDataTree; this.tree.setInput(this.debugService.getViewModel()).then(null, onUnexpectedError); diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 37f6f2c424..e5e2707bbe 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -63,8 +63,8 @@ export class WatchExpressionsView extends ViewletPanel { new WatchExpressionsDataSource(), { ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"), accessibilityProvider: new WatchExpressionsAccessibilityProvider(), - identityProvider: { getId: element => (element).getId() }, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: e => e }, + identityProvider: { getId: (element: IExpression) => element.getId() }, + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression) => e }, dnd: new WatchExpressionsDragAndDrop(this.debugService), }) as WorkbenchAsyncDataTree; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index dfed95f4a5..99b7d2a100 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -168,7 +168,7 @@ export interface IDebugSession extends ITreeElement { removeReplExpressions(): void; addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise; appendToRepl(data: string | IExpression, severity: severity, source?: IReplElementSource): void; - logToRepl(sev: severity, args: any[], frame?: { uri: uri, line: number, column: number }); + logToRepl(sev: severity, args: any[], frame?: { uri: uri, line: number, column: number }): void; // session events readonly onDidEndAdapter: Event; @@ -389,6 +389,7 @@ export interface IEvaluate { } export interface IDebugModel extends ITreeElement { + getSession(sessionId: string | undefined, includeInactive?: boolean): IDebugSession | undefined; getSessions(includeInactive?: boolean): IDebugSession[]; getBreakpoints(filter?: { uri?: uri, lineNumber?: number, column?: number, enabledOnly?: boolean }): ReadonlyArray; areBreakpointsActivated(): boolean; @@ -471,8 +472,8 @@ export interface ICompound { export interface IDebugAdapter extends IDisposable { readonly onError: Event; readonly onExit: Event; - onRequest(callback: (request: DebugProtocol.Request) => void); - onEvent(callback: (event: DebugProtocol.Event) => void); + onRequest(callback: (request: DebugProtocol.Request) => void): void; + onEvent(callback: (event: DebugProtocol.Event) => void): void; startSession(): Promise; sendMessage(message: DebugProtocol.ProtocolMessage): void; sendResponse(response: DebugProtocol.Response): void; diff --git a/src/vs/workbench/contrib/debug/common/debugContentProvider.ts b/src/vs/workbench/contrib/debug/common/debugContentProvider.ts index f7c8b02087..ffabdfe5f2 100644 --- a/src/vs/workbench/contrib/debug/common/debugContentProvider.ts +++ b/src/vs/workbench/contrib/debug/common/debugContentProvider.ts @@ -81,7 +81,7 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC if (resource.query) { const data = Source.getEncodedDebugData(resource); - session = this.debugService.getModel().getSessions().filter(p => p.getId() === data.sessionId).pop(); + session = this.debugService.getModel().getSession(data.sessionId); } if (!session) { diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 5b29635417..96bf6ee8c8 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -778,6 +778,13 @@ export class DebugModel implements IDebugModel { return 'root'; } + getSession(sessionId: string | undefined, includeInactive = false): IDebugSession | undefined { + if (sessionId) { + return this.getSessions(includeInactive).filter(s => s.getId() === sessionId).pop(); + } + return undefined; + } + getSessions(includeInactive = false): IDebugSession[] { // By default do not return inactive sesions. // However we are still holding onto inactive sessions due to repl and debug service session revival (eh scenario) diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index f6fe64250f..ddc71476c1 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -199,7 +199,7 @@ function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePath: (toDA: fixSourcePath(true, (request.arguments).source); break; case 'launchVSCode': - request.arguments.args.forEach(arg => fixSourcePath(false, arg)); + request.arguments.args.forEach((arg: PathContainer | undefined) => fixSourcePath(false, arg)); break; default: break; diff --git a/src/vs/workbench/contrib/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/electron-browser/debug.contribution.ts index 1005d295c5..0cdcf5bc96 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debug.contribution.ts @@ -28,7 +28,7 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { DebugEditorModelManager } from 'vs/workbench/contrib/debug/browser/debugEditorModelManager'; import { StartAction, AddFunctionBreakpointAction, ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; -import { DebugToolbar } from 'vs/workbench/contrib/debug/browser/debugToolbar'; +import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import * as service from 'vs/workbench/contrib/debug/electron-browser/debugService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, REVERSE_CONTINUE_ID, STEP_BACK_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; @@ -124,7 +124,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(OpenDebugPanelAction, registry.registerWorkbenchAction(new SyncActionDescriptor(OpenDebugViewletAction, OpenDebugViewletAction.ID, OpenDebugViewletAction.LABEL, openViewletKb), 'View: Show Debug', nls.localize('view', "View")); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugEditorModelManager, LifecyclePhase.Restored); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugToolbar, LifecyclePhase.Restored); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugToolBar, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugContentProvider, LifecyclePhase.Eventually); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(StatusBarColorProvider, LifecyclePhase.Eventually); @@ -259,8 +259,8 @@ statusBar.registerStatusbarItem(new StatusbarItemDescriptor(DebugStatus, Statusb // Debug toolbar -const registerDebugToolbarItem = (id: string, title: string, icon: string, order: number, when?: ContextKeyExpr, precondition?: ContextKeyExpr) => { - MenuRegistry.appendMenuItem(MenuId.DebugToolbar, { +const registerDebugToolBarItem = (id: string, title: string, icon: string, order: number, when?: ContextKeyExpr, precondition?: ContextKeyExpr) => { + MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { group: 'navigation', when, order, @@ -276,16 +276,16 @@ const registerDebugToolbarItem = (id: string, title: string, icon: string, order }); }; -registerDebugToolbarItem(CONTINUE_ID, continueLabel, 'continue', 10, CONTEXT_DEBUG_STATE.notEqualsTo('running')); -registerDebugToolbarItem(PAUSE_ID, pauseLabel, 'pause', 10, CONTEXT_DEBUG_STATE.isEqualTo('running')); -registerDebugToolbarItem(STOP_ID, stopLabel, 'stop', 70, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); -registerDebugToolbarItem(DISCONNECT_ID, disconnectLabel, 'disconnect', 70, CONTEXT_FOCUSED_SESSION_IS_ATTACH); -registerDebugToolbarItem(STEP_OVER_ID, stepOverLabel, 'step-over', 20, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolbarItem(STEP_INTO_ID, stepIntoLabel, 'step-into', 30, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolbarItem(STEP_OUT_ID, stepOutLabel, 'step-out', 40, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolbarItem(RESTART_SESSION_ID, restartLabel, 'restart', 60); -registerDebugToolbarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), 'step-back', 50, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolbarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 'reverse-continue', 60, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(CONTINUE_ID, continueLabel, 'continue', 10, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(PAUSE_ID, pauseLabel, 'pause', 10, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); +registerDebugToolBarItem(STOP_ID, stopLabel, 'stop', 70, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); +registerDebugToolBarItem(DISCONNECT_ID, disconnectLabel, 'disconnect', 70, CONTEXT_FOCUSED_SESSION_IS_ATTACH); +registerDebugToolBarItem(STEP_OVER_ID, stepOverLabel, 'step-over', 20, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_INTO_ID, stepIntoLabel, 'step-into', 30, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_OUT_ID, stepOutLabel, 'step-out', 40, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(RESTART_SESSION_ID, restartLabel, 'restart', 60); +registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), 'step-back', 50, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 'reverse-continue', 60, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); // Debug callstack context menu const registerDebugCallstackItem = (id: string, title: string, order: number, when?: ContextKeyExpr, precondition?: ContextKeyExpr, group = 'navigation') => { @@ -529,7 +529,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { // Touch Bar if (isMacintosh) { - const registerTouchBarEntry = (id: string, title: string, order, when: ContextKeyExpr, icon: string) => { + const registerTouchBarEntry = (id: string, title: string, order: number, when: ContextKeyExpr, icon: string) => { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { command: { id, title, iconLocation: { dark: URI.parse(require.toUrl(`vs/workbench/contrib/debug/electron-browser/media/${icon}`)) } diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts index d14d395246..b74a6d4aa4 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts @@ -137,7 +137,7 @@ export class DebugService implements IDebugService { this.lifecycleService.onShutdown(this.dispose, this); this.toDispose.push(this.broadcastService.onBroadcast(broadcast => { - const session = this.model.getSessions(true).filter(s => s.getId() === broadcast.payload.debugId).pop(); + const session = this.model.getSession(broadcast.payload.debugId, true); if (session) { switch (broadcast.channel) { diff --git a/src/vs/workbench/contrib/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/electron-browser/rawDebugSession.ts index a85a35ac17..02c7022392 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/rawDebugSession.ts @@ -497,7 +497,7 @@ export class RawDebugSession { success: true }; - const safeSendResponse = (response) => this.debugAdapter && this.debugAdapter.sendResponse(response); + const safeSendResponse = (response: DebugProtocol.Response) => this.debugAdapter && this.debugAdapter.sendResponse(response); switch (request.command) { case 'launchVSCode': diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 1b70f21230..b96c2067c0 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -71,7 +71,7 @@ abstract class TerminalLauncher implements ITerminalLauncher { return this.runInTerminal0(args.title!, args.cwd, args.args, args.env || {}, config); } - abstract runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment | {}, config): Promise; + abstract runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment | {}, config: ITerminalSettings): Promise; } class WinTerminalService extends TerminalLauncher { diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts index 70dd8e720f..8c7d801dfc 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts @@ -23,7 +23,7 @@ suite('Experimental Prompts', () => { let experimentalPrompt: ExperimentalPrompts; let onExperimentEnabledEvent: Emitter; - let storageData = {}; + let storageData: { [key: string]: any } = {}; const promptText = 'Hello there! Can you see this?'; const experiment: IExperiment = { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts index a585a7e67f..6c114f38f3 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts @@ -86,7 +86,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe private _workspaceIgnoredRecommendations: string[] = []; private _extensionsRecommendationsUrl: string; private _disposables: IDisposable[] = []; - public loadWorkspaceConfigPromise: Promise; + public loadWorkspaceConfigPromise: Promise; private proactiveRecommendationsFetched: boolean = false; private readonly _onRecommendationChange = new Emitter(); @@ -137,7 +137,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } // {{SQL CARBON EDIT}} disable extension recommendation prompt - this.loadWorkspaceConfigPromise = this.getWorkspaceRecommendations(); + this.loadWorkspaceConfigPromise = this.getWorkspaceRecommendations().then(); // .then(() => { // this.promptWorkspaceRecommendations(); // this._modelService.onModelAdded(this.promptFiletypeBasedRecommendations, this, this._disposables); @@ -879,7 +879,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe /** * If user has any of the tools listed in product.exeBasedExtensionTips, fetch corresponding recommendations */ - private fetchExecutableRecommendations(): Promise { + private fetchExecutableRecommendations(): Promise { const homeDir = os.homedir(); let foundExecutables: Set = new Set(); @@ -897,7 +897,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe }); }; - let promises: Promise[] = []; + let promises: Promise[] = []; // Loop through recommended extensions forEach(product.exeBasedExtensionTips, entry => { if (typeof entry.value !== 'object' || !Array.isArray(entry.value['recommendations'])) { @@ -921,7 +921,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } }); - return Promise.all(promises); + return Promise.all(promises).then(() => undefined); } /** @@ -1016,17 +1016,18 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe * Fetch extension recommendations from currently running experiments */ private fetchExperimentalRecommendations() { - // {{SQL CARBON EDIT}} disable experiements - // this.experimentService.getExperimentsByType(ExperimentActionType.AddToRecommendations).then(experiments => { - // (experiments || []).forEach(experiment => { - // const action = experiment.action; - // if (action && experiment.state === ExperimentState.Run && action.properties && Array.isArray(action.properties.recommendations) && action.properties.recommendationReason) { - // action.properties.recommendations.forEach(id => { - // this._experimentalRecommendations[id] = action.properties.recommendationReason; - // }); - // } - // }); - // }); + /* // {{SQL CARBON EDIT}} disable experiements + this.experimentService.getExperimentsByType(ExperimentActionType.AddToRecommendations).then(experiments => { + (experiments || []).forEach(experiment => { + const action = experiment.action; + if (action && experiment.state === ExperimentState.Run && action.properties && Array.isArray(action.properties.recommendations) && action.properties.recommendationReason) { + action.properties.recommendations.forEach((id: string) => { + this._experimentalRecommendations[id] = action.properties.recommendationReason; + }); + } + }); + }); + */ } //#endregion diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts index 553b1cd6b4..73b832acdb 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts @@ -340,13 +340,13 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.searchBox = this.instantiationService.createInstance(SuggestEnabledInput, `${VIEWLET_ID}.searchbox`, header, { triggerCharacters: ['@'], - sortKey: item => { + sortKey: (item: string) => { if (item.indexOf(':') === -1) { return 'a'; } else if (/ext:/.test(item) || /tag:/.test(item)) { return 'b'; } else if (/sort:/.test(item)) { return 'c'; } else { return 'd'; } }, - provideResults: (query) => Query.suggestions(query) + provideResults: (query: string) => Query.suggestions(query) }, placeholder, 'extensions:searchinput', { placeholderText: placeholder, value: searchValue }); if (this.searchBox.getValue()) { @@ -453,7 +453,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio super.saveState(); } - private doSearch(): Promise { + private doSearch(): Promise { const value = this.normalizedQuery(); this.searchExtensionsContextKey.set(!!value); this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); @@ -465,9 +465,9 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio return this.progress(Promise.all(this.panels.map(view => (view).show(this.normalizedQuery()) .then(model => this.alertSearchResult(model.length, view.id)) - ))); + ))).then(() => undefined); } - return Promise.resolve(null); + return Promise.resolve(); } protected onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] { @@ -479,7 +479,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio return addedViews; } - private alertSearchResult(count: number, viewId: string) { + private alertSearchResult(count: number, viewId: string): void { switch (count) { case 0: break; @@ -531,7 +531,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio return this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => promise); } - private onError(err: any): void { + private onError(err: Error): void { if (isPromiseCanceledError(err)) { return; } @@ -614,7 +614,7 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { .then(() => this.loopCheckForMaliciousExtensions()); } - private checkForMaliciousExtensions(): Promise { + private checkForMaliciousExtensions(): Promise { return this.extensionsManagementService.getExtensionsReport().then(report => { const maliciousSet = getMaliciousExtensionsSet(report); @@ -635,9 +635,9 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { ); }))); } else { - return Promise.resolve(null); + return Promise.resolve(undefined); } - }); + }).then(() => undefined); }, err => this.logService.error(err)); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts index cf1140492d..c27981f8bb 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts @@ -178,10 +178,9 @@ export class ExtensionsListView extends ViewletPanel { return this.list ? this.list.model : model; }; - const isLocalQuery = ExtensionsListView.isInstalledExtensionsQuery(query) || /@builtin/.test(query); - const request = createCancelablePromise(token => (isLocalQuery ? this.queryLocal(parsedQuery, options) : this.queryGallery(parsedQuery, options, token)).then(successCallback).catch(errorCallback)); + const request = createCancelablePromise(token => this.query(parsedQuery, options, token).then(successCallback).catch(errorCallback)); this.queryRequest = { query, request }; - return request.then(successCallback).catch(errorCallback); + return request; } count(): number { @@ -215,6 +214,36 @@ export class ExtensionsListView extends ViewletPanel { } } + private async query(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g; + const ids: string[] = []; + let idMatch; + while ((idMatch = idRegex.exec(query.value)) !== null) { + const name = idMatch[1]; + ids.push(name); + } + if (ids.length) { + return this.queryByIds(ids, options, token); + } + if (ExtensionsListView.isInstalledExtensionsQuery(query.value) || /@builtin/.test(query.value)) { + return this.queryLocal(query, options); + } + return this.queryGallery(query, options, token); + } + + private async queryByIds(ids: string[], options: IQueryOptions, token: CancellationToken): Promise> { + const idsSet: Set = ids.reduce((result, id) => { result.add(id.toLowerCase()); return result; }, new Set()); + const result = (await this.extensionsWorkbenchService.queryLocal()) + .filter(e => idsSet.has(e.identifier.id.toLowerCase())); + + if (result.length) { + return this.getPagedModel(this.sortExtensions(result, options)); + } + + return this.extensionsWorkbenchService.queryGallery({ names: ids, source: 'queryById' }, token) + .then(pager => this.getPagedModel(pager)); + } + private async queryLocal(query: Query, options: IQueryOptions): Promise> { let value = query.value; if (/@builtin/i.test(value)) { @@ -347,21 +376,6 @@ export class ExtensionsListView extends ViewletPanel { options.sortBy = SortBy.InstallCount; } - let value = query.value; - - const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g; - let idMatch; - const names: string[] = []; - while ((idMatch = idRegex.exec(value)) !== null) { - const name = idMatch[1]; - names.push(name); - } - - if (names.length) { - return this.extensionsWorkbenchService.queryGallery({ names, source: 'queryById' }, token) - .then(pager => this.getPagedModel(pager)); - } - if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) { return this.getWorkspaceRecommendationsModel(query, options, token); } else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) { diff --git a/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts index 88548197e3..46d9a7ec6d 100644 --- a/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts @@ -1056,7 +1056,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, const extension = local.filter(local => areSameExtensions(local.identifier, { id: extensionId }))[0]; if (extension) { - return this.windowService.show() + return this.windowService.focusWindow() .then(() => this.open(extension)); } @@ -1067,7 +1067,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, const extension = result.firstPage[0]; - return this.windowService.show().then(() => { + return this.windowService.focusWindow().then(() => { return this.open(extension).then(() => { this.notificationService.prompt( Severity.Info, diff --git a/src/vs/workbench/contrib/externalTerminal/electron-browser/externalTerminalService.ts b/src/vs/workbench/contrib/externalTerminal/electron-browser/externalTerminalService.ts index ac603f8ecc..a1b99fbe31 100644 --- a/src/vs/workbench/contrib/externalTerminal/electron-browser/externalTerminalService.ts +++ b/src/vs/workbench/contrib/externalTerminal/electron-browser/externalTerminalService.ts @@ -71,7 +71,7 @@ export class WindowsExternalTerminalService implements IExternalTerminalService }); } - private spawnTerminal(spawner, configuration: IExternalTerminalConfiguration, command: string, cwd?: string): Promise { + private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, command: string, cwd?: string): Promise { const terminalConfig = configuration.terminal.external; const exec = terminalConfig.windowsExec || getDefaultTerminalWindows(); const spawnType = this.getSpawnType(exec); @@ -84,7 +84,7 @@ export class WindowsExternalTerminalService implements IExternalTerminalService // cmder ignores the environment cwd and instead opts to always open in %USERPROFILE% // unless otherwise specified if (spawnType === WinSpawnType.CMDER) { - spawner.spawn(exec, [cwd]); + spawner.spawn(exec, cwd ? [cwd] : undefined); return Promise.resolve(undefined); } @@ -192,12 +192,16 @@ export class MacExternalTerminalService implements IExternalTerminalService { }); } - private spawnTerminal(spawner, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { + private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { const terminalConfig = configuration.terminal.external; const terminalApp = terminalConfig.osxExec || DEFAULT_TERMINAL_OSX; return new Promise((c, e) => { - const child = spawner.spawn('/usr/bin/open', ['-a', terminalApp, cwd]); + const args = ['-a', terminalApp]; + if (cwd) { + args.push(cwd); + } + const child = spawner.spawn('/usr/bin/open', args); child.on('error', e); child.on('exit', () => c()); }); @@ -276,13 +280,13 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { }); } - private spawnTerminal(spawner, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { + private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise { const terminalConfig = configuration.terminal.external; const execPromise = terminalConfig.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : getDefaultTerminalLinuxReady(); - const env = cwd ? { cwd: cwd } : undefined; return new Promise((c, e) => { execPromise.then(exec => { + const env = cwd ? { cwd } : undefined; const child = spawner.spawn(exec, [], env); child.on('error', e); child.on('exit', () => c()); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 62d0d6498b..3bb202bba8 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -285,16 +285,15 @@ export class ExplorerView extends ViewletPanel { accessibilityProvider: new ExplorerAccessibilityProvider(), ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), identityProvider: { - getId: stat => (stat).resource + getId: (stat: ExplorerItem) => stat.resource }, keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: stat => { - const item = stat; - if (this.explorerService.isEditable(item)) { + getKeyboardNavigationLabel: (stat: ExplorerItem) => { + if (this.explorerService.isEditable(stat)) { return undefined; } - return item.name; + return stat.name; } }, multipleSelectionSupport: true, diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index e6c3314957..59a61f5826 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -67,7 +67,8 @@ export class ExplorerDataSource implements IAsyncDataSource { - // Do not show error for roots since we already use an explorer decoration to notify user - if (!(element instanceof ExplorerItem && element.isRoot)) { + + if (element instanceof ExplorerItem && element.isRoot) { + if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + // Single folder create a dummy explorer item to show error + const placeholder = new ExplorerItem(element.resource, undefined, false); + placeholder.isError = true; + + return [placeholder]; + } + } else { + // Do not show error for roots since we already use an explorer decoration to notify user this.notificationService.error(e); } diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 1543776014..1fcd2667e9 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -3,14 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentRangeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; import * as nls from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -18,6 +16,8 @@ import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAnd import { Range } from 'vs/editor/common/core/range'; import { showExtensionQuery } from 'vs/workbench/contrib/format/browser/showExtensionQuery'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; interface IIndexedPick extends IQuickPickItem { index: number; @@ -28,20 +28,34 @@ const openExtensionAction: IQuickInputButton = { iconClass: 'format-show-extension' }; +function logFormatterTelemetry(telemetryService: ITelemetryService, mode: 'document' | 'range', options: T[], pick?: T) { + + function extKey(obj: T): string { + return obj.extensionId ? ExtensionIdentifier.toKey(obj.extensionId) : 'unknown'; + } + /* + * __GDPR__ + "formatterpick" : { + "mode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "pick" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + telemetryService.publicLog('formatterpick', { + mode, + extensions: options.map(extKey), + pick: pick ? extKey(pick) : 'none' + }); +} + registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { constructor() { super({ id: 'editor.action.formatDocument.multiple', - label: nls.localize('formatDocument.label.multiple', "Format Document..."), + label: nls.localize('formatDocument.label.multiple', "Format Document With..."), alias: 'Format Document...', precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasMultipleDocumentFormattingProvider), - kbOpts: { - kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F, - linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I }, - weight: KeybindingWeight.EditorContrib, - }, menuOpts: { group: '1_modification', order: 1.3 @@ -56,6 +70,7 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { const instaService = accessor.get(IInstantiationService); const quickPickService = accessor.get(IQuickInputService); const viewletService = accessor.get(IViewletService); + const telemetryService = accessor.get(ITelemetryService); const model = editor.getModel(); const provider = getRealAndSyntheticDocumentFormattersOrdered(model); @@ -77,6 +92,8 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { if (pick) { await instaService.invokeFunction(formatDocumentWithProvider, provider[pick.index], editor, CancellationToken.None); } + + logFormatterTelemetry(telemetryService, 'document', provider, pick && provider[pick.index]); } }); @@ -85,14 +102,9 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { constructor() { super({ id: 'editor.action.formatSelection.multiple', - label: nls.localize('formatSelection.label.multiple', "Format Selection..."), + label: nls.localize('formatSelection.label.multiple', "Format Selection With..."), alias: 'Format Code...', precondition: ContextKeyExpr.and(ContextKeyExpr.and(EditorContextKeys.writable), EditorContextKeys.hasMultipleDocumentSelectionFormattingProvider), - kbOpts: { - kbExpr: EditorContextKeys.editorTextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F), - weight: KeybindingWeight.EditorContrib - }, menuOpts: { when: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection), group: '1_modification', @@ -108,6 +120,7 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { const instaService = accessor.get(IInstantiationService); const quickPickService = accessor.get(IQuickInputService); const viewletService = accessor.get(IViewletService); + const telemetryService = accessor.get(ITelemetryService); const model = editor.getModel(); let range: Range = editor.getSelection(); @@ -134,5 +147,7 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { if (pick) { await instaService.invokeFunction(formatDocumentRangeWithProvider, provider[pick.index], editor, range, CancellationToken.None); } + + logFormatterTelemetry(telemetryService, 'range', provider, pick && provider[pick.index]); } }); diff --git a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts index 543b840a7d..af3985c5c6 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts @@ -12,6 +12,8 @@ import { OpenIssueReporterAction, ReportPerformanceIssueUsingReporterAction, Ope import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; import { WorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issueService'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IIssueService } from 'vs/platform/issue/common/issue'; const helpCategory = nls.localize('help', "Help"); const workbenchActionsRegistry = Registry.as(Extensions.WorkbenchActions); @@ -25,3 +27,7 @@ const developerCategory = nls.localize('developer', "Developer"); workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenProcessExplorer, OpenProcessExplorer.ID, OpenProcessExplorer.LABEL), 'Developer: Open Process Explorer', developerCategory); registerSingleton(IWorkbenchIssueService, WorkbenchIssueService, true); + +CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { + return accessor.get(IIssueService).getSystemStatus(); +}); diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index b3536f4ec0..7f1760a4e3 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -318,7 +318,7 @@ class MarkerWidget extends Disposable { dom.toggleClass(messageContainer, 'multiline', multiline); let lastLineElement = messageContainer; - for (let index = 0; index < lines.length; index++) { + for (let index = 0; index < (multiline ? lines.length : 1); index++) { lastLineElement = dom.append(messageContainer, dom.$('.marker-message-line')); const highlightedLabel = new HighlightedLabel(lastLineElement, false); highlightedLabel.set(lines[index], lineMatches[index]); diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index c2cfcb1a63..4c3ac50a11 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -33,7 +33,7 @@ .vs .monaco-action-bar .markers-panel-action-filter .monaco-inputbox { height: 25px; - border: 1px solid #ddd; + border: 1px solid transparent; } .markers-panel-action-filter > .markers-panel-filter-controls { diff --git a/src/vs/workbench/contrib/outline/browser/outlinePanel.ts b/src/vs/workbench/contrib/outline/browser/outlinePanel.ts index f5703198eb..76e9bb01a2 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePanel.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePanel.ts @@ -368,7 +368,7 @@ export class OutlinePanel extends ViewletPanel { this.disposables.push(this.onDidChangeBodyVisibility(visible => { if (visible && !this._requestOracle) { - this._requestOracle = this._instantiationService.createInstance(RequestOracle, (editor, event) => this._doUpdate(editor, event), DocumentSymbolProviderRegistry); + this._requestOracle = this._instantiationService.createInstance(RequestOracle, (editor: ICodeEditor | undefined, event: IModelContentChangedEvent | undefined) => this._doUpdate(editor, event), DocumentSymbolProviderRegistry); } else if (!visible) { dispose(this._requestOracle); this._requestOracle = undefined; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts index e7d5e57157..c98836240f 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts @@ -88,6 +88,24 @@ export class OpenGlobalSettingsAction extends Action { } } +export class OpenRemoteSettingsAction extends Action { + + static readonly ID = 'workbench.action.openRemoteSettings'; + static readonly LABEL = nls.localize('openRemoteSettings', "Open User Settings (Remote)"); + + constructor( + id: string, + label: string, + @IPreferencesService private readonly preferencesService: IPreferencesService, + ) { + super(id, label); + } + + run(event?: any): Promise { + return this.preferencesService.openRemoteSettings(); + } +} + export class OpenGlobalKeybindingsAction extends Action { static readonly ID = 'workbench.action.openGlobalKeybindings'; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 60e2b59936..4aee6e0fcc 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -995,6 +995,9 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre } })); + toDispose.push(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); + toDispose.push(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); + return template; } diff --git a/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts index 58cce34632..e967c56e79 100644 --- a/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts @@ -13,7 +13,7 @@ import * as nls from 'vs/nls'; import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; +import { WorkbenchStateContext, IsRemoteContext } from 'vs/workbench/common/contextkeys'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -28,7 +28,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchCo import { EditorInput, Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { KeybindingsEditor } from 'vs/workbench/contrib/preferences/browser/keybindingsEditor'; -import { ConfigureLanguageBasedSettingsAction, OpenDefaultKeybindingsFileAction, OpenFolderSettingsAction, OpenGlobalKeybindingsAction, OpenGlobalKeybindingsFileAction, OpenGlobalSettingsAction, OpenRawDefaultSettingsAction, OpenSettings2Action, OpenSettingsJsonAction, OpenWorkspaceSettingsAction, OPEN_FOLDER_SETTINGS_COMMAND, OPEN_FOLDER_SETTINGS_LABEL } from 'vs/workbench/contrib/preferences/browser/preferencesActions'; +import { ConfigureLanguageBasedSettingsAction, OpenDefaultKeybindingsFileAction, OpenFolderSettingsAction, OpenGlobalKeybindingsAction, OpenGlobalKeybindingsFileAction, OpenGlobalSettingsAction, OpenRawDefaultSettingsAction, OpenSettings2Action, OpenSettingsJsonAction, OpenWorkspaceSettingsAction, OPEN_FOLDER_SETTINGS_COMMAND, OPEN_FOLDER_SETTINGS_LABEL, OpenRemoteSettingsAction } from 'vs/workbench/contrib/preferences/browser/preferencesActions'; import { PreferencesEditor } from 'vs/workbench/contrib/preferences/browser/preferencesEditor'; import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IKeybindingsEditor, IPreferencesSearchService, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, MODIFIED_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING, SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED, SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH, SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST, SETTINGS_EDITOR_COMMAND_SEARCH, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON, SETTINGS_COMMAND_OPEN_SETTINGS, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN } from 'vs/workbench/contrib/preferences/common/preferences'; import { PreferencesContribution } from 'vs/workbench/contrib/preferences/common/preferencesContribution'; @@ -566,6 +566,18 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { group: '1_keyboard_preferences_actions' }); +CommandsRegistry.registerCommand(OpenRemoteSettingsAction.ID, serviceAccessor => { + serviceAccessor.get(IInstantiationService).createInstance(OpenRemoteSettingsAction, OpenRemoteSettingsAction.ID, OpenRemoteSettingsAction.LABEL).run(); +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: OpenRemoteSettingsAction.ID, + title: { value: OpenRemoteSettingsAction.LABEL, original: 'Preferences: Open Remote Settings' }, + category: nls.localize('preferencesCategory', "Preferences") + }, + when: IsRemoteContext +}); + abstract class SettingsCommand extends Command { protected getPreferencesEditor(accessor: ServicesAccessor): PreferencesEditor | SettingsEditor2 | null { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index c871c66bf0..6f0e5afb6d 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -234,7 +234,7 @@ export class MainPanel extends ViewletPanel { protected renderBody(container: HTMLElement): void { const delegate = new ProvidersListDelegate(); const renderer = this.instantiationService.createInstance(ProviderRenderer); - const identityProvider = { getId: r => r.provider.id }; + const identityProvider = { getId: (r: ISCMRepository) => r.provider.id }; this.list = this.instantiationService.createInstance(WorkbenchList, container, delegate, [renderer], { identityProvider, diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index cc9ea5a5cb..531d393186 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -394,5 +394,5 @@ } .vs .search-panel .search-view .monaco-inputbox { - border: 1px solid #ddd; + border: 1px solid transparent; } diff --git a/src/vs/workbench/contrib/search/browser/openFileHandler.ts b/src/vs/workbench/contrib/search/browser/openFileHandler.ts index 6ce11dff93..d358290f68 100644 --- a/src/vs/workbench/contrib/search/browser/openFileHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openFileHandler.ts @@ -3,37 +3,38 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as errors from 'vs/base/common/errors'; -import * as nls from 'vs/nls'; -import { isAbsolute } from 'vs/base/common/path'; -import * as objects from 'vs/base/common/objects'; -import { defaultGenerator } from 'vs/base/common/idGenerator'; -import { URI } from 'vs/base/common/uri'; -import { basename, dirname } from 'vs/base/common/resources'; import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { IModeService } from 'vs/editor/common/services/modeService'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import * as errors from 'vs/base/common/errors'; +import { defaultGenerator } from 'vs/base/common/idGenerator'; +import { untildify } from 'vs/base/common/labels'; +import { Schemas } from 'vs/base/common/network'; +import * as objects from 'vs/base/common/objects'; +import { isAbsolute } from 'vs/base/common/path'; +import { basename, dirname } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { QuickOpenEntry, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; +import { IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen'; +import { IPreparedQuery, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { IRange } from 'vs/editor/common/core/range'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen'; -import { QuickOpenEntry, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { QuickOpenHandler, EditorQuickOpenEntry } from 'vs/workbench/browser/quickopen'; -import { QueryBuilder, IFileQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; -import { EditorInput, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; -import { IResourceInput } from 'vs/platform/editor/common/editor'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ISearchService, IFileSearchStats, IFileQuery, ISearchComplete } from 'vs/workbench/services/search/common/search'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IRange } from 'vs/editor/common/core/range'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { EditorQuickOpenEntry, QuickOpenHandler } from 'vs/workbench/browser/quickopen'; +import { EditorInput, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; +import { IFileQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { prepareQuery, IPreparedQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; -import { IFileService } from 'vs/platform/files/common/files'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { untildify } from 'vs/base/common/labels'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { IFileQuery, IFileSearchStats, ISearchComplete, ISearchService } from 'vs/workbench/services/search/common/search'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; export class FileQuickOpenModel extends QuickOpenModel { @@ -142,9 +143,6 @@ export class OpenFileHandler extends QuickOpenHandler { return Promise.resolve(new FileQuickOpenModel([])); } - // Untildify file pattern - query.value = untildify(query.value, this.environmentService.userHome); - // Do find results return this.doFindResults(query, token, this.cacheState.cacheKey, maxSortedResults); } @@ -186,10 +184,16 @@ export class OpenFileHandler extends QuickOpenHandler { } private getAbsolutePathResult(query: IPreparedQuery): Promise { - if (isAbsolute(query.original)) { - const resource = URI.file(query.original); + const detildifiedQuery = untildify(query.original, this.environmentService.userHome); + if (isAbsolute(detildifiedQuery)) { + const workspaceFolders = this.contextService.getWorkspace().folders; + const resource = workspaceFolders[0] && workspaceFolders[0].uri.scheme !== Schemas.file ? + workspaceFolders[0].uri.with({ path: detildifiedQuery }) : + URI.file(detildifiedQuery); - return this.fileService.resolveFile(resource).then(stat => stat.isDirectory ? undefined : resource, error => undefined); + return this.fileService.resolveFile(resource).then( + stat => stat.isDirectory ? undefined : resource, + error => undefined); } return Promise.resolve(undefined); diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 4030718d6c..4f9bea6df5 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -888,7 +888,8 @@ export interface TaskEvent { } export const enum TaskRunSource { - User, // Default + System, + User, FolderOpen, ConfigurationChange } @@ -932,7 +933,7 @@ export namespace KeyedTaskIdentifier { for (let position in keys) { let stringified = literal[keys[position]]; if (stringified instanceof Object) { - stringified = sortedStringify(test); + stringified = sortedStringify(stringified); } else if (typeof stringified === 'string') { stringified = stringified.replace(/,/g, ',,'); } @@ -942,7 +943,6 @@ export namespace KeyedTaskIdentifier { } export function create(value: TaskIdentifier): KeyedTaskIdentifier { const resultKey = sortedStringify(value); - console.log(resultKey); return { _key: resultKey, type: value.taskType }; } } diff --git a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts index 6e263f6364..5dfeda2490 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts @@ -870,7 +870,7 @@ class TaskService extends Disposable implements ITaskService { }); } - public run(task: Task | undefined, options?: ProblemMatcherRunOptions): Promise { + public run(task: Task | undefined, options?: ProblemMatcherRunOptions, runSource: TaskRunSource = TaskRunSource.System): Promise { if (!task) { throw new TaskError(Severity.Info, nls.localize('TaskServer.noTask', 'Task to execute is undefined'), TaskErrors.TaskNotFound); } @@ -886,7 +886,14 @@ class TaskService extends Disposable implements ITaskService { }); } return this.executeTask(task, resolver); - }).then(value => value, (error) => { + }).then((value) => { + if (runSource === TaskRunSource.User) { + this.getWorkspaceTasks().then(workspaceTasks => { + RunAutomaticTasks.promptForPermission(this, this.storageService, this.notificationService, workspaceTasks); + }); + } + return value; + }, (error) => { this.handleError(error); return Promise.reject(error); }); @@ -1548,11 +1555,6 @@ class TaskService extends Disposable implements ITaskService { return this._workspaceTasksPromise; } this.updateWorkspaceTasks(runSource); - if (runSource === TaskRunSource.User) { - this._workspaceTasksPromise!.then(workspaceFolderTasks => { - RunAutomaticTasks.promptForPermission(this, this.storageService, this.notificationService, workspaceFolderTasks); - }); - } return this._workspaceTasksPromise!; } @@ -2049,7 +2051,7 @@ class TaskService extends Disposable implements ITaskService { if (task === null) { this.runConfigureTasks(); } else { - this.run(task, { attachProblemMatcher: true }).then(undefined, reason => { + this.run(task, { attachProblemMatcher: true }, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index f278b014f5..bbce257372 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -422,11 +422,13 @@ export class TerminalInstance implements ITerminalInstance { if (this._shellLaunchConfig.initialText) { this._xterm.writeln(this._shellLaunchConfig.initialText); } - this._xterm.winptyCompatInit(); this._xterm.on('linefeed', () => this._onLineFeed()); this._xterm.on('key', (key, ev) => this._onKey(key, ev)); if (this._processManager) { + if (this._processManager.os === platform.OperatingSystem.Windows) { + this._xterm.winptyCompatInit(); + } this._processManager.onProcessData(data => this._onProcessData(data)); this._xterm.on('data', data => this._processManager!.write(data)); // TODO: How does the cwd work on detached processes? diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 8c17c1dd7a..d10e51219e 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -646,9 +646,9 @@ export interface ITerminalProcessManager extends IDisposable { readonly onProcessTitle: Event; readonly onProcessExit: Event; - addDisposable(disposable: IDisposable); - dispose(immediate?: boolean); - createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number); + addDisposable(disposable: IDisposable): void; + dispose(immediate?: boolean): void; + createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void; write(data: string): void; setDimensions(cols: number, rows: number): void; diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 6908a4caab..be331acdf3 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -22,7 +22,7 @@ import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configur import { LIGHT, DARK, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; import { colorThemeSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; export class SelectColorThemeAction extends Action { @@ -45,20 +45,21 @@ export class SelectColorThemeAction extends Action { return this.themeService.getColorThemes().then(themes => { const currentTheme = this.themeService.getColorTheme(); - const picks: QuickPickInput[] = ([] as QuickPickInput[]).concat( - toEntries(themes.filter(t => t.type === LIGHT), localize('themes.category.light', "light themes")), - toEntries(themes.filter(t => t.type === DARK), localize('themes.category.dark', "dark themes")), - toEntries(themes.filter(t => t.type === HIGH_CONTRAST), localize('themes.category.hc', "high contrast themes")), + const picks: QuickPickInput[] = [ + ...toEntries(themes.filter(t => t.type === LIGHT), localize('themes.category.light', "light themes")), + ...toEntries(themes.filter(t => t.type === DARK), localize('themes.category.dark', "dark themes")), + ...toEntries(themes.filter(t => t.type === HIGH_CONTRAST), localize('themes.category.hc', "high contrast themes")), // {{SQL CARBON EDIT}} - //configurationEntries(this.extensionGalleryService, localize('installColorThemes', "Install Additional Color Themes...")) - ); + // ...configurationEntries(this.extensionGalleryService, localize('installColorThemes', "Install Additional Color Themes...")) + ]; - const selectTheme = (theme: IColorTheme, applyTheme: boolean) => { + const selectTheme = (theme: ThemeItem, applyTheme: boolean) => { + let themeId = theme.id; if (typeof theme.id === 'undefined') { // 'pick in marketplace' entry if (applyTheme) { openExtensionViewlet(this.viewletService, 'category:themes '); } - theme = currentTheme; + themeId = currentTheme.id; } let target: ConfigurationTarget | undefined = undefined; if (applyTheme) { @@ -66,7 +67,7 @@ export class SelectColorThemeAction extends Action { target = typeof confValue.workspace !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; } - this.themeService.setColorTheme(theme.id, target).then(undefined, + this.themeService.setColorTheme(themeId, target).then(undefined, err => { onUnexpectedError(err); this.themeService.setColorTheme(currentTheme.id, undefined); @@ -75,12 +76,13 @@ export class SelectColorThemeAction extends Action { }; const placeHolder = localize('themes.selectTheme', "Select Color Theme (Up/Down Keys to Preview)"); - const autoFocusIndex = firstIndex(picks, p => p.type !== 'separator' && p.id === currentTheme.id); + const autoFocusIndex = firstIndex(picks, p => isItem(p) && p.id === currentTheme.id); + const activeItem: ThemeItem = picks[autoFocusIndex] as ThemeItem; const delayer = new Delayer(100); - const chooseTheme = theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0); - const tryTheme = theme => delayer.trigger(() => selectTheme(theme, false)); + const chooseTheme = (theme: ThemeItem) => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0); + const tryTheme = (theme: ThemeItem) => delayer.trigger(() => selectTheme(theme, false)); - return this.quickInputService.pick(picks, { placeHolder, activeItem: picks[autoFocusIndex], onDidFocus: tryTheme }) + return this.quickInputService.pick(picks, { placeHolder, activeItem, onDidFocus: tryTheme }) .then(chooseTheme); }); } @@ -108,26 +110,27 @@ class SelectIconThemeAction extends Action { return this.themeService.getFileIconThemes().then(themes => { const currentTheme = this.themeService.getFileIconTheme(); - let picks: QuickPickInput[] = [{ id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') }]; + let picks: QuickPickInput[] = [{ id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') }]; picks = picks.concat( toEntries(themes), // {{SQL CARBON EDIT}} // configurationEntries(this.extensionGalleryService, localize('installIconThemes', "Install Additional File Icon Themes...")) ); - const selectTheme = (theme, applyTheme: boolean) => { + const selectTheme = (theme: ThemeItem, applyTheme: boolean) => { + let themeId = theme.id; if (typeof theme.id === 'undefined') { // 'pick in marketplace' entry if (applyTheme) { openExtensionViewlet(this.viewletService, 'tag:icon-theme '); } - theme = currentTheme; + themeId = currentTheme.id; } let target: ConfigurationTarget | undefined = undefined; if (applyTheme) { let confValue = this.configurationService.inspect(ICON_THEME_SETTING); target = typeof confValue.workspace !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; } - this.themeService.setFileIconTheme(theme && theme.id, target).then(undefined, + this.themeService.setFileIconTheme(themeId, target).then(undefined, err => { onUnexpectedError(err); this.themeService.setFileIconTheme(currentTheme.id, undefined); @@ -136,18 +139,19 @@ class SelectIconThemeAction extends Action { }; const placeHolder = localize('themes.selectIconTheme', "Select File Icon Theme"); - const autoFocusIndex = firstIndex(picks, p => p.type !== 'separator' && p.id === currentTheme.id); + const autoFocusIndex = firstIndex(picks, p => isItem(p) && p.id === currentTheme.id); + const activeItem: ThemeItem = picks[autoFocusIndex] as ThemeItem; const delayer = new Delayer(100); - const chooseTheme = theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0); - const tryTheme = theme => delayer.trigger(() => selectTheme(theme, false)); + const chooseTheme = (theme: ThemeItem) => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0); + const tryTheme = (theme: ThemeItem) => delayer.trigger(() => selectTheme(theme, false)); - return this.quickInputService.pick(picks, { placeHolder, activeItem: picks[autoFocusIndex], onDidFocus: tryTheme }) + return this.quickInputService.pick(picks, { placeHolder, activeItem, onDidFocus: tryTheme }) .then(chooseTheme); }); } } -function configurationEntries(extensionGalleryService: IExtensionGalleryService, label: string): QuickPickInput[] { +function configurationEntries(extensionGalleryService: IExtensionGalleryService, label: string): QuickPickInput[] { if (extensionGalleryService.isEnabled()) { return [ { @@ -156,7 +160,7 @@ function configurationEntries(extensionGalleryService: IExtensionGalleryService, { id: undefined, label: label, - alwaysShow: true, + alwaysShow: true } ]; } @@ -171,11 +175,21 @@ function openExtensionViewlet(viewletService: IViewletService, query: string) { } }); } +interface ThemeItem { + id: string | undefined; + label: string; + description?: string; + alwaysShow?: boolean; +} -function toEntries(themes: Array, label?: string) { - const toEntry = theme => { id: theme.id, label: theme.label, description: theme.description }; - const sorter = (t1: IQuickPickItem, t2: IQuickPickItem) => t1.label.localeCompare(t2.label); - let entries: QuickPickInput[] = themes.map(toEntry).sort(sorter); +function isItem(i: QuickPickInput): i is ThemeItem { + return i['type'] !== 'separatpr'; +} + +function toEntries(themes: Array, label?: string): QuickPickInput[] { + const toEntry = (theme: IColorTheme): ThemeItem => ({ id: theme.id, label: theme.label, description: theme.description }); + const sorter = (t1: ThemeItem, t2: ThemeItem) => t1.label.localeCompare(t2.label); + let entries: QuickPickInput[] = themes.map(toEntry).sort(sorter); if (entries.length > 0 && label) { entries.unshift({ type: 'separator', label }); } diff --git a/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts index 524242c617..34a5679f9d 100644 --- a/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/electron-browser/releaseNotesEditor.ts @@ -209,7 +209,7 @@ export class ReleaseNotesManager { renderer.code = (code, lang) => { const modeId = this._modeService.getModeIdForLanguageName(lang); - return `${tokenizeToString(code, modeId ? TokenizationRegistry.get(modeId) : undefined)}`; + return `${tokenizeToString(code, modeId ? TokenizationRegistry.get(modeId)! : undefined)}`; }; return renderer; } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts index 8af8048a82..452f763fb4 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditor.ts @@ -257,9 +257,9 @@ export class WebviewEditor extends BaseEditor { private getDefaultLocalResourceRoots(): URI[] { const rootPaths = this._contextService.getWorkspace().folders.map(x => x.uri); - const extensionLocation = (this.input as WebviewEditorInput).extensionLocation; - if (extensionLocation) { - rootPaths.push(extensionLocation); + const extension = (this.input as WebviewEditorInput).extension; + if (extension) { + rootPaths.push(extension.location); } return rootPaths; } @@ -283,7 +283,7 @@ export class WebviewEditor extends BaseEditor { this._layoutService.getContainer(Parts.EDITOR_PART), { allowSvgs: true, - extensionLocation: input.extensionLocation, + extension: input.extension, enableFindWidget: input.options.enableFindWidget }, {}); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts index 5cb27ab80e..2376ad4d7b 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInput.ts @@ -7,6 +7,7 @@ import { Emitter } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IEditorModel } from 'vs/platform/editor/common/editor'; +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'; @@ -63,7 +64,10 @@ export class WebviewEditorInput extends EditorInput { private _scrollYPercentage: number = 0; private _state: any; - public readonly extensionLocation: URI | undefined; + public readonly extension?: { + readonly location: URI; + readonly id: ExtensionIdentifier; + }; private readonly _id: number; constructor( @@ -73,7 +77,10 @@ export class WebviewEditorInput extends EditorInput { options: WebviewInputOptions, state: any, events: WebviewEvents, - extensionLocation: URI | undefined, + extension: undefined | { + readonly location: URI; + readonly id: ExtensionIdentifier; + }, @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, ) { super(); @@ -89,7 +96,7 @@ export class WebviewEditorInput extends EditorInput { this._options = options; this._events = events; this._state = state; - this.extensionLocation = extensionLocation; + this.extension = extension; } public getTypeId(): string { @@ -313,11 +320,14 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput { options: WebviewInputOptions, state: any, events: WebviewEvents, - extensionLocation: URI | undefined, + extension: undefined | { + readonly location: URI; + readonly id: ExtensionIdentifier + }, public readonly reviver: (input: WebviewEditorInput) => Promise, @IWorkbenchLayoutService partService: IWorkbenchLayoutService, ) { - super(viewType, id, name, options, state, events, extensionLocation, partService); + super(viewType, id, 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/electron-browser/webviewEditorInputFactory.ts index 3b844e0539..2e42caed64 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorInputFactory.ts @@ -8,6 +8,7 @@ import { IEditorInputFactory } from 'vs/workbench/common/editor'; import { WebviewEditorInput } from './webviewEditorInput'; import { IWebviewEditorService, WebviewInputOptions } from './webviewEditorService'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; interface SerializedIconPath { light: string | UriComponents; @@ -20,6 +21,7 @@ interface SerializedWebview { readonly title: string; readonly options: WebviewInputOptions; readonly extensionLocation: string | UriComponents | undefined; + readonly extensionId: string | undefined; readonly state: any; readonly iconPath: SerializedIconPath | undefined; readonly group?: number; @@ -45,7 +47,8 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { id: input.getId(), title: input.getName(), options: input.options, - extensionLocation: input.extensionLocation, + extensionLocation: input.extension ? input.extension.location : undefined, + extensionId: input.extension ? input.extension.id.value : undefined, state: input.state, iconPath: input.iconPath ? { light: input.iconPath.light, dark: input.iconPath.dark, } : undefined, group: input.group @@ -64,8 +67,12 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { ): WebviewEditorInput { const data: SerializedWebview = JSON.parse(serializedEditorInput); 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, data.group); + return this._webviewService.reviveWebview(data.viewType, data.id, data.title, iconPath, data.state, data.options, extensionLocation ? { + location: extensionLocation, + id: extensionId + } : undefined, data.group); } } function reviveIconPath(data: SerializedIconPath | undefined) { diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts index bf9edd5f51..02537fbb23 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts @@ -7,12 +7,13 @@ import { equals } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; +import { IWebviewOptions, IWebviewPanelOptions } from 'vs/editor/common/modes'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { RevivedWebviewEditorInput, WebviewEditorInput } from './webviewEditorInput'; -import { IWebviewOptions, IWebviewPanelOptions } from 'vs/editor/common/modes'; export const IWebviewEditorService = createDecorator('webviewEditorService'); @@ -29,7 +30,10 @@ export interface IWebviewEditorService { title: string, showOptions: ICreateWebViewShowOptions, options: WebviewInputOptions, - extensionLocation: URI | undefined, + extension: undefined | { + location: URI, + id: ExtensionIdentifier + }, events: WebviewEvents ): WebviewEditorInput; @@ -40,7 +44,10 @@ export interface IWebviewEditorService { iconPath: { light: URI, dark: URI } | undefined, state: any, options: WebviewInputOptions, - extensionLocation: URI | undefined, + extension: undefined | { + readonly location: URI, + readonly id?: ExtensionIdentifier + }, group: number | undefined ): WebviewEditorInput; @@ -130,10 +137,13 @@ export class WebviewEditorService implements IWebviewEditorService { title: string, showOptions: ICreateWebViewShowOptions, options: IWebviewOptions, - extensionLocation: URI | undefined, + extension: undefined | { + location: URI, + id: ExtensionIdentifier + }, events: WebviewEvents ): WebviewEditorInput { - const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, undefined, title, options, {}, events, extensionLocation); + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, undefined, title, options, {}, events, extension); this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus }, showOptions.group); return webviewInput; } @@ -160,10 +170,13 @@ export class WebviewEditorService implements IWebviewEditorService { iconPath: { light: URI, dark: URI } | undefined, state: any, options: WebviewInputOptions, - extensionLocation: URI, + extension: undefined | { + readonly location: URI, + readonly id?: ExtensionIdentifier + }, group: number | undefined, ): WebviewEditorInput { - const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, id, title, options, state, {}, extensionLocation, async (webview: WebviewEditorInput): Promise => { + const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, id, title, options, state, {}, extension, async (webview: WebviewEditorInput): Promise => { const didRevive = await this.tryRevive(webview); if (didRevive) { return Promise.resolve(undefined); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 5cee5c015c..12a5ffb15e 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -19,6 +19,8 @@ import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/the import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols'; import { areWebviewInputOptionsEqual } from './webviewEditorService'; import { WebviewFindWidget } from './webviewFindWidget'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export interface WebviewPortMapping { readonly port: number; @@ -32,7 +34,10 @@ export interface WebviewPortMapping { export interface WebviewOptions { readonly allowSvgs?: boolean; - readonly extensionLocation?: URI; + readonly extension?: { + readonly location: URI; + readonly id?: ExtensionIdentifier; + }; readonly enableFindWidget?: boolean; } @@ -145,10 +150,14 @@ class WebviewPortMappingProvider extends Disposable { constructor( session: WebviewSession, - mappings: () => ReadonlyArray + mappings: () => ReadonlyArray, + extensionId: ExtensionIdentifier | undefined, + @ITelemetryService telemetryService: ITelemetryService, ) { super(); + let hasLogged = false; + session.onBeforeRequest(async (details) => { const uri = URI.parse(details.url); if (uri.scheme !== 'http' && uri.scheme !== 'https') { @@ -157,6 +166,17 @@ class WebviewPortMappingProvider extends Disposable { const localhostMatch = /^localhost:(\d+)$/.exec(uri.authority); if (localhostMatch) { + if (!hasLogged && extensionId) { + hasLogged = true; + + /* __GDPR__ + "webview.accessLocalhost" : { + "extension" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + telemetryService.publicLog('webview.accessLocalhost', { extension: extensionId.value }); + } + const port = +localhostMatch[1]; for (const mapping of mappings()) { if (mapping.port === port && mapping.port !== mapping.resolvedPort) { @@ -317,6 +337,7 @@ export class WebviewElement extends Disposable { @IThemeService themeService: IThemeService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, + @ITelemetryService telemetryService: ITelemetryService, ) { super(); this._webview = document.createElement('webview'); @@ -348,15 +369,16 @@ export class WebviewElement extends Disposable { this._register(new WebviewProtocolProvider( this._webview, - this._options.extensionLocation, + this._options.extension ? this._options.extension.location : undefined, () => (this._contentOptions.localResourceRoots || []), environmentService, fileService)); this._register(new WebviewPortMappingProvider( session, - () => (this._contentOptions.portMappings || []) - )); + () => (this._contentOptions.portMappings || []), + _options.extension ? _options.extension.id : undefined, + telemetryService)); if (!this._options.allowSvgs) { @@ -556,7 +578,7 @@ export class WebviewElement extends Disposable { colors['vscode-' + entry.id.replace('.', '-')] = color.toString(); } return colors; - }, {}); + }, {} as { [key: string]: string }); const styles = { diff --git a/src/vs/workbench/electron-browser/actions/windowActions.ts b/src/vs/workbench/electron-browser/actions/windowActions.ts index 44fccd3185..ad6be4b884 100644 --- a/src/vs/workbench/electron-browser/actions/windowActions.ts +++ b/src/vs/workbench/electron-browser/actions/windowActions.ts @@ -253,7 +253,7 @@ export abstract class BaseSwitchWindow extends Action { }); }).then(pick => { if (pick) { - this.windowsService.showWindow(pick.payload); + this.windowsService.focusWindow(pick.payload); } }); } diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 481e69c9a0..99e62dee19 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -25,9 +25,6 @@ import { SupportsWorkspacesContext, IsMacContext, HasMacNativeTabsContext, IsDev import { NoEditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; import { LogStorageAction } from 'vs/platform/storage/node/storageService'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { DiskFileSystemSupport } from 'vs/workbench/services/files2/node/diskFileSystemSupport'; // {{SQL CARBON EDIT}} import { InstallVSIXAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; @@ -706,10 +703,3 @@ import { InstallVSIXAction } from 'vs/workbench/contrib/extensions/electron-brow } }); })(); - -// Disk File System -(function registerFileSystem(): void { - const registry = Registry.as(WorkbenchExtensions.Workbench); - - registry.registerWorkbenchContribution(DiskFileSystemSupport, LifecyclePhase.Starting); -})(); diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 344766ed69..0e513ce5a5 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -12,7 +12,7 @@ import { ElectronWindow } from 'vs/workbench/electron-browser/window'; import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser'; 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 { isLinux, isMacintosh, isWindows, OperatingSystem } 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 { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -43,6 +43,11 @@ import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-brow import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; 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 { IFileService } from 'vs/platform/files/common/files'; +import { DiskFileSystemProvider } from 'vs/workbench/services/files2/electron-browser/diskFileSystemProvider'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME, RemoteExtensionsFileSystemProvider } from 'vs/platform/remote/common/remoteAgentFileSystemChannel'; class CodeRendererMain extends Disposable { @@ -172,14 +177,31 @@ class CodeRendererMain extends Disposable { const logService = this._register(this.createLogService(mainProcessService, environmentService)); serviceCollection.set(ILogService, logService); + // Files + const fileService = new FileService2(logService); + serviceCollection.set(IFileService, fileService); + + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider()); + // Remote const remoteAuthorityResolverService = new RemoteAuthorityResolverService(); serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); const remoteAgentService = new RemoteAgentService(this.configuration, environmentService, remoteAuthorityResolverService); serviceCollection.set(IRemoteAgentService, remoteAgentService); + const connection = remoteAgentService.getConnection(); + if (connection) { + const channel = connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); + const fileSystemProvider = new RemoteExtensionsFileSystemProvider(channel); + fileService.registerProvider('vscode-remote', fileSystemProvider); + remoteAgentService.getEnvironment().then(remoteAgentEnvironment => { + const isCaseSensitive = !!(remoteAgentEnvironment && remoteAgentEnvironment.os === OperatingSystem.Linux); + fileSystemProvider.setCaseSensitive(isCaseSensitive); + }); + } + return this.resolveWorkspaceInitializationPayload(environmentService).then(payload => Promise.all([ - this.createWorkspaceService(payload, environmentService, logService).then(service => { + this.createWorkspaceService(payload, environmentService, remoteAgentService, logService).then(service => { // Workspace serviceCollection.set(IWorkspaceContextService, service); @@ -270,8 +292,8 @@ class CodeRendererMain extends Disposable { }, error => onUnexpectedError(error)); } - private createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IEnvironmentService, logService: ILogService): Promise { - const workspaceService = new WorkspaceService(environmentService); + private createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IEnvironmentService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { + const workspaceService = new WorkspaceService(this.configuration, environmentService, remoteAgentService); return workspaceService.initialize(payload).then(() => workspaceService, error => { onUnexpectedError(error); diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 66e1ca3b2f..866faa8717 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -15,7 +15,7 @@ import { isResourceFileEdit, isResourceTextEdit, ResourceFileEdit, ResourceTextE import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { emptyProgressRunner, IProgress, IProgressRunner } from 'vs/platform/progress/common/progress'; @@ -326,7 +326,12 @@ export class BulkEdit { } else if (!edit.newUri && edit.oldUri) { // delete file if (!options.ignoreIfNotExists || await this._fileService.existsFile(edit.oldUri)) { - await this._textFileService.delete(edit.oldUri, { useTrash: this._configurationService.getValue('files.enableTrash'), recursive: options.recursive }); + let useTrash = this._configurationService.getValue('files.enableTrash'); + if (useTrash && !(await this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) { + useTrash = false; // not supported by provider + } + + await this._textFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive }); } } else if (edit.newUri && !edit.oldUri) { diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 5c723ccaeb..12c85e9180 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -28,6 +28,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { withUndefinedAsNull, withNullAsUndefined } from 'vs/base/common/types'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export const enum ConfigurationEditingErrorCode { @@ -120,6 +121,7 @@ export class ConfigurationEditingService { public _serviceBrand: any; private queue: Queue; + private remoteSettingsResource: URI | null; constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @@ -130,9 +132,15 @@ export class ConfigurationEditingService { @ITextFileService private readonly textFileService: ITextFileService, @INotificationService private readonly notificationService: INotificationService, @IPreferencesService private readonly preferencesService: IPreferencesService, - @IEditorService private readonly editorService: IEditorService + @IEditorService private readonly editorService: IEditorService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService ) { this.queue = new Queue(); + remoteAgentService.getEnvironment().then(environment => { + if (environment) { + this.remoteSettingsResource = environment.appSettingsPath; + } + }); } writeConfiguration(target: ConfigurationTarget, value: IConfigurationValue, options: IConfigurationEditingOptions = {}): Promise { @@ -458,7 +466,7 @@ export class ConfigurationEditingService { if (config.key) { const standaloneConfigurationKeys = Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS); for (const key of standaloneConfigurationKeys) { - const resource = this.getConfigurationFileResource(target, WORKSPACE_STANDALONE_CONFIGURATIONS[key], overrides.resource); + const resource = this.getConfigurationFileResource(target, config, WORKSPACE_STANDALONE_CONFIGURATIONS[key], overrides.resource); // Check for prefix if (config.key === key) { @@ -478,10 +486,10 @@ export class ConfigurationEditingService { let key = config.key; let jsonPath = overrides.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier), key] : [key]; if (target === ConfigurationTarget.USER) { - return { key, jsonPath, value: config.value, resource: URI.file(this.environmentService.appSettingsPath), target }; + return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, config, '', null)), target }; } - const resource = this.getConfigurationFileResource(target, FOLDER_SETTINGS_PATH, overrides.resource); + const resource = this.getConfigurationFileResource(target, config, FOLDER_SETTINGS_PATH, overrides.resource); if (this.isWorkspaceConfigurationResource(resource)) { jsonPath = ['settings', ...jsonPath]; } @@ -493,8 +501,17 @@ export class ConfigurationEditingService { return !!(workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath); } - private getConfigurationFileResource(target: ConfigurationTarget, relativePath: string, resource: URI | null | undefined): URI | null { + private getConfigurationFileResource(target: ConfigurationTarget, config: IConfigurationValue, relativePath: string, resource: URI | null | undefined): URI | null { + if (target === ConfigurationTarget.USER_LOCAL) { + return URI.file(this.environmentService.appSettingsPath); + } + if (target === ConfigurationTarget.USER_REMOTE) { + return this.remoteSettingsResource; + } if (target === ConfigurationTarget.USER) { + if (this.configurationService.inspect(config.key).userRemote !== undefined) { + return this.remoteSettingsResource; + } return URI.file(this.environmentService.appSettingsPath); } diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index c96079bf39..2a1df3e357 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -131,13 +131,14 @@ export class Configuration extends BaseConfiguration { constructor( defaults: ConfigurationModel, - user: ConfigurationModel, + localUser: ConfigurationModel, + remoteUser: ConfigurationModel, workspaceConfiguration: ConfigurationModel, folders: ResourceMap, memoryConfiguration: ConfigurationModel, memoryConfigurationByResource: ResourceMap, private readonly _workspace?: Workspace) { - super(defaults, user, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); + super(defaults, localUser, remoteUser, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); } getValue(key: string | undefined, overrides: IConfigurationOverrides = {}): any { @@ -164,17 +165,26 @@ export class Configuration extends BaseConfiguration { return super.keys(this._workspace); } - compareAndUpdateUserConfiguration(user: ConfigurationModel): ConfigurationChangeEvent { - const { added, updated, removed } = compare(this.user, user); + compareAndUpdateLocalUserConfiguration(user: ConfigurationModel): ConfigurationChangeEvent { + const { added, updated, removed } = compare(this.localUserConfiguration, user); let changedKeys = [...added, ...updated, ...removed]; if (changedKeys.length) { - super.updateUserConfiguration(user); + super.updateLocalUserConfiguration(user); + } + return new ConfigurationChangeEvent().change(changedKeys); + } + + compareAndUpdateRemoteUserConfiguration(user: ConfigurationModel): ConfigurationChangeEvent { + const { added, updated, removed } = compare(this.remoteUserConfiguration, user); + let changedKeys = [...added, ...updated, ...removed]; + if (changedKeys.length) { + super.updateRemoteUserConfiguration(user); } return new ConfigurationChangeEvent().change(changedKeys); } compareAndUpdateWorkspaceConfiguration(workspaceConfiguration: ConfigurationModel): ConfigurationChangeEvent { - const { added, updated, removed } = compare(this.workspace, workspaceConfiguration); + const { added, updated, removed } = compare(this.workspaceConfiguration, workspaceConfiguration); let changedKeys = [...added, ...updated, ...removed]; if (changedKeys.length) { super.updateWorkspaceConfiguration(workspaceConfiguration); @@ -183,7 +193,7 @@ export class Configuration extends BaseConfiguration { } compareAndUpdateFolderConfiguration(resource: URI, folderConfiguration: ConfigurationModel): ConfigurationChangeEvent { - const currentFolderConfiguration = this.folders.get(resource); + const currentFolderConfiguration = this.folderConfigurations.get(resource); if (currentFolderConfiguration) { const { added, updated, removed } = compare(currentFolderConfiguration, folderConfiguration); let changedKeys = [...added, ...updated, ...removed]; @@ -202,7 +212,7 @@ export class Configuration extends BaseConfiguration { // Do not remove workspace configuration return new ConfigurationChangeEvent(); } - const folderConfig = this.folders.get(folder); + const folderConfig = this.folderConfigurations.get(folder); if (!folderConfig) { throw new Error('Unknown folder'); } diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index 920b69613d..765cbdda41 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -25,7 +25,145 @@ 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 } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationModel, compare } from 'vs/platform/configuration/common/configuration'; +import { FileServiceBasedUserConfiguration, NodeBasedUserConfiguration } from 'vs/platform/configuration/node/configuration'; + +export class LocalUserConfiguration extends Disposable { + + private userConfiguration: NodeBasedUserConfiguration | FileServiceBasedUserConfiguration; + + private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); + public readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; + + constructor( + environmentService: IEnvironmentService + ) { + super(); + this.userConfiguration = this._register(new NodeBasedUserConfiguration(environmentService.appSettingsPath)); + this._register(this.userConfiguration.onDidChangeConfiguration(configurationModel => this._onDidChangeConfiguration.fire(configurationModel))); + } + + initialize(): Promise { + return this.userConfiguration.initialize(); + } + + reload(): Promise { + return this.userConfiguration.reload(); + } + + async adopt(fileService: IFileService): Promise { + return null; + } +} + +export class RemoteUserConfiguration extends Disposable { + + private readonly _cachedConfiguration: CachedUserConfiguration; + private _userConfiguration: FileServiceBasedUserConfiguration | CachedUserConfiguration; + + private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); + public readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; + + constructor( + remoteAuthority: string, + environmentService: IEnvironmentService + ) { + super(); + this._userConfiguration = this._cachedConfiguration = new CachedUserConfiguration(remoteAuthority, environmentService); + } + + initialize(): Promise { + return this._userConfiguration.initialize(); + } + + reload(): Promise { + return this._userConfiguration.reload(); + } + + async adopt(configurationResource: URI | null, fileService: IFileService): Promise { + if (this._userConfiguration instanceof CachedUserConfiguration) { + const oldConfigurationModel = this._userConfiguration.getConfigurationModel(); + let newConfigurationModel = new ConfigurationModel(); + if (configurationResource) { + this._userConfiguration = new FileServiceBasedUserConfiguration(configurationResource, fileService); + this._register(this._userConfiguration.onDidChangeConfiguration(configurationModel => this.onDidUserConfigurationChange(configurationModel))); + newConfigurationModel = await this._userConfiguration.initialize(); + } + const { added, updated, removed } = compare(oldConfigurationModel, newConfigurationModel); + if (added.length > 0 || updated.length > 0 || removed.length > 0) { + this.updateCache(newConfigurationModel); + return newConfigurationModel; + } + } + return null; + } + + private onDidUserConfigurationChange(configurationModel: ConfigurationModel): void { + this.updateCache(configurationModel); + this._onDidChangeConfiguration.fire(configurationModel); + } + + private updateCache(configurationModel: ConfigurationModel): Promise { + return this._cachedConfiguration.updateConfiguration(configurationModel); + } +} + +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 configurationModel: ConfigurationModel; + + constructor( + remoteAuthority: string, + private environmentService: IEnvironmentService + ) { + super(); + this.cachedFolderPath = join(this.environmentService.userDataPath, 'CachedConfigurations', 'user', remoteAuthority); + this.cachedConfigurationPath = join(this.cachedFolderPath, 'configuration.json'); + this.configurationModel = new ConfigurationModel(); + } + + getConfigurationModel(): ConfigurationModel { + return this.configurationModel; + } + + initialize(): Promise { + 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; + }); + } + + 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; + }); + } + + 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; diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 9b97dddd26..4fad94b840 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -28,14 +28,15 @@ 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 } from 'vs/workbench/services/configuration/node/configuration'; +import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, LocalUserConfiguration } from 'vs/workbench/services/configuration/node/configuration'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; -import { UserConfiguration } from 'vs/platform/configuration/node/configuration'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { localize } from 'vs/nls'; 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 { @@ -45,7 +46,8 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private completeWorkspaceBarrier: Barrier; private _configuration: Configuration; private defaultConfiguration: DefaultConfigurationModel; - private userConfiguration: UserConfiguration; + private localUserConfiguration: LocalUserConfiguration; + private remoteUserConfiguration: RemoteUserConfiguration | null = null; private workspaceConfiguration: WorkspaceConfiguration; private cachedFolderConfigs: ResourceMap; @@ -67,14 +69,18 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private configurationEditingService: ConfigurationEditingService; private jsonEditingService: JSONEditingService; - constructor(private environmentService: IEnvironmentService, private workspaceSettingsRootFolder: string = FOLDER_CONFIG_FOLDER_NAME) { + constructor(configuration: IWindowConfiguration, private environmentService: IEnvironmentService, private remoteAgentService: IRemoteAgentService, private workspaceSettingsRootFolder: string = FOLDER_CONFIG_FOLDER_NAME) { super(); this.completeWorkspaceBarrier = new Barrier(); this.defaultConfiguration = new DefaultConfigurationModel(); - this.userConfiguration = this._register(new UserConfiguration(environmentService.appSettingsPath)); + 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._register(this.remoteUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onRemoteUserConfigurationChanged(userConfiguration))); + } this.workspaceConfiguration = this._register(new WorkspaceConfiguration(environmentService)); - this._register(this.userConfiguration.onDidChangeConfiguration(userConfiguration => this.onUserConfigurationChanged(userConfiguration))); this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged())); this._register(Registry.as(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas())); @@ -254,8 +260,8 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return this.reloadWorkspaceFolderConfiguration(folder, key); } return this.reloadUserConfiguration() - .then(userConfigurationModel => this.reloadWorkspaceConfiguration() - .then(() => this.loadConfiguration(userConfigurationModel))); + .then(({ local, remote }) => this.reloadWorkspaceConfiguration() + .then(() => this.loadConfiguration(local, remote))); } inspect(key: string, overrides?: IConfigurationOverrides): { @@ -289,6 +295,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic acquireFileService(fileService: IFileService): void { this.fileService = fileService; const changedWorkspaceFolders: IWorkspaceFolder[] = []; + this.localUserConfiguration.adopt(fileService); Promise.all([this.workspaceConfiguration.adopt(fileService), ...this.cachedFolderConfigs.values() .map(folderConfiguration => folderConfiguration.adopt(fileService) .then(result => { @@ -306,6 +313,15 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } this.releaseWorkspaceBarrier(); }); + if (this.remoteUserConfiguration) { + this.remoteAgentService.getEnvironment() + .then(environment => this.remoteUserConfiguration!.adopt(environment ? environment.appSettingsPath : null, fileService) + .then(changedModel => { + if (changedModel) { + this.onRemoteUserConfigurationChanged(changedModel); + } + })); + } } acquireInstantiationService(instantiationService: IInstantiationService): void { @@ -425,12 +441,18 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private initializeConfiguration(): Promise { this.registerConfigurationSchemas(); - return this.userConfiguration.initialize() - .then(userConfigurationModel => this.loadConfiguration(userConfigurationModel)); + return this.initializeUserConfiguration() + .then(({ local, remote }) => this.loadConfiguration(local, remote)); } - private reloadUserConfiguration(key?: string): Promise { - return this.userConfiguration.reload(); + private initializeUserConfiguration(): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> { + return Promise.all([this.localUserConfiguration.initialize(), 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())]) + .then(([local, remote]) => ({ local, remote })); } private reloadWorkspaceConfiguration(key?: string): Promise { @@ -448,7 +470,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return this.onWorkspaceFolderConfigurationChanged(folder, key); } - private loadConfiguration(userConfigurationModel: ConfigurationModel): Promise { + private loadConfiguration(userConfigurationModel: ConfigurationModel, remoteUserConfigurationModel: ConfigurationModel): Promise { // reset caches this.cachedFolderConfigs = new ResourceMap(); @@ -461,7 +483,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic folderConfigurations.forEach((folderConfiguration, index) => folderConfigurationModels.set(folders[index].uri, folderConfiguration)); const currentConfiguration = this._configuration; - this._configuration = new Configuration(this.defaultConfiguration, userConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap(), this.workspace); + this._configuration = new Configuration(this.defaultConfiguration, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap(), this.workspace); if (currentConfiguration) { const changedKeys = this._configuration.compare(currentConfiguration); @@ -528,8 +550,13 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } } - private onUserConfigurationChanged(userConfiguration: ConfigurationModel): void { - const keys = this._configuration.compareAndUpdateUserConfiguration(userConfiguration); + private onLocalUserConfigurationChanged(userConfiguration: ConfigurationModel): void { + const keys = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration); + this.triggerConfigurationChange(keys, ConfigurationTarget.USER); + } + + private onRemoteUserConfigurationChanged(userConfiguration: ConfigurationModel): void { + const keys = this._configuration.compareAndUpdateRemoteUserConfiguration(userConfiguration); this.triggerConfigurationChange(keys, ConfigurationTarget.USER); } @@ -618,7 +645,13 @@ export class WorkspaceService extends Disposable implements IConfigurationServic .then(() => { switch (target) { case ConfigurationTarget.USER: - return this.reloadUserConfiguration().then(_ => Promise.resolve()); + return this.reloadUserConfiguration() + .then(({ local, remote }) => { + this.onLocalUserConfigurationChanged(local); + if (this.remoteUserConfiguration) { + this.onRemoteUserConfigurationChanged(remote); + } + }); case ConfigurationTarget.WORKSPACE: return this.reloadWorkspaceConfiguration(); case ConfigurationTarget.WORKSPACE_FOLDER: @@ -670,9 +703,9 @@ export class WorkspaceService extends Disposable implements IConfigurationServic case ConfigurationTarget.DEFAULT: return this._configuration.defaults.contents; case ConfigurationTarget.USER: - return this._configuration.user.contents; + return this._configuration.userConfiguration.contents; case ConfigurationTarget.WORKSPACE: - return this._configuration.workspace.contents; + return this._configuration.workspaceConfiguration.contents; } return {}; } diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 55c92bf338..c567d0d1e7 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -201,7 +201,7 @@ suite('AllKeysConfigurationChangeEvent', () => { test('changeEvent affects keys for any resource', () => { const configuraiton = new Configuration(new ConfigurationModel({}, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']), - new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), null!); + new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), null!); let testObject = new AllKeysConfigurationChangeEvent(configuraiton, ConfigurationTarget.USER, null); assert.deepEqual(testObject.affectedKeys, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']); 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 740d2dcb18..b7ea165c07 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 @@ -36,6 +36,9 @@ 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'; class SettingsTestEnvironmentService extends EnvironmentService { @@ -100,7 +103,9 @@ suite('ConfigurationEditingService', () => { instantiationService = workbenchInstantiationService(); const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); instantiationService.stub(IEnvironmentService, environmentService); - const workspaceService = new WorkspaceService(environmentService); + const remoteAgentService = instantiationService.createInstance(RemoteAgentService, {}); + instantiationService.stub(IRemoteAgentService, remoteAgentService); + const workspaceService = new WorkspaceService({}, environmentService, 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); 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 664c297db8..7bceb37165 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 @@ -37,6 +37,10 @@ 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'; class SettingsTestEnvironmentService extends EnvironmentService { diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index 671201b8ce..dd960abcd2 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -47,6 +47,7 @@ export class RemoteFileDialog { private userValue: string; private scheme: string = REMOTE_HOST_SCHEME; private shouldOverwriteFile: boolean = false; + private autoComplete: string; constructor( @IFileService private readonly fileService: IFileService, @@ -222,18 +223,21 @@ export class RemoteFileDialog { }); this.filePickBox.onDidChangeValue(async value => { - if (value !== this.userValue) { - this.filePickBox.validationMessage = undefined; - this.shouldOverwriteFile = false; - const trimmedPickBoxValue = ((this.filePickBox.value.length > 1) && this.endsWithSlash(this.filePickBox.value)) ? this.filePickBox.value.substr(0, this.filePickBox.value.length - 1) : this.filePickBox.value; - const valueUri = this.remoteUriFrom(trimmedPickBoxValue); - if (!resources.isEqual(this.currentFolder, valueUri, true)) { - await this.tryUpdateItems(value, this.remoteUriFrom(this.filePickBox.value)); + // onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything + if (!this.autoComplete || (value !== this.autoComplete)) { + if (value !== this.userValue) { + this.filePickBox.validationMessage = undefined; + this.shouldOverwriteFile = false; + const trimmedPickBoxValue = ((this.filePickBox.value.length > 1) && this.endsWithSlash(this.filePickBox.value)) ? this.filePickBox.value.substr(0, this.filePickBox.value.length - 1) : this.filePickBox.value; + const valueUri = this.remoteUriFrom(trimmedPickBoxValue); + if (!resources.isEqual(this.currentFolder, valueUri, true)) { + await this.tryUpdateItems(value, this.remoteUriFrom(this.filePickBox.value)); + } + this.setActiveItems(value); + this.userValue = value; + } else { + this.filePickBox.activeItems = []; } - this.setActiveItems(value); - this.userValue = value; - } else { - this.filePickBox.activeItems = []; } }); this.filePickBox.onDidHide(() => { @@ -356,8 +360,12 @@ export class RemoteFileDialog { const itemBasename = (item.label === '..') ? item.label : resources.basename(item.uri); if ((itemBasename.length >= inputBasename.length) && (itemBasename.substr(0, inputBasename.length).toLowerCase() === inputBasename.toLowerCase())) { this.filePickBox.activeItems = [item]; - this.filePickBox.value = this.filePickBox.value + itemBasename.substr(inputBasename.length); - this.filePickBox.valueSelection = [value.length, this.filePickBox.value.length]; + const insertValue = itemBasename.substr(inputBasename.length); + this.autoComplete = value + insertValue; + if (this.filePickBox.inputHasFocus()) { + document.execCommand('insertText', false, insertValue); + this.filePickBox.valueSelection = [value.length, this.filePickBox.value.length]; + } hasMatch = true; break; } @@ -520,7 +528,7 @@ export class RemoteFileDialog { } if (this.fallbackListItem) { - sorted.unshift(this.fallbackListItem); + sorted.push(this.fallbackListItem); } return sorted; } diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index 2fd84d2d5d..9dbcda3781 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -10,10 +10,14 @@ import { isLinux, isWindows } from 'vs/base/common/platform'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions } from 'vs/platform/dialogs/common/dialogs'; +import { DialogService as HTMLDialogService } from 'vs/platform/dialogs/browser/dialogService'; import { ILogService } from 'vs/platform/log/common/log'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { DialogChannel } from 'vs/platform/dialogs/node/dialogIpc'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; interface IMassagedMessageBoxOptions { @@ -31,6 +35,38 @@ interface IMassagedMessageBoxOptions { } export class DialogService implements IDialogService { + _serviceBrand: any; + + private impl: IDialogService; + + constructor( + @IConfigurationService configurationService: IConfigurationService, + @ILogService logService: ILogService, + @ILayoutService layoutService: ILayoutService, + @IThemeService themeService: IThemeService, + @IWindowService windowService: IWindowService, + @ISharedProcessService sharedProcessService: ISharedProcessService + ) { + + // Use HTML based dialogs + if (configurationService.getValue('workbench.dialogs.customEnabled') === true) { + this.impl = new HTMLDialogService(logService, layoutService, themeService); + } + // Electron dialog service + else { + this.impl = new NativeDialogService(windowService, logService, sharedProcessService); + } + } + + confirm(confirmation: IConfirmation): Promise { + return this.impl.confirm(confirmation); + } + show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions | undefined): Promise { + return this.impl.show(severity, message, buttons, options); + } +} + +class NativeDialogService implements IDialogService { _serviceBrand: any; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index 66c2507679..855fb011e8 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -18,8 +18,9 @@ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IRemoteConsoleLog, log, parse } from 'vs/base/common/console'; import { findFreePort, randomPort } from 'vs/base/node/ports'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; -import { PersistentProtocol, generateRandomPipeName } from 'vs/base/parts/ipc/node/ipc.net'; +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'; @@ -36,6 +37,7 @@ import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/ 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'; export interface IExtensionHostStarter { readonly onCrashed: Event<[number, string | null]>; @@ -344,7 +346,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { // using a buffered message protocol here because between now // and the first time a `then` executes some messages might be lost // unless we immediately register a listener for `onMessage`. - resolve(new PersistentProtocol(this._extensionHostConnection)); + resolve(new PersistentProtocol(new NodeSocket(this._extensionHostConnection))); }); }).then((protocol) => { @@ -377,7 +379,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { // Wait 60s for the initialized message installTimeoutCheck(); - protocol.send(Buffer.from(JSON.stringify(data))); + protocol.send(VSBuffer.fromString(JSON.stringify(data))); }); return; } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts index 0b48056bed..6e2b318141 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts @@ -7,7 +7,7 @@ import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostCustomersRegistry } from 'vs/workbench/api/common/extHostCustomers'; @@ -320,7 +320,7 @@ class RPCLogger implements IRPCProtocolLogger { private _totalIncoming = 0; private _totalOutgoing = 0; - private _log(direction: string, totalLength, msgLength: number, req: number, initiator: RequestInitiator, str: string, data: any): void { + private _log(direction: string, totalLength: number, msgLength: number, req: number, initiator: RequestInitiator, str: string, data: any): void { data = pretty(data); const colorTable = colorTables[initiator]; diff --git a/src/vs/workbench/services/extensions/node/extensionHostMain.ts b/src/vs/workbench/services/extensions/node/extensionHostMain.ts index 05ab0cdf4e..c50c2dbda3 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostMain.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostMain.ts @@ -9,7 +9,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Counter } from 'vs/base/common/numbers'; import { URI, setUriThrowOnMissingScheme } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { IEnvironment, IInitData, MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcess.ts b/src/vs/workbench/services/extensions/node/extensionHostProcess.ts index ab172ac8bc..44c33f5d00 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcess.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcess.ts @@ -7,12 +7,14 @@ import * as nativeWatchdog from 'native-watchdog'; import * as net from 'net'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; -import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/node/ipc.net'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import product from 'vs/platform/product/node/product'; import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage } from 'vs/workbench/services/extensions/node/extensionHostProtocol'; import { exit, ExtensionHostMain } from 'vs/workbench/services/extensions/node/extensionHostMain'; +import { VSBuffer } from 'vs/base/common/buffer'; // With Electron 2.x and node.js 8.x the "natives" module // can cause a native crash (see https://github.com/nodejs/node/issues/19891 and @@ -58,18 +60,18 @@ function _createExtHostProtocol(): Promise { process.on('message', (msg: IExtHostSocketMessage, handle: net.Socket) => { if (msg && msg.type === 'VSCODE_EXTHOST_IPC_SOCKET') { - const initialDataChunk = Buffer.from(msg.initialDataChunk, 'base64'); + const initialDataChunk = VSBuffer.wrap(Buffer.from(msg.initialDataChunk, 'base64')); if (protocol) { // reconnection case if (disconnectWaitTimer) { clearTimeout(disconnectWaitTimer); disconnectWaitTimer = null; } - protocol.beginAcceptReconnection(handle, initialDataChunk); + protocol.beginAcceptReconnection(new NodeSocket(handle), initialDataChunk); protocol.endAcceptReconnection(); } else { clearTimeout(timer); - protocol = new PersistentProtocol(handle, initialDataChunk); + protocol = new PersistentProtocol(new NodeSocket(handle), initialDataChunk); protocol.onClose(() => onTerminate()); resolve(protocol); @@ -99,7 +101,7 @@ function _createExtHostProtocol(): Promise { const socket = net.createConnection(pipeName, () => { socket.removeListener('error', reject); - resolve(new PersistentProtocol(socket)); + resolve(new PersistentProtocol(new NodeSocket(socket))); }); socket.once('error', reject); diff --git a/src/vs/workbench/services/extensions/node/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/node/extensionHostProtocol.ts index 6eb3180398..c00b766251 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProtocol.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from 'vs/base/common/buffer'; + export interface IExtHostReadyMessage { type: 'VSCODE_EXTHOST_IPC_READY'; } @@ -18,24 +20,24 @@ export const enum MessageType { Terminate } -export function createMessageOfType(type: MessageType): Buffer { - const result = Buffer.allocUnsafe(1); +export function createMessageOfType(type: MessageType): VSBuffer { + const result = VSBuffer.alloc(1); switch (type) { - case MessageType.Initialized: result.writeUInt8(1, 0); break; - case MessageType.Ready: result.writeUInt8(2, 0); break; - case MessageType.Terminate: result.writeUInt8(3, 0); break; + case MessageType.Initialized: result.writeUint8(1, 0); break; + case MessageType.Ready: result.writeUint8(2, 0); break; + case MessageType.Terminate: result.writeUint8(3, 0); break; } return result; } -export function isMessageOfType(message: Buffer, type: MessageType): boolean { - if (message.length !== 1) { +export function isMessageOfType(message: VSBuffer, type: MessageType): boolean { + if (message.byteLength !== 1) { return false; } - switch (message.readUInt8(0)) { + switch (message.readUint8(0)) { case 1: return type === MessageType.Initialized; case 2: return type === MessageType.Ready; case 3: return type === MessageType.Terminate; diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index b43ce92e61..de85cbb55f 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -457,7 +457,7 @@ async function readCaCertificates() { } function readWindowsCaCertificates() { - const winCA = require.__$__nodeRequire('win-ca-lib'); + const winCA = require.__$__nodeRequire('vscode-windows-ca-certs'); let ders: any[] = []; const store = winCA(); @@ -515,7 +515,7 @@ async function readLinuxCaCertificates() { return undefined; } -function derToPem(blob) { +function derToPem(blob: Buffer) { const lines = ['-----BEGIN CERTIFICATE-----']; const der = blob.toString('base64'); for (let i = 0; i < der.length; i += 64) { diff --git a/src/vs/workbench/services/extensions/node/rpcProtocol.ts b/src/vs/workbench/services/extensions/node/rpcProtocol.ts index 1964f3c01a..d683d104a2 100644 --- a/src/vs/workbench/services/extensions/node/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/node/rpcProtocol.ts @@ -10,9 +10,10 @@ import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IURITransformer, transformIncomingURIs } from 'vs/base/common/uriIpc'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise'; import { IRPCProtocol, ProxyIdentifier, getStringIdentifierForProxy } from 'vs/workbench/services/extensions/common/proxyIdentifier'; +import { VSBuffer } from 'vs/base/common/buffer'; export interface JSONStringifyReplacer { (key: string, value: any): any; @@ -205,12 +206,12 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { } } - private _receiveOneMessage(rawmsg: Buffer): void { + private _receiveOneMessage(rawmsg: VSBuffer): void { if (this._isDisposed) { return; } - const msgLength = rawmsg.length; + const msgLength = rawmsg.byteLength; const buff = MessageBuffer.read(rawmsg, 0); const messageType = buff.readUInt8(); const req = buff.readUInt32(); @@ -262,6 +263,11 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { this._receiveReply(msgLength, req, value); break; } + case MessageType.ReplyOKVSBuffer: { + let value = MessageIO.deserializeReplyOKVSBuffer(buff); + this._receiveReply(msgLength, req, value); + break; + } case MessageType.ReplyErrError: { let err = MessageIO.deserializeReplyErrError(buff); if (this._uriTransformer) { @@ -435,24 +441,24 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { class MessageBuffer { public static alloc(type: MessageType, req: number, messageSize: number): MessageBuffer { - let result = new MessageBuffer(Buffer.allocUnsafe(messageSize + 1 /* type */ + 4 /* req */), 0); + let result = new MessageBuffer(VSBuffer.alloc(messageSize + 1 /* type */ + 4 /* req */), 0); result.writeUInt8(type); result.writeUInt32(req); return result; } - public static read(buff: Buffer, offset: number): MessageBuffer { + public static read(buff: VSBuffer, offset: number): MessageBuffer { return new MessageBuffer(buff, offset); } - private _buff: Buffer; + private _buff: VSBuffer; private _offset: number; - public get buffer(): Buffer { + public get buffer(): VSBuffer { return this._buff; } - private constructor(buff: Buffer, offset: number) { + private constructor(buff: VSBuffer, offset: number) { this._buff = buff; this._offset = offset; } @@ -462,108 +468,136 @@ class MessageBuffer { } public writeUInt8(n: number): void { - this._buff.writeUInt8(n, this._offset, true); this._offset += 1; + this._buff.writeUint8(n, this._offset); this._offset += 1; } public readUInt8(): number { - const n = this._buff.readUInt8(this._offset, true); this._offset += 1; + const n = this._buff.readUint8(this._offset); this._offset += 1; return n; } public writeUInt32(n: number): void { - this._buff.writeUInt32BE(n, this._offset, true); this._offset += 4; + this._buff.writeUint32BE(n, this._offset); this._offset += 4; } public readUInt32(): number { - const n = this._buff.readUInt32BE(this._offset, true); this._offset += 4; + const n = this._buff.readUint32BE(this._offset); this._offset += 4; return n; } - public static sizeShortString(str: string, strByteLength: number): number { - return 1 /* string length */ + strByteLength /* actual string */; + public static sizeShortString(str: VSBuffer): number { + return 1 /* string length */ + str.byteLength /* actual string */; } - public writeShortString(str: string, strByteLength: number): void { - this._buff.writeUInt8(strByteLength, this._offset, true); this._offset += 1; - this._buff.write(str, this._offset, strByteLength, 'utf8'); this._offset += strByteLength; + public writeShortString(str: VSBuffer): void { + this._buff.writeUint8(str.byteLength, this._offset); this._offset += 1; + this._buff.set(str, this._offset); this._offset += str.byteLength; } public readShortString(): string { - const strLength = this._buff.readUInt8(this._offset, true); this._offset += 1; - const str = this._buff.toString('utf8', this._offset, this._offset + strLength); this._offset += strLength; + const strByteLength = this._buff.readUint8(this._offset); this._offset += 1; + const strBuff = this._buff.slice(this._offset, this._offset + strByteLength); + const str = strBuff.toString(); this._offset += strByteLength; return str; } - public static sizeLongString(str: string, strByteLength: number): number { - return 4 /* string length */ + strByteLength /* actual string */; + public static sizeLongString(str: VSBuffer): number { + return 4 /* string length */ + str.byteLength /* actual string */; } - public writeLongString(str: string, strByteLength: number): void { - this._buff.writeUInt32LE(strByteLength, this._offset, true); this._offset += 4; - this._buff.write(str, this._offset, strByteLength, 'utf8'); this._offset += strByteLength; + public writeLongString(str: VSBuffer): void { + this._buff.writeUint32BE(str.byteLength, this._offset); this._offset += 4; + this._buff.set(str, this._offset); this._offset += str.byteLength; } public readLongString(): string { - const strLength = this._buff.readUInt32LE(this._offset, true); this._offset += 4; - const str = this._buff.toString('utf8', this._offset, this._offset + strLength); this._offset += strLength; + const strByteLength = this._buff.readUint32BE(this._offset); this._offset += 4; + const strBuff = this._buff.slice(this._offset, this._offset + strByteLength); + const str = strBuff.toString(); this._offset += strByteLength; return str; } - public static sizeBuffer(buff: Buffer, buffByteLength: number): number { - return 4 /* buffer length */ + buffByteLength /* actual buffer */; + public static sizeBuffer(buff: VSBuffer): number { + return 4 /* buffer length */ + buff.byteLength /* actual buffer */; } - public writeBuffer(buff: Buffer, buffByteLength: number): void { - this._buff.writeUInt32LE(buffByteLength, this._offset, true); this._offset += 4; - buff.copy(this._buff, this._offset); this._offset += buffByteLength; + public writeBuffer(buff: VSBuffer): void { + this._buff.writeUint32BE(buff.byteLength, this._offset); this._offset += 4; + this._buff.set(buff, this._offset); this._offset += buff.byteLength; } public readBuffer(): Buffer { - const buffLength = this._buff.readUInt32LE(this._offset, true); this._offset += 4; + const buffLength = this._buff.readUint32BE(this._offset); this._offset += 4; + const buff = this._buff.slice(this._offset, this._offset + buffLength); this._offset += buffLength; + return buff.buffer; + } + + public static sizeVSBuffer(buff: VSBuffer): number { + return 4 /* buffer length */ + buff.byteLength /* actual buffer */; + } + + public writeVSBuffer(buff: VSBuffer): void { + this._buff.writeUint32BE(buff.byteLength, this._offset); this._offset += 4; + this._buff.set(buff, this._offset); this._offset += buff.byteLength; + } + + public readVSBuffer(): VSBuffer { + const buffLength = this._buff.readUint32BE(this._offset); this._offset += 4; const buff = this._buff.slice(this._offset, this._offset + buffLength); this._offset += buffLength; return buff; } - public static sizeMixedArray(arr: Array, arrLengths: number[]): number { + public static sizeMixedArray(arr: VSBuffer[], arrType: ArgType[]): number { let size = 0; size += 1; // arr length for (let i = 0, len = arr.length; i < len; i++) { const el = arr[i]; - const elLength = arrLengths[i]; + const elType = arrType[i]; size += 1; // arg type - if (typeof el === 'string') { - size += this.sizeLongString(el, elLength); + if (elType === ArgType.String) { + size += this.sizeLongString(el); + } else if (elType === ArgType.Buffer) { + size += this.sizeBuffer(el); } else { - size += this.sizeBuffer(el, elLength); + size += this.sizeVSBuffer(el); } } return size; } - public writeMixedArray(arr: Array, arrLengths: number[]): void { - this._buff.writeUInt8(arr.length, this._offset, true); this._offset += 1; + public writeMixedArray(arr: VSBuffer[], arrType: ArgType[]): void { + this._buff.writeUint8(arr.length, this._offset); this._offset += 1; for (let i = 0, len = arr.length; i < len; i++) { const el = arr[i]; - const elLength = arrLengths[i]; - if (typeof el === 'string') { + const elType = arrType[i]; + if (elType === ArgType.String) { this.writeUInt8(ArgType.String); - this.writeLongString(el, elLength); - } else { + this.writeLongString(el); + } else if (elType === ArgType.Buffer) { this.writeUInt8(ArgType.Buffer); - this.writeBuffer(el, elLength); + this.writeVSBuffer(el); + } else { + this.writeUInt8(ArgType.VSBuffer); + this.writeVSBuffer(el); } } } - public readMixedArray(): Array { - const arrLen = this._buff.readUInt8(this._offset, true); this._offset += 1; - let arr: Array = new Array(arrLen); + public readMixedArray(): Array { + const arrLen = this._buff.readUint8(this._offset); this._offset += 1; + let arr: Array = new Array(arrLen); for (let i = 0; i < arrLen; i++) { const argType = this.readUInt8(); - if (argType === ArgType.String) { - arr[i] = this.readLongString(); - } else { - arr[i] = this.readBuffer(); + switch (argType) { + case ArgType.String: + arr[i] = this.readLongString(); + break; + case ArgType.Buffer: + arr[i] = this.readBuffer(); + break; + case ArgType.VSBuffer: + arr[i] = this.readVSBuffer(); + break; } } return arr; @@ -577,42 +611,48 @@ class MessageIO { if (Buffer.isBuffer(arr[i])) { return true; } + if (arr[i] instanceof VSBuffer) { + return true; + } } return false; } - public static serializeRequest(req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean, replacer: JSONStringifyReplacer | null): Buffer { + public static serializeRequest(req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean, replacer: JSONStringifyReplacer | null): VSBuffer { if (this._arrayContainsBuffer(args)) { - let massagedArgs: Array = new Array(args.length); - let argsLengths: number[] = new Array(args.length); + let massagedArgs: VSBuffer[] = []; + let massagedArgsType: ArgType[] = []; for (let i = 0, len = args.length; i < len; i++) { const arg = args[i]; if (Buffer.isBuffer(arg)) { + massagedArgs[i] = VSBuffer.wrap(arg); + massagedArgsType[i] = ArgType.Buffer; + } else if (arg instanceof VSBuffer) { massagedArgs[i] = arg; - argsLengths[i] = arg.byteLength; + massagedArgsType[i] = ArgType.VSBuffer; } else { - massagedArgs[i] = safeStringify(arg, replacer); - argsLengths[i] = Buffer.byteLength(massagedArgs[i], 'utf8'); + massagedArgs[i] = VSBuffer.fromString(safeStringify(arg, replacer)); + massagedArgsType[i] = ArgType.String; } } - return this._requestMixedArgs(req, rpcId, method, massagedArgs, argsLengths, usesCancellationToken); + return this._requestMixedArgs(req, rpcId, method, massagedArgs, massagedArgsType, usesCancellationToken); } return this._requestJSONArgs(req, rpcId, method, safeStringify(args, replacer), usesCancellationToken); } - private static _requestJSONArgs(req: number, rpcId: number, method: string, args: string, usesCancellationToken: boolean): Buffer { - const methodByteLength = Buffer.byteLength(method, 'utf8'); - const argsByteLength = Buffer.byteLength(args, 'utf8'); + private static _requestJSONArgs(req: number, rpcId: number, method: string, args: string, usesCancellationToken: boolean): VSBuffer { + const methodBuff = VSBuffer.fromString(method); + const argsBuff = VSBuffer.fromString(args); let len = 0; len += MessageBuffer.sizeUInt8(); - len += MessageBuffer.sizeShortString(method, methodByteLength); - len += MessageBuffer.sizeLongString(args, argsByteLength); + len += MessageBuffer.sizeShortString(methodBuff); + len += MessageBuffer.sizeLongString(argsBuff); let result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestJSONArgsWithCancellation : MessageType.RequestJSONArgs, req, len); result.writeUInt8(rpcId); - result.writeShortString(method, methodByteLength); - result.writeLongString(args, argsByteLength); + result.writeShortString(methodBuff); + result.writeLongString(argsBuff); return result.buffer; } @@ -627,18 +667,18 @@ class MessageIO { }; } - private static _requestMixedArgs(req: number, rpcId: number, method: string, args: Array, argsLengths: number[], usesCancellationToken: boolean): Buffer { - const methodByteLength = Buffer.byteLength(method, 'utf8'); + private static _requestMixedArgs(req: number, rpcId: number, method: string, args: VSBuffer[], argsType: ArgType[], usesCancellationToken: boolean): VSBuffer { + const methodBuff = VSBuffer.fromString(method); let len = 0; len += MessageBuffer.sizeUInt8(); - len += MessageBuffer.sizeShortString(method, methodByteLength); - len += MessageBuffer.sizeMixedArray(args, argsLengths); + len += MessageBuffer.sizeShortString(methodBuff); + len += MessageBuffer.sizeMixedArray(args, argsType); let result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestMixedArgsWithCancellation : MessageType.RequestMixedArgs, req, len); result.writeUInt8(rpcId); - result.writeShortString(method, methodByteLength); - result.writeMixedArray(args, argsLengths); + result.writeShortString(methodBuff); + result.writeMixedArray(args, argsType); return result.buffer; } @@ -662,36 +702,49 @@ class MessageIO { }; } - public static serializeAcknowledged(req: number): Buffer { + public static serializeAcknowledged(req: number): VSBuffer { return MessageBuffer.alloc(MessageType.Acknowledged, req, 0).buffer; } - public static serializeCancel(req: number): Buffer { + public static serializeCancel(req: number): VSBuffer { return MessageBuffer.alloc(MessageType.Cancel, req, 0).buffer; } - public static serializeReplyOK(req: number, res: any, replacer: JSONStringifyReplacer | null): Buffer { + public static serializeReplyOK(req: number, res: any, replacer: JSONStringifyReplacer | null): VSBuffer { if (typeof res === 'undefined') { return this._serializeReplyOKEmpty(req); } if (Buffer.isBuffer(res)) { return this._serializeReplyOKBuffer(req, res); } + if (res instanceof VSBuffer) { + return this._serializeReplyOKVSBuffer(req, res); + } return this._serializeReplyOKJSON(req, safeStringify(res, replacer)); } - private static _serializeReplyOKEmpty(req: number): Buffer { + private static _serializeReplyOKEmpty(req: number): VSBuffer { return MessageBuffer.alloc(MessageType.ReplyOKEmpty, req, 0).buffer; } - private static _serializeReplyOKBuffer(req: number, res: Buffer): Buffer { - const resByteLength = res.byteLength; + private static _serializeReplyOKBuffer(req: number, res: Buffer): VSBuffer { + const buff = VSBuffer.wrap(res); let len = 0; - len += MessageBuffer.sizeBuffer(res, resByteLength); + len += MessageBuffer.sizeBuffer(buff); let result = MessageBuffer.alloc(MessageType.ReplyOKBuffer, req, len); - result.writeBuffer(res, resByteLength); + result.writeBuffer(buff); + return result.buffer; + } + + private static _serializeReplyOKVSBuffer(req: number, res: VSBuffer): VSBuffer { + + let len = 0; + len += MessageBuffer.sizeVSBuffer(res); + + let result = MessageBuffer.alloc(MessageType.ReplyOKVSBuffer, req, len); + result.writeVSBuffer(res); return result.buffer; } @@ -699,14 +752,18 @@ class MessageIO { return buff.readBuffer(); } - private static _serializeReplyOKJSON(req: number, res: string): Buffer { - const resByteLength = Buffer.byteLength(res, 'utf8'); + public static deserializeReplyOKVSBuffer(buff: MessageBuffer): VSBuffer { + return buff.readVSBuffer(); + } + + private static _serializeReplyOKJSON(req: number, res: string): VSBuffer { + const resBuff = VSBuffer.fromString(res); let len = 0; - len += MessageBuffer.sizeLongString(res, resByteLength); + len += MessageBuffer.sizeLongString(resBuff); let result = MessageBuffer.alloc(MessageType.ReplyOKJSON, req, len); - result.writeLongString(res, resByteLength); + result.writeLongString(resBuff); return result.buffer; } @@ -715,22 +772,21 @@ class MessageIO { return JSON.parse(res); } - public static serializeReplyErr(req: number, err: any): Buffer { + public static serializeReplyErr(req: number, err: any): VSBuffer { if (err instanceof Error) { return this._serializeReplyErrEror(req, err); } return this._serializeReplyErrEmpty(req); } - private static _serializeReplyErrEror(req: number, _err: Error): Buffer { - const err = safeStringify(errors.transformErrorForSerialization(_err), null); - const errByteLength = Buffer.byteLength(err, 'utf8'); + private static _serializeReplyErrEror(req: number, _err: Error): VSBuffer { + const errBuff = VSBuffer.fromString(safeStringify(errors.transformErrorForSerialization(_err), null)); let len = 0; - len += MessageBuffer.sizeLongString(err, errByteLength); + len += MessageBuffer.sizeLongString(errBuff); let result = MessageBuffer.alloc(MessageType.ReplyErrError, req, len); - result.writeLongString(err, errByteLength); + result.writeLongString(errBuff); return result.buffer; } @@ -739,7 +795,7 @@ class MessageIO { return JSON.parse(err); } - private static _serializeReplyErrEmpty(req: number): Buffer { + private static _serializeReplyErrEmpty(req: number): VSBuffer { return MessageBuffer.alloc(MessageType.ReplyErrEmpty, req, 0).buffer; } } @@ -753,6 +809,7 @@ const enum MessageType { Cancel = 6, ReplyOKEmpty = 7, ReplyOKBuffer = 8, + ReplyOKVSBuffer = 8, ReplyOKJSON = 9, ReplyErrError = 10, ReplyErrEmpty = 11, @@ -760,5 +817,6 @@ const enum MessageType { const enum ArgType { String = 1, - Buffer = 2 + Buffer = 2, + VSBuffer = 3 } diff --git a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts index 757d77e97e..201f641202 100644 --- a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts @@ -6,23 +6,24 @@ import * as assert from 'assert'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol'; +import { VSBuffer } from 'vs/base/common/buffer'; suite('RPCProtocol', () => { class MessagePassingProtocol implements IMessagePassingProtocol { private _pair: MessagePassingProtocol; - private readonly _onMessage = new Emitter(); - public readonly onMessage: Event = this._onMessage.event; + private readonly _onMessage = new Emitter(); + public readonly onMessage: Event = this._onMessage.event; public setPair(other: MessagePassingProtocol) { this._pair = other; } - public send(buffer: Buffer): void { + public send(buffer: VSBuffer): void { process.nextTick(() => { this._pair._onMessage.fire(buffer); }); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 8b52df3c78..13f13ab202 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -8,7 +8,7 @@ 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 } from 'vs/platform/files/common/files'; +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 { 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'; @@ -207,7 +207,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile } registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { - throw new Error('not implemented'); + return Disposable.None; } activateProvider(scheme: string): Promise { @@ -218,6 +218,10 @@ export class FileService extends Disposable implements ILegacyFileService, IFile return resource.scheme === Schemas.file; } + hasCapability(resource: uri, capability: FileSystemProviderCapabilities): Promise { + return Promise.resolve(false); + } + resolveContent(resource: uri, options?: IResolveContentOptions): Promise { return this.resolveStreamContent(resource, options).then(streamContent => { return new Promise((resolve, reject) => { diff --git a/src/vs/workbench/services/files/node/remoteFileService.ts b/src/vs/workbench/services/files/node/remoteFileService.ts index 142dbc11ce..70abcfe9eb 100644 --- a/src/vs/workbench/services/files/node/remoteFileService.ts +++ b/src/vs/workbench/services/files/node/remoteFileService.ts @@ -290,7 +290,7 @@ export class RemoteFileService extends FileService { options ); } - if (fileStat.etag === options.etag) { + if (typeof options.etag === 'string' && fileStat.etag === options.etag) { throw new FileOperationError( localize('fileNotModifiedError', "File not modified since"), FileOperationResult.FILE_NOT_MODIFIED_SINCE, @@ -407,7 +407,7 @@ export class RemoteFileService extends FileService { return new Promise((resolve, reject) => { readable.pipe(encoder).pipe(target); target.once('error', err => reject(err)); - target.once('finish', _ => resolve(undefined)); + target.once('finish', (_: unknown) => resolve(undefined)); }).then(_ => { return this.resolveFile(resource, { resolveMetadata: true }) as Promise; }); @@ -431,77 +431,6 @@ export class RemoteFileService extends FileService { }); } - // --- delete - - del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise { - if (resource.scheme === Schemas.file) { - return super.del(resource, options); - } else { - return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { - return provider.delete(resource, { recursive: !!(options && options.recursive) }).then(() => { - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); - }); - }); - } - } - - copyFile(source: URI, target: URI, overwrite?: boolean): Promise { - if (source.scheme === target.scheme && source.scheme === Schemas.file) { - return super.copyFile(source, target, overwrite); - } - - return this._withProvider(target).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => { - - if (source.scheme === target.scheme && (provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy)) { - // good: provider supports copy withing scheme - return provider.copy!(source, target, { overwrite: !!overwrite }).then(() => { - return this.resolveFile(target, { resolveMetadata: true }); - }).then(fileStat => { - this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat)); - return fileStat; - }, err => { - const result = toFileOperationResult(err); - if (result === FileOperationResult.FILE_MOVE_CONFLICT) { - throw new FileOperationError(localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), result); - } - throw err; - }); - } - - const prepare = overwrite - ? Promise.resolve(this.del(target, { recursive: true }).catch(_err => { /*ignore*/ })) - : Promise.resolve(); - - // todo@ben, can only copy text files - // https://github.com/Microsoft/vscode/issues/41543 - return prepare.then(() => { - return this.resolveContent(source, { acceptTextOnly: true }).then(content => { - return this._withProvider(target).then(provider => { - return this._writeFile( - provider, target, - new StringSnapshot(content.value), - content.encoding, - { create: true, overwrite: !!overwrite } - ).then(fileStat => { - this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat)); - return fileStat; - }); - }, err => { - const result = toFileOperationResult(err); - if (result === FileOperationResult.FILE_MOVE_CONFLICT) { - throw new FileOperationError(localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), result); - } else if (err instanceof Error && err.name === 'ENOPRO') { - // file scheme - return super.updateContent(target, content.value, { encoding: content.encoding }); - } else { - return Promise.reject(err); - } - }); - }); - }); - }); - } - private _activeWatches = new Map, count: number }>(); watchFileChanges(resource: URI, opts: IWatchOptions = { recursive: false, excludes: [] }): void { diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts index ef7356a112..7caadc367b 100644 --- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getNextTickChannel } from 'vs/base/parts/ipc/node/ipc'; +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'; diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts index 7a1d327f65..c3cc209ae1 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getNextTickChannel } from 'vs/base/parts/ipc/node/ipc'; +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'; diff --git a/src/vs/workbench/services/files2/common/fileService2.ts b/src/vs/workbench/services/files2/common/fileService2.ts index 7c2fc7697d..a5a753cd1e 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ 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 } from 'vs/platform/files/common/files'; +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 } 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'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { isAbsolutePath, dirname, basename, joinPath, isEqual, isEqualOrParent } from 'vs/base/common/resources'; import { localize } from 'vs/nls'; import { TernarySearchTree } from 'vs/base/common/map'; @@ -20,21 +19,34 @@ export class FileService2 extends Disposable implements IFileService { //#region TODO@Ben HACKS - private _impl: IFileService; + private _legacy: IFileService | null; - setImpl(service: IFileService): void { - this._impl = this._register(service); + setLegacyService(legacy: IFileService): void { + this._legacy = this._register(legacy); - this._register(service.onFileChanges(e => this._onFileChanges.fire(e))); - this._register(service.onAfterOperation(e => this._onAfterOperation.fire(e))); + this._register(legacy.onFileChanges(e => this._onFileChanges.fire(e))); + this._register(legacy.onAfterOperation(e => this._onAfterOperation.fire(e))); + + this.provider.forEach((provider, scheme) => { + legacy.registerProvider(scheme, provider); + }); + + this.joinOnImplResolve(legacy); } //#endregion _serviceBrand: ServiceIdentifier; + private joinOnLegacy: Promise; + private joinOnImplResolve: (service: IFileService) => void; + constructor(@ILogService private logService: ILogService) { super(); + + this.joinOnLegacy = new Promise(resolve => { + this.joinOnImplResolve = resolve; + }); } //#region File System Provider @@ -53,8 +65,8 @@ export class FileService2 extends Disposable implements IFileService { } let legacyDisposal: IDisposable; - if (this._impl) { - legacyDisposal = this._impl.registerProvider(scheme, provider); + if (this._legacy) { + legacyDisposal = this._legacy.registerProvider(scheme, provider); } else { legacyDisposal = Disposable.None; } @@ -104,6 +116,12 @@ 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); + + return !!(provider.capabilities & capability); + } + private async withProvider(resource: URI): Promise { // Assert path is absolute @@ -119,9 +137,9 @@ export class FileService2 extends Disposable implements IFileService { if (!provider) { const err = new Error(); err.name = 'ENOPRO'; - err.message = `no provider for ${resource.toString()}`; + err.message = `No provider found for ${resource.toString()}`; - return Promise.reject(err); + throw err; } return provider; @@ -211,7 +229,7 @@ export class FileService2 extends Disposable implements IFileService { const childResource = joinPath(resource, name); const childStat = resolveMetadata ? await provider.stat(childResource) : { type }; - return this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse); + return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse); } catch (error) { this.logService.trace(error); @@ -236,43 +254,20 @@ export class FileService2 extends Disposable implements IFileService { async resolveFiles(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise; async resolveFiles(toResolve: { resource: URI, options: IResolveMetadataFileOptions }[]): Promise; async resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise { + return Promise.all(toResolve.map(async entry => { + try { + return { stat: await this.doResolveFile(entry.resource, entry.options), success: true }; + } catch (error) { + this.logService.trace(error); - // soft-groupBy, keep order, don't rearrange/merge groups - const groups: Array = []; - let group: typeof toResolve | undefined; - for (const request of toResolve) { - if (!group || group[0].resource.scheme !== request.resource.scheme) { - group = []; - groups.push(group); + return { stat: undefined, success: false }; } - - group.push(request); - } - - // resolve files (in parallel) - const result: Promise[] = []; - for (const group of groups) { - for (const groupEntry of group) { - result.push((async () => { - try { - return { stat: await this.doResolveFile(groupEntry.resource, groupEntry.options), success: true }; - } catch (error) { - this.logService.trace(error); - - return { stat: undefined, success: false }; - } - })()); - } - } - - return Promise.all(result); + })); } async existsFile(resource: URI): Promise { try { - await this.resolveFile(resource); - - return true; + return !!(await this.resolveFile(resource)); } catch (error) { return false; } @@ -282,43 +277,85 @@ export class FileService2 extends Disposable implements IFileService { //#region File Reading/Writing - get encoding(): IResourceEncodings { return this._impl.encoding; } + get encoding(): IResourceEncodings { + if (!this._legacy) { + throw new Error('Legacy file service not ready yet'); + } - createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise { - return this._impl.createFile(resource, content, options); + return this._legacy.encoding; + } + + async createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise { + const useLegacy = true; // can only disable this when encoding is sorted out + if (useLegacy) { + return this.joinOnLegacy.then(legacy => legacy.createFile(resource, content, options)); + } + + const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); + + // validate overwrite + const overwrite = !!(options && options.overwrite); + if (await this.existsFile(resource)) { + if (!overwrite) { + throw new FileOperationError(localize('fileExists', "File to create already exists ({0})", resource.toString(true)), FileOperationResult.FILE_MODIFIED_SINCE, options); + } + + // delete otherwise + await this.del(resource, { recursive: true }); + } + + try { + + // mkdir recursively + await this.mkdirp(provider, dirname(resource)); + + // create file: buffered + if (hasOpenReadWriteCloseCapability(provider)) { + await this.doWriteBuffered(provider, resource, new TextEncoder().encode(content)); + } + + // create file: unbuffered + else if (hasReadWriteCapability(provider)) { + await this.doWriteUnbuffered(provider, resource, new TextEncoder().encode(content), overwrite); + } + + // give up if provider has insufficient capabilities + else { + return Promise.reject('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed to support creating a file.'); + } + } catch (error) { + throw new FileOperationError(localize('err.create', "Failed to create file {0}", resource.toString(false)), toFileOperationResult(error), options); + } + + // events + const fileStat = await this.resolveFile(resource, { resolveMetadata: true }); + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); + + return fileStat; } resolveContent(resource: URI, options?: IResolveContentOptions): Promise { - return this._impl.resolveContent(resource, options); + return this.joinOnLegacy.then(legacy => legacy.resolveContent(resource, options)); } resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise { - return this._impl.resolveStreamContent(resource, options); + return this.joinOnLegacy.then(legacy => legacy.resolveStreamContent(resource, options)); } updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise { - return this._impl.updateContent(resource, value, options); + return this.joinOnLegacy.then(legacy => legacy.updateContent(resource, value, options)); } //#endregion //#region Move/Copy/Delete/Create Folder - moveFile(source: URI, target: URI, overwrite?: boolean): Promise { - if (source.scheme === target.scheme) { - return this.doMoveCopyWithSameProvider(source, target, false /* just move */, overwrite); - } + async moveFile(source: URI, target: URI, overwrite?: boolean): Promise { + const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withProvider(source)); + const targetProvider = this.throwIfFileSystemIsReadonly(await this.withProvider(target)); - return this.doMoveWithDifferentProvider(source, target); - } - - private async doMoveWithDifferentProvider(source: URI, target: URI, overwrite?: boolean): Promise { - - // copy file source => target - await this.copyFile(source, target, overwrite); - - // delete source - await this.del(source, { recursive: true }); + // move + await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', overwrite); // resolve and send events const fileStat = await this.resolveFile(target, { resolveMetadata: true }); @@ -328,68 +365,146 @@ export class FileService2 extends Disposable implements IFileService { } async copyFile(source: URI, target: URI, overwrite?: boolean): Promise { - if (source.scheme === target.scheme) { - return this.doCopyWithSameProvider(source, target, overwrite); - } + const sourceProvider = await this.withProvider(source); + const targetProvider = this.throwIfFileSystemIsReadonly(await this.withProvider(target)); - return this.doCopyWithDifferentProvider(source, target); + // copy + await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite); + + // resolve and send events + const fileStat = await this.resolveFile(target, { resolveMetadata: true }); + this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat)); + + return fileStat; } - private async doCopyWithSameProvider(source: URI, target: URI, overwrite: boolean = false): Promise { - const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(source)); - - // check if provider supports fast file/folder copy - if (provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy && typeof provider.copy === 'function') { - return this.doMoveCopyWithSameProvider(source, target, true /* keep copy */, overwrite); - } - - return this._impl.copyFile(source, target, overwrite); // TODO@ben implement properly - } - - private async doCopyWithDifferentProvider(source: URI, target: URI, overwrite?: boolean): Promise { - return this._impl.copyFile(source, target, overwrite); // TODO@ben implement properly - } - - private async doMoveCopyWithSameProvider(source: URI, target: URI, keepCopy: boolean, overwrite?: boolean): Promise { - const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(source)); + private async doMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise { // validation - const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); - const isCaseChange = isPathCaseSensitive ? false : isEqual(source, target, true /* ignore case */); - if (!isCaseChange && isEqualOrParent(target, source, !isPathCaseSensitive)) { - return Promise.reject(new Error(localize('unableToMoveCopyError1', "Unable to move/copy when source path is equal or parent of target path"))); + const { exists, isCaseChange } = await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, overwrite); + + // delete as needed + if (exists && !isCaseChange && overwrite) { + await this.del(target, { recursive: true }); } + // create parent folders + await this.mkdirp(targetProvider, dirname(target)); + + // copy source => target + if (mode === 'copy') { + + // same provider with fast copy: leverage copy() functionality + if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) { + return sourceProvider.copy(source, target, { overwrite: !!overwrite }); + } + + // otherwise, ensure we got the capabilities to do this + if ( + !(hasOpenReadWriteCloseCapability(sourceProvider) || hasReadWriteCapability(sourceProvider)) || + !(hasOpenReadWriteCloseCapability(targetProvider) || hasReadWriteCapability(targetProvider)) + ) { + return Promise.reject('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed to support copy.'); + } + + // when copying via buffer/unbuffered, we have to manually + // traverse the source if it is a folder and not a file + const sourceFile = await this.resolveFile(source); + if (sourceFile.isDirectory) { + return this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target, overwrite); + } else { + return this.doCopyFile(sourceProvider, source, targetProvider, target, overwrite); + } + } + + // move source => target + else { + + // same provider: leverage rename() functionality + if (sourceProvider === targetProvider) { + return sourceProvider.rename(source, target, { overwrite: !!overwrite }); + } + + // across providers: copy to target & delete at source + else { + await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite); + + return this.del(source, { recursive: true }); + } + } + } + + private async doCopyFile(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, overwrite?: boolean): Promise { + + // copy: source (buffered) => target (buffered) + if (hasOpenReadWriteCloseCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) { + return this.doPipeBuffered(sourceProvider, source, targetProvider, target); + } + + // copy: source (buffered) => target (unbuffered) + if (hasOpenReadWriteCloseCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) { + return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target, !!overwrite); + } + + // copy: source (unbuffered) => target (buffered) + if (hasReadWriteCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) { + return this.doPipeUnbufferedToBuffered(sourceProvider, source, targetProvider, target); + } + + // copy: source (unbuffered) => target (unbuffered) + if (hasReadWriteCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) { + return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target, !!overwrite); + } + } + + private async doCopyFolder(sourceProvider: IFileSystemProvider, sourceFolder: IFileStat, targetProvider: IFileSystemProvider, targetFolder: URI, overwrite?: boolean): Promise { + + // create folder in target + await targetProvider.mkdir(targetFolder); + + // create children in target + if (Array.isArray(sourceFolder.children)) { + await Promise.all(sourceFolder.children.map(async sourceChild => { + const targetChild = joinPath(targetFolder, sourceChild.name); + if (sourceChild.isDirectory) { + return this.doCopyFolder(sourceProvider, await this.resolveFile(sourceChild.resource), targetProvider, targetChild, overwrite); + } else { + return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild, overwrite); + } + })); + } + } + + private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, overwrite?: boolean): Promise<{ exists: boolean, isCaseChange: boolean }> { + let isCaseChange = false; + let isPathCaseSensitive = false; + + // Check if source is equal or parent to target (requires providers to be the same) + if (sourceProvider === targetProvider) { + const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + isCaseChange = isPathCaseSensitive ? false : isEqual(source, target, true /* ignore case */); + if (!isCaseChange && isEqualOrParent(target, source, !isPathCaseSensitive)) { + return Promise.reject(new Error(localize('unableToMoveCopyError1', "Unable to move/copy when source path is equal or parent of target path"))); + } + } + + // Extra checks if target exists and this is not a rename const exists = await this.existsFile(target); if (exists && !isCaseChange) { + + // Bail out if target exists and we are not about to overwrite if (!overwrite) { throw new FileOperationError(localize('unableToMoveCopyError2', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT); } // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw - if (isEqualOrParent(source, target, !isPathCaseSensitive)) { + if (sourceProvider === targetProvider && isEqualOrParent(source, target, !isPathCaseSensitive)) { return Promise.reject(new Error(localize('unableToMoveCopyError3', "Unable to move/copy. File would replace folder it is contained in."))); } - - await this.del(target, { recursive: true }); } - // create parent folders - await this.mkdirp(provider, dirname(target)); - - // rename/copy source => target - if (keepCopy) { - await provider.copy!(source, target, { overwrite: !!overwrite }); - } else { - await provider.rename(source, target, { overwrite: !!overwrite }); - } - - // resolve and send events - const fileStat = await this.resolveFile(target, { resolveMetadata: true }); - this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, fileStat)); - - return fileStat; + return { exists, isCaseChange }; } async createFolder(resource: URI): Promise { @@ -440,14 +555,25 @@ export class FileService2 extends Disposable implements IFileService { } async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise { - if (options && options.useTrash) { - return this._impl.del(resource, options); //TODO@ben this is https://github.com/Microsoft/vscode/issues/48259 - } - const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); + // Validate trash support + const useTrash = !!(options && options.useTrash); + if (useTrash && !(provider.capabilities & FileSystemProviderCapabilities.Trash)) { + throw new Error(localize('err.trash', "Provider does not support trash.")); + } + + // Validate recursive + const recursive = !!(options && options.recursive); + if (!recursive && await this.existsFile(resource)) { + const stat = await this.resolveFile(resource); + if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) { + throw new Error(localize('deleteFailed', "Failed to delete non-empty folder '{0}'.", resource.toString())); + } + } + // Delete through provider - await provider.delete(resource, { recursive: !!(options && options.recursive) }); + await provider.delete(resource, { recursive, useTrash }); // Events this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); @@ -461,17 +587,121 @@ export class FileService2 extends Disposable implements IFileService { get onFileChanges(): Event { return this._onFileChanges.event; } watchFileChanges(resource: URI): void { - this._impl.watchFileChanges(resource); + this.joinOnLegacy.then(legacy => legacy.watchFileChanges(resource)); } unwatchFileChanges(resource: URI): void { - this._impl.unwatchFileChanges(resource); + this.joinOnLegacy.then(legacy => legacy.unwatchFileChanges(resource)); } //#endregion //#region Helpers + private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, buffer: Uint8Array): Promise { + + // Open handle + const handle = await provider.open(resource, { create: true }); + + // write into handle until all bytes from buffer have been written + await this.doWriteBuffer(provider, handle, buffer, buffer.byteLength, 0, 0); + + // Close handle + return provider.close(handle); + } + + private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: Uint8Array, 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); + totalBytesWritten += bytesWritten; + } + } + + private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, buffer: Uint8Array, overwrite: boolean): Promise { + return provider.writeFile(resource, buffer, { create: true, overwrite }); + } + + private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + + // Open handles + const sourceHandle = await sourceProvider.open(source, { create: false }); + const targetHandle = await targetProvider.open(target, { create: true }); + + const buffer = new Uint8Array(8 * 1024); + + let posInFile = 0; + let posInBuffer = 0; + let bytesRead = 0; + 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); + + // write into target (targetHandle) at current position (posInFile) from buffer (buffer) at + // buffer position (posInBuffer) all bytes we read (bytesRead). + await this.doWriteBuffer(targetProvider, targetHandle, buffer, bytesRead, posInFile, posInBuffer); + + posInFile += bytesRead; + posInBuffer += bytesRead; + + // when buffer full, fill it again from the beginning + if (posInBuffer === buffer.length) { + posInBuffer = 0; + } + } while (bytesRead > 0); + + // Close handles + return Promise.all([ + sourceProvider.close(sourceHandle), + targetProvider.close(targetHandle) + ]).then(() => undefined); + } + + private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI, overwrite: boolean): Promise { + return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite }); + } + + private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + + // Open handle + const targetHandle = await targetProvider.open(target, { create: true }); + + // Read entire buffer from source and write buffered + const buffer = await sourceProvider.readFile(source); + await this.doWriteBuffer(targetProvider, targetHandle, buffer, buffer.byteLength, 0, 0); + + // Close handle + return targetProvider.close(targetHandle); + } + + private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI, overwrite: boolean): Promise { + + // Determine file size + const size = (await this.resolveFile(source, { resolveMetadata: true })).size; + + // Open handle + const sourceHandle = await sourceProvider.open(source, { create: false }); + + const buffer = new Uint8Array(size); + + let pos = 0; + let bytesRead = 0; + 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, pos, buffer, pos, buffer.byteLength - pos); + + pos += bytesRead; + } while (bytesRead > 0); + + // Write buffer into target at once + await this.doWriteUnbuffered(targetProvider, target, buffer, overwrite); + + // Close handle + return sourceProvider.close(sourceHandle); + } + private throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED); @@ -482,5 +712,3 @@ export class FileService2 extends Disposable implements IFileService { //#endregion } - -registerSingleton(IFileService, FileService2); diff --git a/src/vs/workbench/services/files2/electron-browser/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/electron-browser/diskFileSystemProvider.ts new file mode 100644 index 0000000000..cb53f87043 --- /dev/null +++ b/src/vs/workbench/services/files2/electron-browser/diskFileSystemProvider.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { shell } from 'electron'; +import { DiskFileSystemProvider as NodeDiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider'; +import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { isWindows } from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; +import { basename } from 'vs/base/common/path'; + +export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { + + get capabilities(): FileSystemProviderCapabilities { + if (!this._capabilities) { + this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash; + } + + return this._capabilities; + } + + protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise { + if (!opts.useTrash) { + return super.doDelete(filePath, opts); + } + + const result = shell.moveItemToTrash(filePath); + if (!result) { + throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath))); + } + } +} \ 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 69894c5776..4d5ed22466 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -3,17 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mkdir } from 'fs'; +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 { 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 } from 'vs/base/common/platform'; -import { statLink, readdir, unlink, del, move, copy } from 'vs/base/node/pfs'; +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 { normalize } 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'; export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider { @@ -21,7 +23,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro onDidChangeCapabilities: Event = Event.None; - private _capabilities: FileSystemProviderCapabilities; + protected _capabilities: FileSystemProviderCapabilities; get capabilities(): FileSystemProviderCapabilities { if (!this._capabilities) { this._capabilities = @@ -45,8 +47,15 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro try { const { stat, isSymbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly + let type: number; + if (isSymbolicLink) { + type = FileType.SymbolicLink | (stat.isDirectory() ? FileType.Directory : FileType.File); + } else { + type = stat.isFile() ? FileType.File : stat.isDirectory() ? FileType.Directory : FileType.Unknown; + } + return { - type: isSymbolicLink ? FileType.SymbolicLink : stat.isFile() ? FileType.File : stat.isDirectory() ? FileType.Directory : FileType.Unknown, + type, ctime: stat.ctime.getTime(), mtime: stat.mtime.getTime(), size: stat.size @@ -78,28 +87,112 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro //#region File Reading/Writing - readFile(resource: URI): Promise { - throw new Error('Method not implemented.'); + async readFile(resource: URI): Promise { + try { + const filePath = this.toFilePath(resource); + + return await readFile(filePath); + } catch (error) { + throw this.toFileSystemProviderError(error); + } } - writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - throw new Error('Method not implemented.'); + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + try { + const filePath = this.toFilePath(resource); + + // Validate target + const exists = await fileExists(filePath); + if (exists && !opts.overwrite) { + throw createFileSystemProviderError(new Error('File already exists'), FileSystemProviderErrorCode.FileExists); + } else if (!exists && !opts.create) { + throw createFileSystemProviderError(new Error('File does not exist'), FileSystemProviderErrorCode.FileNotFound); + } + + if (exists && isWindows) { + try { + // On Windows and if the file exists, we use a different strategy of saving the file + // by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows + // (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams + // (see https://github.com/Microsoft/vscode/issues/6363) + await truncate(filePath, 0); + + // We heard from one user that fs.truncate() succeeds, but the save fails (https://github.com/Microsoft/vscode/issues/61310) + // In that case, the file is now entirely empty and the contents are gone. This can happen if an external file watcher is + // installed that reacts on the truncate and keeps the file busy right after. Our workaround is to retry to save after a + // short timeout, assuming that the file is free to write then. + await retry(() => writeFile(filePath, content, { flag: 'r+' }), 100 /* ms delay */, 3 /* retries */); + } catch (error) { + // we heard from users that fs.truncate() fails (https://github.com/Microsoft/vscode/issues/59561) + // in that case we simply save the file without truncating first (same as macOS and Linux) + await writeFile(filePath, content); + } + } + + // macOS/Linux: just write directly + else { + await writeFile(filePath, content); + } + } catch (error) { + throw this.toFileSystemProviderError(error); + } } - open(resource: URI, opts: FileOpenOptions): Promise { - throw new Error('Method not implemented.'); + async open(resource: URI, opts: FileOpenOptions): Promise { + try { + const filePath = this.toFilePath(resource); + + let mode: string; + if (opts.create) { + // we take this as a hint that the file is opened for writing + // as such we use 'w' to truncate an existing or create the + // file otherwise. we do not allow reading. + mode = 'w'; + } else { + // otherwise we assume the file is opened for reading + // as such we use 'r' to neither truncate, nor create + // the file. + mode = 'r'; + } + + return await promisify(open)(filePath, mode); + } catch (error) { + throw this.toFileSystemProviderError(error); + } } - close(fd: number): Promise { - throw new Error('Method not implemented.'); + async close(fd: number): Promise { + try { + return await promisify(close)(fd); + } catch (error) { + throw this.toFileSystemProviderError(error); + } } - read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - throw new Error('Method not implemented.'); + async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + try { + const result = await promisify(read)(fd, data, offset, length, pos); + if (typeof result === 'number') { + return result; // node.d.ts fail + } + + return result.bytesRead; + } catch (error) { + throw this.toFileSystemProviderError(error); + } } - write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - throw new Error('Method not implemented.'); + async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + try { + const result = await promisify(write)(fd, data, offset, length, pos); + if (typeof result === 'number') { + return result; // node.d.ts fail + } + + return result.bytesWritten; + } catch (error) { + throw this.toFileSystemProviderError(error); + } } //#endregion @@ -118,11 +211,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro try { const filePath = this.toFilePath(resource); - if (opts.recursive) { - await del(filePath, tmpdir()); - } else { - await unlink(filePath); - } + await this.doDelete(filePath, opts); } catch (error) { if (error.code === 'ENOENT') { return Promise.resolve(); // tolerate that the file might not exist @@ -132,11 +221,23 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro } } + protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise { + if (opts.recursive) { + await del(filePath, tmpdir()); + } else { + await unlink(filePath); + } + } + async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { try { const fromFilePath = this.toFilePath(from); const toFilePath = this.toFilePath(to); + // Ensure target does not exist + await this.validateTargetDeleted(from, to, opts && opts.overwrite); + + // Move await move(fromFilePath, toFilePath); } catch (error) { throw this.toFileSystemProviderError(error); @@ -148,12 +249,33 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro const fromFilePath = this.toFilePath(from); const toFilePath = this.toFilePath(to); - return copy(fromFilePath, toFilePath); + // Ensure target does not exist + await this.validateTargetDeleted(from, to, opts && opts.overwrite); + + // Copy + await copy(fromFilePath, toFilePath); } catch (error) { throw this.toFileSystemProviderError(error); } } + private async validateTargetDeleted(from: URI, to: URI, overwrite?: boolean): Promise { + const fromFilePath = this.toFilePath(from); + const toFilePath = this.toFilePath(to); + + const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + const isCaseChange = isPathCaseSensitive ? false : isEqual(fromFilePath, toFilePath, true /* ignore case */); + + // handle existing target (unless this is a case change) + if (!isCaseChange && await fileExists(toFilePath)) { + if (!overwrite) { + throw createFileSystemProviderError(new Error('File at target already exists'), FileSystemProviderErrorCode.FileExists); + } + + await this.delete(to, { recursive: true, useTrash: false }); + } + } + //#endregion //#region File Watching @@ -169,7 +291,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro //#region Helpers - private toFilePath(resource: URI): string { + protected toFilePath(resource: URI): string { return normalize(resource.fsPath); } diff --git a/src/vs/workbench/services/files2/node/diskFileSystemSupport.ts b/src/vs/workbench/services/files2/node/diskFileSystemSupport.ts deleted file mode 100644 index a01696ed1f..0000000000 --- a/src/vs/workbench/services/files2/node/diskFileSystemSupport.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 { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IFileService } from 'vs/platform/files/common/files'; -import { Schemas } from 'vs/base/common/network'; -import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider'; -import { Disposable } from 'vs/base/common/lifecycle'; - -export class DiskFileSystemSupport extends Disposable implements IWorkbenchContribution { - - constructor(@IFileService fileService: IFileService) { - super(); - - this._register(fileService.registerProvider(Schemas.file, new DiskFileSystemProvider())); - } -} \ 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 618a328b38..71d8bf0253 100644 --- a/src/vs/workbench/services/files2/test/browser/fileService2.test.ts +++ b/src/vs/workbench/services/files2/test/browser/fileService2.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { FileService2 } from 'vs/workbench/services/files2/common/fileService2'; import { URI } from 'vs/base/common/uri'; -import { IFileSystemProviderRegistrationEvent } from 'vs/platform/files/common/files'; +import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IDisposable } from 'vs/base/common/lifecycle'; import { NullFileSystemProvider } from 'vs/workbench/test/workbenchTestServices'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -15,8 +15,9 @@ suite('File Service 2', () => { test('provider registration', async () => { const service = new FileService2(new NullLogService()); + const resource = URI.parse('test://foo/bar'); - assert.equal(service.canHandleResource(URI.parse('test://foo/bar')), false); + assert.equal(service.canHandleResource(resource), false); const registrations: IFileSystemProviderRegistrationEvent[] = []; service.onDidChangeFileSystemProviderRegistrations(e => { @@ -39,7 +40,7 @@ suite('File Service 2', () => { await service.activateProvider('test'); - assert.equal(service.canHandleResource(URI.parse('test://foo/bar')), true); + assert.equal(service.canHandleResource(resource), true); assert.equal(registrations.length, 1); assert.equal(registrations[0].scheme, 'test'); @@ -49,9 +50,12 @@ 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); + registrationDisposable!.dispose(); - assert.equal(service.canHandleResource(URI.parse('test://foo/bar')), false); + assert.equal(service.canHandleResource(resource), false); assert.equal(registrations.length, 2); assert.equal(registrations[1].scheme, 'test'); 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 dd20636297..053a16b6f6 100644 --- a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts @@ -14,14 +14,11 @@ import { join, basename, dirname, posix } from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { copy, del } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { existsSync, statSync, readdirSync } from 'fs'; -import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; -import { TestContextService, TestEnvironmentService, TestTextResourceConfigurationService, TestLifecycleService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; -import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { existsSync, statSync, readdirSync, readFileSync } from 'fs'; +import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; +import { isLinux } from 'vs/base/common/platform'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; function getByName(root: IFileStat, name: string): IFileStat | null { if (root.children === undefined) { @@ -37,80 +34,104 @@ function getByName(root: IFileStat, name: string): IFileStat | null { return null; } +export class TestDiskFileSystemProvider extends DiskFileSystemProvider { + + private _testCapabilities: FileSystemProviderCapabilities; + get capabilities(): FileSystemProviderCapabilities { + if (!this._testCapabilities) { + this._testCapabilities = + FileSystemProviderCapabilities.FileReadWrite | + FileSystemProviderCapabilities.FileOpenReadWriteClose | + FileSystemProviderCapabilities.FileFolderCopy; + + if (isLinux) { + this._testCapabilities |= FileSystemProviderCapabilities.PathCaseSensitive; + } + } + + return this._testCapabilities; + } + + set capabilities(capabilities: FileSystemProviderCapabilities) { + this._testCapabilities = capabilities; + } +} + suite('Disk File Service', () => { const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice'); + const testSchema = 'test'; let service: FileService2; + let fileProvider: TestDiskFileSystemProvider; + let testProvider: TestDiskFileSystemProvider; let testDir: string; + let disposables: IDisposable[] = []; + setup(async () => { service = new FileService2(new NullLogService()); - service.registerProvider(Schemas.file, new DiskFileSystemProvider()); + disposables.push(service); + + fileProvider = new TestDiskFileSystemProvider(); + service.registerProvider(Schemas.file, fileProvider); + + testProvider = new TestDiskFileSystemProvider(); + service.registerProvider(testSchema, testProvider); const id = generateUuid(); testDir = join(parentDir, id); const sourceDir = getPathFromAmdModule(require, './fixtures/service'); await copy(sourceDir, testDir); - - const legacyService = new FileService(new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); - service.setImpl(legacyService); }); teardown(async () => { - service.dispose(); + disposables = dispose(disposables); + await del(parentDir, tmpdir()); }); test('createFolder', async () => { let event: FileOperationEvent | undefined; - const toDispose = service.onAfterOperation(e => { - event = e; - }); + disposables.push(service.onAfterOperation(e => event = e)); const parent = await service.resolveFile(URI.file(testDir)); - const resource = URI.file(join(parent.resource.fsPath, 'newFolder')); + const newFolderResource = URI.file(join(parent.resource.fsPath, 'newFolder')); - const folder = await service.createFolder(resource); + const newFolder = await service.createFolder(newFolderResource); - assert.equal(folder.name, 'newFolder'); - assert.equal(existsSync(folder.resource.fsPath), true); + assert.equal(newFolder.name, 'newFolder'); + assert.equal(existsSync(newFolder.resource.fsPath), true); assert.ok(event); - assert.equal(event!.resource.fsPath, resource.fsPath); + assert.equal(event!.resource.fsPath, newFolderResource.fsPath); assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, resource.fsPath); + assert.equal(event!.target!.resource.fsPath, newFolderResource.fsPath); assert.equal(event!.target!.isDirectory, true); - - toDispose.dispose(); }); test('createFolder: creating multiple folders at once', async function () { let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); + disposables.push(service.onAfterOperation(e => event = e)); const multiFolderPaths = ['a', 'couple', 'of', 'folders']; const parent = await service.resolveFile(URI.file(testDir)); - const resource = URI.file(join(parent.resource.fsPath, ...multiFolderPaths)); + const newFolderResource = URI.file(join(parent.resource.fsPath, ...multiFolderPaths)); - const folder = await service.createFolder(resource); + const newFolder = await service.createFolder(newFolderResource); const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1]; - assert.equal(folder.name, lastFolderName); - assert.equal(existsSync(folder.resource.fsPath), true); + assert.equal(newFolder.name, lastFolderName); + assert.equal(existsSync(newFolder.resource.fsPath), true); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); + assert.equal(event!.resource.fsPath, newFolderResource.fsPath); assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, resource.fsPath); + assert.equal(event!.target!.resource.fsPath, newFolderResource.fsPath); assert.equal(event!.target!.isDirectory, true); - - toDispose.dispose(); }); test('existsFile', async () => { @@ -288,9 +309,7 @@ suite('Disk File Service', () => { test('deleteFile', async () => { let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); + disposables.push(service.onAfterOperation(e => event = e)); const resource = URI.file(join(testDir, 'deep', 'conway.js')); const source = await service.resolveFile(resource); @@ -301,15 +320,11 @@ suite('Disk File Service', () => { assert.ok(event!); assert.equal(event!.resource.fsPath, resource.fsPath); assert.equal(event!.operation, FileOperation.DELETE); - - toDispose.dispose(); }); test('deleteFolder (recursive)', async () => { let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); + disposables.push(service.onAfterOperation(e => event = e)); const resource = URI.file(join(testDir, 'deep')); const source = await service.resolveFile(resource); @@ -320,8 +335,6 @@ suite('Disk File Service', () => { assert.ok(event!); assert.equal(event!.resource.fsPath, resource.fsPath); assert.equal(event!.operation, FileOperation.DELETE); - - toDispose.dispose(); }); test('deleteFolder (non recursive)', async () => { @@ -329,128 +342,7 @@ suite('Disk File Service', () => { const source = await service.resolveFile(resource); try { await service.del(source.resource); - return Promise.reject(new Error('Unexpected')); - } - catch (error) { - return Promise.resolve(true); - } - }); - test('renameFile', async () => { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - const resource = URI.file(join(testDir, 'index.html')); - const source = await service.resolveFile(resource); - - const renamed = await service.moveFile(source.resource, URI.file(join(dirname(source.resource.fsPath), 'other.html'))); - - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.resource.fsPath), false); - assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); - - toDispose.dispose(); - }); - - test('renameFile - multi folder', async () => { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - const multiFolderPaths = ['a', 'couple', 'of', 'folders']; - const renameToPath = join(...multiFolderPaths, 'other.html'); - - const resource = URI.file(join(testDir, 'index.html')); - const source = await service.resolveFile(resource); - - const renamed = await service.moveFile(source.resource, URI.file(join(dirname(source.resource.fsPath), renameToPath))); - - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.resource.fsPath), false); - assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); - - toDispose.dispose(); - }); - - test('renameFolder', async () => { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - const resource = URI.file(join(testDir, 'deep')); - const source = await service.resolveFile(resource); - - const renamed = await service.moveFile(source.resource, URI.file(join(dirname(source.resource.fsPath), 'deeper'))); - - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.resource.fsPath), false); - assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); - - toDispose.dispose(); - }); - - test('renameFolder - multi folder', async () => { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - const multiFolderPaths = ['a', 'couple', 'of', 'folders']; - const renameToPath = join(...multiFolderPaths); - - const resource = URI.file(join(testDir, 'deep')); - const source = await service.resolveFile(resource); - - const renamed = await service.moveFile(source.resource, URI.file(join(dirname(source.resource.fsPath), renameToPath))); - - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.resource.fsPath), false); - assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); - - toDispose.dispose(); - }); - test('renameFile - MIX CASE', function () { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - const resource = URI.file(join(testDir, 'index.html')); - return service.resolveFile(resource).then(source => { - return service.moveFile(source.resource, URI.file(join(dirname(source.resource.fsPath), 'INDEX.html'))).then(renamed => { - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(basename(renamed.resource.fsPath), 'INDEX.html'); - - assert.ok(event); - assert.equal(event.resource.fsPath, resource.fsPath); - assert.equal(event.operation, FileOperation.MOVE); - assert.equal(event.target!.resource.fsPath, renamed.resource.fsPath); - toDispose.dispose(); - }); - }); - }); - - test('deleteFolder (non recursive)', async () => { - const resource = URI.file(join(testDir, 'deep')); - const source = await service.resolveFile(resource); - try { - await service.del(source.resource); return Promise.reject(new Error('Unexpected')); } catch (error) { @@ -460,30 +352,216 @@ suite('Disk File Service', () => { test('moveFile', async () => { let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); + disposables.push(service.onAfterOperation(e => event = e)); - const resource = URI.file(join(testDir, 'index.html')); - const source = await service.resolveFile(resource); + const source = URI.file(join(testDir, 'index.html')); + const sourceContents = readFileSync(source.fsPath); - const renamed = await service.moveFile(source.resource, URI.file(join(testDir, 'other.html'))); + const target = URI.file(join(dirname(source.fsPath), 'other.html')); + + const renamed = await service.moveFile(source, target); assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.resource.fsPath), false); + assert.equal(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); + assert.equal(event!.resource.fsPath, source.fsPath); assert.equal(event!.operation, FileOperation.MOVE); assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); - toDispose.dispose(); + const targetContents = readFileSync(target.fsPath); + + assert.equal(sourceContents.byteLength, targetContents.byteLength); + assert.equal(sourceContents.toString(), targetContents.toString()); }); - test('move - source parent of target', async () => { + test('moveFile - across providers (buffered => buffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + await testMoveAcrossProviders(); + }); + + test('moveFile - across providers (unbuffered => unbuffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileReadWrite); + + await testMoveAcrossProviders(); + }); + + test('moveFile - across providers (buffered => unbuffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileReadWrite); + + await testMoveAcrossProviders(); + }); + + test('moveFile - across providers (unbuffered => buffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + await testMoveAcrossProviders(); + }); + + test('moveFile - across providers - large (buffered => buffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + await testMoveAcrossProviders('lorem.txt'); + }); + + test('moveFile - across providers - large (unbuffered => unbuffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileReadWrite); + + await testMoveAcrossProviders('lorem.txt'); + }); + + test('moveFile - across providers - large (buffered => unbuffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileReadWrite); + + await testMoveAcrossProviders('lorem.txt'); + }); + + test('moveFile - across providers - large (unbuffered => buffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + await testMoveAcrossProviders('lorem.txt'); + }); + + async function testMoveAcrossProviders(sourceFile = 'index.html'): Promise { let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); + disposables.push(service.onAfterOperation(e => event = e)); + + const source = URI.file(join(testDir, sourceFile)); + const sourceContents = readFileSync(source.fsPath); + + const target = URI.file(join(dirname(source.fsPath), 'other.html')).with({ scheme: testSchema }); + + const renamed = await service.moveFile(source, target); + + assert.equal(existsSync(renamed.resource.fsPath), true); + assert.equal(existsSync(source.fsPath), false); + assert.ok(event!); + assert.equal(event!.resource.fsPath, source.fsPath); + assert.equal(event!.operation, FileOperation.MOVE); + assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + + const targetContents = readFileSync(target.fsPath); + + assert.equal(sourceContents.byteLength, targetContents.byteLength); + assert.equal(sourceContents.toString(), targetContents.toString()); + } + + test('moveFile - multi folder', async () => { + let event: FileOperationEvent; + disposables.push(service.onAfterOperation(e => event = e)); + + const multiFolderPaths = ['a', 'couple', 'of', 'folders']; + const renameToPath = join(...multiFolderPaths, 'other.html'); + + const source = URI.file(join(testDir, 'index.html')); + + const renamed = await service.moveFile(source, URI.file(join(dirname(source.fsPath), renameToPath))); + + assert.equal(existsSync(renamed.resource.fsPath), true); + assert.equal(existsSync(source.fsPath), false); + assert.ok(event!); + assert.equal(event!.resource.fsPath, source.fsPath); + assert.equal(event!.operation, FileOperation.MOVE); + assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + }); + + test('moveFile - directory', async () => { + let event: FileOperationEvent; + disposables.push(service.onAfterOperation(e => event = e)); + + const source = URI.file(join(testDir, 'deep')); + + const renamed = await service.moveFile(source, URI.file(join(dirname(source.fsPath), 'deeper'))); + + assert.equal(existsSync(renamed.resource.fsPath), true); + assert.equal(existsSync(source.fsPath), false); + assert.ok(event!); + assert.equal(event!.resource.fsPath, source.fsPath); + assert.equal(event!.operation, FileOperation.MOVE); + assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + }); + + test('moveFile - directory - across providers (buffered => buffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + await testMoveFolderAcrossProviders(); + }); + + test('moveFile - directory - across providers (unbuffered => unbuffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileReadWrite); + + await testMoveFolderAcrossProviders(); + }); + + test('moveFile - directory - across providers (buffered => unbuffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileReadWrite); + + await testMoveFolderAcrossProviders(); + }); + + test('moveFile - directory - across providers (unbuffered => buffered)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); + setCapabilities(testProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + await testMoveFolderAcrossProviders(); + }); + + async function testMoveFolderAcrossProviders(): Promise { + let event: FileOperationEvent; + disposables.push(service.onAfterOperation(e => event = e)); + + const source = URI.file(join(testDir, 'deep')); + const sourceChildren = readdirSync(source.fsPath); + + const target = URI.file(join(dirname(source.fsPath), 'deeper')).with({ scheme: testSchema }); + + const renamed = await service.moveFile(source, target); + + assert.equal(existsSync(renamed.resource.fsPath), true); + assert.equal(existsSync(source.fsPath), false); + assert.ok(event!); + assert.equal(event!.resource.fsPath, source.fsPath); + assert.equal(event!.operation, FileOperation.MOVE); + assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + + const targetChildren = readdirSync(target.fsPath); + assert.equal(sourceChildren.length, targetChildren.length); + for (let i = 0; i < sourceChildren.length; i++) { + assert.equal(sourceChildren[i], targetChildren[i]); + } + } + + test('moveFile - MIX CASE', async () => { + let event: FileOperationEvent; + disposables.push(service.onAfterOperation(e => event = e)); + + const source = URI.file(join(testDir, 'index.html')); + await service.resolveFile(source); + + const renamed = await service.moveFile(source, URI.file(join(dirname(source.fsPath), 'INDEX.html'))); + + assert.equal(existsSync(renamed.resource.fsPath), true); + assert.equal(basename(renamed.resource.fsPath), 'INDEX.html'); + assert.ok(event!); + assert.equal(event!.resource.fsPath, source.fsPath); + assert.equal(event!.operation, FileOperation.MOVE); + assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + }); + + test('moveFile - source parent of target', async () => { + let event: FileOperationEvent; + disposables.push(service.onAfterOperation(e => event = e)); await service.resolveFile(URI.file(join(testDir, 'index.html'))); try { @@ -491,15 +569,12 @@ suite('Disk File Service', () => { } catch (e) { assert.ok(e); assert.ok(!event!); - toDispose.dispose(); } }); - test('move - FILE_MOVE_CONFLICT', async () => { + test('moveFile - FILE_MOVE_CONFLICT', async () => { let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); + disposables.push(service.onAfterOperation(e => event = e)); const source = await service.resolveFile(URI.file(join(testDir, 'index.html'))); try { @@ -507,36 +582,14 @@ suite('Disk File Service', () => { } catch (e) { assert.equal(e.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT); assert.ok(!event!); - toDispose.dispose(); } }); - test('moveFile - MIX CASE', async () => { - let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); - - const resource = URI.file(join(testDir, 'index.html')); - const source = await service.resolveFile(resource); - - const renamed = await service.moveFile(source.resource, URI.file(join(testDir, 'INDEX.html'))); - - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(basename(renamed.resource.fsPath), 'INDEX.html'); - assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); - - toDispose.dispose(); - }); - test('moveFile - overwrite folder with file', async () => { let createEvent: FileOperationEvent; let moveEvent: FileOperationEvent; let deleteEvent: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { + disposables.push(service.onAfterOperation(e => { if (e.operation === FileOperation.CREATE) { createEvent = e; } else if (e.operation === FileOperation.DELETE) { @@ -544,37 +597,68 @@ suite('Disk File Service', () => { } else if (e.operation === FileOperation.MOVE) { moveEvent = e; } - }); + })); const parent = await service.resolveFile(URI.file(testDir)); const folderResource = URI.file(join(parent.resource.fsPath, 'conway.js')); const f = await service.createFolder(folderResource); - const resource = URI.file(join(testDir, 'deep', 'conway.js')); + const source = URI.file(join(testDir, 'deep', 'conway.js')); - const moved = await service.moveFile(resource, f.resource, true); + const moved = await service.moveFile(source, f.resource, true); assert.equal(existsSync(moved.resource.fsPath), true); assert.ok(statSync(moved.resource.fsPath).isFile); assert.ok(createEvent!); assert.ok(deleteEvent!); assert.ok(moveEvent!); - assert.equal(moveEvent!.resource.fsPath, resource.fsPath); + assert.equal(moveEvent!.resource.fsPath, source.fsPath); assert.equal(moveEvent!.target!.resource.fsPath, moved.resource.fsPath); assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); - - toDispose.dispose(); }); test('copyFile', async () => { + await doTestCopyFile(); + }); + + test('copyFile - unbuffered (FileSystemProviderCapabilities.FileReadWrite)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); + + await doTestCopyFile(); + }); + + test('copyFile - unbuffered large (FileSystemProviderCapabilities.FileReadWrite)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); + + await doTestCopyFile('lorem.txt'); + }); + + test('copyFile - buffered (FileSystemProviderCapabilities.FileOpenReadWriteClose)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + await doTestCopyFile(); + }); + + test('copyFile - buffered large (FileSystemProviderCapabilities.FileOpenReadWriteClose)', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + await doTestCopyFile('lorem.txt'); + }); + + function setCapabilities(provider: TestDiskFileSystemProvider, capabilities: FileSystemProviderCapabilities): void { + provider.capabilities = capabilities; + if (isLinux) { + provider.capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; + } + } + + async function doTestCopyFile(sourceName: string = 'index.html') { let event: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { - event = e; - }); + disposables.push(service.onAfterOperation(e => event = e)); - const source = await service.resolveFile(URI.file(join(testDir, 'index.html'))); - const resource = URI.file(join(testDir, 'other.html')); + const source = await service.resolveFile(URI.file(join(testDir, sourceName))); + const target = URI.file(join(testDir, 'other.html')); - const copied = await service.copyFile(source.resource, resource); + const copied = await service.copyFile(source.resource, target); assert.equal(existsSync(copied.resource.fsPath), true); assert.equal(existsSync(source.resource.fsPath), true); @@ -582,14 +666,19 @@ suite('Disk File Service', () => { assert.equal(event!.resource.fsPath, source.resource.fsPath); assert.equal(event!.operation, FileOperation.COPY); assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); - toDispose.dispose(); - }); + + const sourceContents = readFileSync(source.resource.fsPath); + const targetContents = readFileSync(target.fsPath); + + assert.equal(sourceContents.byteLength, targetContents.byteLength); + assert.equal(sourceContents.toString(), targetContents.toString()); + } test('copyFile - overwrite folder with file', async () => { let createEvent: FileOperationEvent; let copyEvent: FileOperationEvent; let deleteEvent: FileOperationEvent; - const toDispose = service.onAfterOperation(e => { + disposables.push(service.onAfterOperation(e => { if (e.operation === FileOperation.CREATE) { createEvent = e; } else if (e.operation === FileOperation.DELETE) { @@ -597,25 +686,23 @@ suite('Disk File Service', () => { } else if (e.operation === FileOperation.COPY) { copyEvent = e; } - }); + })); const parent = await service.resolveFile(URI.file(testDir)); const folderResource = URI.file(join(parent.resource.fsPath, 'conway.js')); const f = await service.createFolder(folderResource); - const resource = URI.file(join(testDir, 'deep', 'conway.js')); + const source = URI.file(join(testDir, 'deep', 'conway.js')); - const copied = await service.copyFile(resource, f.resource, true); + const copied = await service.copyFile(source, f.resource, true); assert.equal(existsSync(copied.resource.fsPath), true); assert.ok(statSync(copied.resource.fsPath).isFile); assert.ok(createEvent!); assert.ok(deleteEvent!); assert.ok(copyEvent!); - assert.equal(copyEvent!.resource.fsPath, resource.fsPath); + assert.equal(copyEvent!.resource.fsPath, source.fsPath); assert.equal(copyEvent!.target!.resource.fsPath, copied.resource.fsPath); assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); - - toDispose.dispose(); }); test('copyFile - MIX CASE', async () => { @@ -623,6 +710,7 @@ suite('Disk File Service', () => { const renamed = await service.moveFile(source.resource, URI.file(join(dirname(source.resource.fsPath), 'CONWAY.js'))); assert.equal(existsSync(renamed.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'CONWAY.js')); + const source_1 = await service.resolveFile(URI.file(join(testDir, 'deep', 'conway.js'))); const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source_1.resource.path)) }); diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index cdd9ea5cd6..f576a64593 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -36,6 +36,7 @@ import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, getSettingsTarge import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel, DefaultRawSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; const emptyEditableSettingsContent = '{\n}'; @@ -69,7 +70,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic @IModelService private readonly modelService: IModelService, @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, @IModeService private readonly modeService: IModeService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService ) { super(); // The default keybindings.json updates based on keyboard layouts, so here we make sure @@ -210,6 +212,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic this.openOrSwitchSettings2(ConfigurationTarget.USER, undefined, options, group); } + async openRemoteSettings(): Promise { + const environemnt = await this.remoteAgentService.getEnvironment(); + if (environemnt) { + await this.createIfNotExists(environemnt.appSettingsPath, emptyEditableSettingsContent); + return this.editorService.openEditor({ resource: environemnt.appSettingsPath, options: { pinned: true, revealIfOpened: true } }); + } + return null; + } + openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise { jsonEditor = typeof jsonEditor === 'undefined' ? this.configurationService.getValue('workbench.settings.editor') === 'json' : diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index ce3e6783d0..12c2759cdc 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -201,6 +201,7 @@ export interface IPreferencesService { openRawDefaultSettings(): Promise; openSettings(jsonEditor?: boolean): Promise; openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; + openRemoteSettings(): Promise; openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise; switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): Promise; diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts new file mode 100644 index 0000000000..23ea5e1d70 --- /dev/null +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Client } from 'vs/base/parts/ipc/common/ipc.net'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { connectRemoteAgentManagement, IConnectionOptions, IWebSocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { RemoteExtensionEnvironmentChannelClient } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; +import { INotificationService } from 'vs/platform/notification/common/notification'; + +export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService { + + _serviceBrand: any; + + private _environment: Promise | null; + + constructor( + @IEnvironmentService protected readonly _environmentService: IEnvironmentService + ) { + super(); + } + + abstract getConnection(): IRemoteAgentConnection | null; + + getEnvironment(bail?: boolean): Promise { + if (!this._environment) { + const connection = this.getConnection(); + if (connection) { + const client = new RemoteExtensionEnvironmentChannelClient(connection.getChannel('remoteextensionsenvironment')); + this._environment = client.getEnvironmentData(connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI); + } else { + this._environment = Promise.resolve(null); + } + } + return bail ? this._environment : this._environment.then(undefined, () => null); + } +} + +export class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection { + + readonly remoteAuthority: string; + private _connection: Promise> | null; + + constructor( + remoteAuthority: string, + private _commit: string | undefined, + private _webSocketFactory: IWebSocketFactory, + private _environmentService: IEnvironmentService, + private _remoteAuthorityResolverService: IRemoteAuthorityResolverService + ) { + super(); + this.remoteAuthority = remoteAuthority; + this._connection = null; + } + + getChannel(channelName: string): T { + return getDelayedChannel(this._getOrCreateConnection().then(c => c.getChannel(channelName))); + } + + registerChannel>(channelName: string, channel: T): void { + this._getOrCreateConnection().then(client => client.registerChannel(channelName, channel)); + } + + private _getOrCreateConnection(): Promise> { + if (!this._connection) { + this._connection = this._createConnection(); + } + return this._connection; + } + + private async _createConnection(): Promise> { + const options: IConnectionOptions = { + isBuilt: this._environmentService.isBuilt, + commit: this._commit, + webSocketFactory: this._webSocketFactory, + addressProvider: { + getAddress: async () => { + const { host, port } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority); + return { host, port }; + } + } + }; + const connection = await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`); + this._register(connection); + return connection.client; + } +} + +class RemoteConnectionFailureNotificationContribution implements IWorkbenchContribution { + + constructor( + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @INotificationService notificationService: INotificationService, + ) { + // Let's cover the case where connecting to fetch the remote extension info fails + remoteAgentService.getEnvironment(true) + .then(undefined, err => notificationService.error(nls.localize('connectionError', "Failed to connect to the remote extension host agent (Error: {0})", err ? err.message : ''))); + } + +} + +const workbenchRegistry = Registry.as(Extensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(RemoteConnectionFailureNotificationContribution, LifecyclePhase.Ready); diff --git a/src/vs/workbench/services/remote/node/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts similarity index 100% rename from src/vs/workbench/services/remote/node/remoteAgentEnvironmentChannel.ts rename to src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index 2cd6957ffb..f4b56e6e85 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -15,7 +15,7 @@ export interface IRemoteAgentService { _serviceBrand: any; getConnection(): IRemoteAgentConnection | null; - getEnvironment(): Promise; + getEnvironment(bail?: boolean): Promise; } export interface IRemoteAgentConnection { diff --git a/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts b/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts index 394af585cd..aaac649ac3 100644 --- a/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts +++ b/src/vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl.ts @@ -3,109 +3,29 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { getDelayedChannel } from 'vs/base/parts/ipc/node/ipc'; -import { Client } from 'vs/base/parts/ipc/node/ipc.net'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { connectRemoteAgentManagement } from 'vs/platform/remote/node/remoteAgentConnection'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteAgentConnection } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { RemoteExtensionEnvironmentChannelClient } from 'vs/workbench/services/remote/node/remoteAgentEnvironmentChannel'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { localize } from 'vs/nls'; +import product from 'vs/platform/product/node/product'; +import { nodeWebSocketFactory } from 'vs/platform/remote/node/nodeWebSocketFactory'; +import { AbstractRemoteAgentService, RemoteAgentConnection } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; -export class RemoteAgentService extends Disposable implements IRemoteAgentService { - - _serviceBrand: any; +export class RemoteAgentService extends AbstractRemoteAgentService { private readonly _connection: IRemoteAgentConnection | null = null; - private _environment: Promise | null; - constructor( - { remoteAuthority }: IWindowConfiguration, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, + constructor({ remoteAuthority }: IWindowConfiguration, + @IEnvironmentService environmentService: IEnvironmentService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService ) { - super(); + super(environmentService); if (remoteAuthority) { - this._connection = this._register(new RemoteAgentConnection(remoteAuthority, _environmentService, remoteAuthorityResolverService)); + this._connection = this._register(new RemoteAgentConnection(remoteAuthority, product.commit, nodeWebSocketFactory, environmentService, remoteAuthorityResolverService)); } } getConnection(): IRemoteAgentConnection | null { return this._connection; } - - getEnvironment(bail?: boolean): Promise { - if (!this._environment) { - const connection = this.getConnection(); - if (connection) { - const client = new RemoteExtensionEnvironmentChannelClient(connection.getChannel('remoteextensionsenvironment')); - this._environment = client.getEnvironmentData(connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI); - } else { - this._environment = Promise.resolve(null); - } - } - return bail ? this._environment : this._environment.then(undefined, () => null); - } } - -class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection { - - readonly remoteAuthority: string; - private _connection: Promise> | null; - - constructor( - remoteAuthority: string, - private _environmentService: IEnvironmentService, - private _remoteAuthorityResolverService: IRemoteAuthorityResolverService - ) { - super(); - this.remoteAuthority = remoteAuthority; - this._connection = null; - } - - getChannel(channelName: string): T { - return getDelayedChannel(this._getOrCreateConnection().then(c => c.getChannel(channelName))); - } - - registerChannel>(channelName: string, channel: T): void { - this._getOrCreateConnection().then(client => client.registerChannel(channelName, channel)); - } - - private _getOrCreateConnection(): Promise> { - if (!this._connection) { - this._connection = this._createConnection(); - } - return this._connection; - } - - private async _createConnection(): Promise> { - const resolvedAuthority = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority); - const connection = await connectRemoteAgentManagement(this.remoteAuthority, resolvedAuthority.host, resolvedAuthority.port, `renderer`, this._environmentService.isBuilt); - this._register(connection); - return connection.client; - } -} - -class RemoteConnectionFailureNotificationContribution implements IWorkbenchContribution { - - constructor( - @IRemoteAgentService remoteAgentService: RemoteAgentService, - @INotificationService notificationService: INotificationService, - ) { - // Let's cover the case where connecting to fetch the remote extension info fails - remoteAgentService.getEnvironment(true) - .then(undefined, err => notificationService.error(localize('connectionError', "Failed to connect to the remote extension host agent (Error: {0})", err ? err.message : ''))); - } - -} - -const workbenchRegistry = Registry.as(Extensions.Workbench); -workbenchRegistry.registerWorkbenchContribution(RemoteConnectionFailureNotificationContribution, LifecyclePhase.Ready); \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/fileSearchManager.ts b/src/vs/workbench/services/search/node/fileSearchManager.ts index 15398775e6..3cc69a9451 100644 --- a/src/vs/workbench/services/search/node/fileSearchManager.ts +++ b/src/vs/workbench/services/search/node/fileSearchManager.ts @@ -136,10 +136,10 @@ class FileSearchEngine { if (results) { results.forEach(result => { - const relativePath = path.relative(fq.folder.fsPath, result.fsPath); + const relativePath = path.posix.relative(fq.folder.path, result.path); if (noSiblingsClauses) { - const basename = path.basename(result.fsPath); + const basename = path.basename(result.path); this.matchFile(onResult, { base: fq.folder, relativePath, basename }); return; diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 9c46e581dd..ac529fdcd8 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -140,6 +140,10 @@ export function rgErrorMsgForDisplay(msg: string): Maybe { return new SearchError(firstLine.charAt(0).toUpperCase() + firstLine.substr(1), SearchErrorCode.invalidLiteral); } + if (startsWith(firstLine, 'PCRE2: error compiling pattern')) { + return new SearchError(firstLine, SearchErrorCode.regexParseError); + } + return undefined; } diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index b40a83f41a..e39184d324 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -14,7 +14,7 @@ import { Schemas } from 'vs/base/common/network'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI as uri } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; -import { getNextTickChannel } from 'vs/base/parts/ipc/node/ipc'; +import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -403,13 +403,6 @@ export class SearchService extends Disposable implements ISearchService { } private matches(resource: uri, query: ITextQuery): boolean { - // includes - if (query.includePattern) { - if (resource.scheme !== Schemas.file) { - return false; // if we match on file patterns, we have to ignore non file resources - } - } - return pathIncludedInQuery(query, resource.fsPath); } diff --git a/src/vs/workbench/services/themes/common/fileIconThemeStore.ts b/src/vs/workbench/services/themes/common/fileIconThemeStore.ts index 9e8fc68ec5..0816834bbc 100644 --- a/src/vs/workbench/services/themes/common/fileIconThemeStore.ts +++ b/src/vs/workbench/services/themes/common/fileIconThemeStore.ts @@ -153,7 +153,7 @@ export class FileIconThemeStore { }); } - public findThemeDataByParentLocation(parentLocation: URI | undefined): any { + public findThemeDataByParentLocation(parentLocation: URI | undefined): Promise { if (parentLocation) { return this.getFileIconThemes().then(allThemes => { return allThemes.filter(t => t.location && resources.isEqualOrParent(t.location, parentLocation)); diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index b9acc6d0bb..3aa3deb1c3 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -56,7 +56,7 @@ export interface IWorkbenchThemeService extends IThemeService { getColorTheme(): IColorTheme; getColorThemes(): Promise; onDidColorThemeChange: Event; - restoreColorTheme(); + restoreColorTheme(): void; setFileIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined): Promise; getFileIconTheme(): IFileIconTheme; diff --git a/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts b/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts index 1253767271..36efd36757 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostConfiguration.test.ts @@ -56,7 +56,7 @@ suite('ExtHostConfiguration', function () { assert.equal(extHostConfig.getConfiguration('search.exclude')['**/node_modules'], true); assert.equal(extHostConfig.getConfiguration('search.exclude').get('**/node_modules'), true); - assert.equal(extHostConfig.getConfiguration('search').get('exclude')!['**/node_modules'], true); + assert.equal(extHostConfig.getConfiguration('search').get('exclude')['**/node_modules'], true); assert.equal(extHostConfig.getConfiguration('search.exclude').has('**/node_modules'), true); assert.equal(extHostConfig.getConfiguration('search').has('exclude.**/node_modules'), true); @@ -111,7 +111,7 @@ suite('ExtHostConfiguration', function () { }); let testObject = all.getConfiguration(); - let actual = testObject.get('farboo')!; + let actual = testObject.get('farboo')!; actual['nested']['config1'] = 41; assert.equal(41, actual['nested']['config1']); actual['farboo1'] = 'newValue'; @@ -190,7 +190,7 @@ suite('ExtHostConfiguration', function () { 'config4': '' }), JSON.stringify(actual)); - actual = testObject.get('workbench')!['colorCustomizations']!; + actual = testObject.get('workbench')!['colorCustomizations']!; actual['statusBar.background'] = 'anothervalue'; assert.deepEqual(JSON.stringify({ 'statusBar.foreground': 'somevalue', 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 7224ca1281..548f2d8947 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -181,7 +181,7 @@ suite('ExtHostSearch', () => { const reportedResults = [ joinPath(rootFolderA, 'file1.ts'), joinPath(rootFolderA, 'file2.ts'), - joinPath(rootFolderA, 'file3.ts') + joinPath(rootFolderA, 'subfolder/file3.ts') ]; await registerTestFileSearchProvider({ @@ -586,7 +586,7 @@ suite('ExtHostSearch', () => { const reportedResults = [ joinPath(fancySchemeFolderA, 'file1.ts'), joinPath(fancySchemeFolderA, 'file2.ts'), - joinPath(fancySchemeFolderA, 'file3.ts'), + joinPath(fancySchemeFolderA, 'subfolder/file3.ts'), ]; diff --git a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts index 55dd92ed9b..f211ddb40e 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -43,9 +43,9 @@ suite('ExtHostTreeView', function () { let target: RecordingShape; let onDidChangeTreeNode: Emitter<{ key: string } | undefined>; let onDidChangeTreeNodeWithId: Emitter<{ key: string }>; - let tree: object; - let labels: object; - let nodes: object; + let tree: { [key: string]: any }; + let labels: { [key: string]: string }; + let nodes: { [key: string]: { key: string } }; setup(() => { tree = { @@ -392,7 +392,7 @@ suite('ExtHostTreeView', function () { tree[dupItems['adup1']] = {}; tree['d'] = {}; - const bdup1Tree = {}; + const bdup1Tree: { [key: string]: any } = {}; bdup1Tree['h'] = {}; bdup1Tree[dupItems['hdup1']] = {}; bdup1Tree['j'] = {}; @@ -596,7 +596,7 @@ suite('ExtHostTreeView', function () { } if (typeof obj === 'object') { - const result = {}; + const result: { [key: string]: any } = {}; for (const key of Object.keys(obj)) { if (obj[key] !== undefined) { result[key] = removeUnsetKeys(obj[key]); diff --git a/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts b/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts index 3912070edb..06685b8691 100644 --- a/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts @@ -9,7 +9,7 @@ import { editorMarkerNavigationError } from 'vs/editor/contrib/gotoError/gotoErr import { overviewRulerModifiedForeground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { STATUS_BAR_DEBUGGING_BACKGROUND } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { debugExceptionWidgetBackground } from 'vs/workbench/contrib/debug/browser/exceptionWidget'; -import { debugToolBarBackground } from 'vs/workbench/contrib/debug/browser/debugToolbar'; +import { debugToolBarBackground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { buttonBackground } from 'vs/workbench/contrib/welcome/page/browser/welcomePage'; import { embeddedEditorBackground } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart'; import { request, asText } from 'vs/base/node/request'; diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 07bbae0942..e372ae8c35 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -37,7 +37,7 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IWindowsService, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, MenuBarVisibility, IURIToOpen } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IMessageBoxResult, IWindowConfiguration, MenuBarVisibility, IURIToOpen, IOpenSettings } from 'vs/platform/windows/common/windows'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -1014,7 +1014,7 @@ export class TestFileService implements IFileService { onDidChangeFileSystemProviderRegistrations = Event.None; - registerProvider(_scheme: string, _provider) { + registerProvider(_scheme: string, _provider: IFileSystemProvider) { return { dispose() { } }; } @@ -1026,6 +1026,8 @@ export class TestFileService implements IFileService { return resource.scheme === 'file'; } + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise { return Promise.resolve(false); } + del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean }): Promise { return Promise.resolve(); } @@ -1222,7 +1224,7 @@ export class TestWindowService implements IWindowService { return Promise.resolve(); } - openWindow(_uris: IURIToOpen[], _options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): Promise { + openWindow(_uris: IURIToOpen[], _options?: IOpenSettings): Promise { return Promise.resolve(); } @@ -1238,10 +1240,6 @@ export class TestWindowService implements IWindowService { return Promise.resolve(); } - show(): Promise { - return Promise.resolve(); - } - showMessageBox(_options: Electron.MessageBoxOptions): Promise { return Promise.resolve({ button: 0 }); } @@ -1431,7 +1429,7 @@ export class TestWindowsService implements IWindowsService { } // Global methods - openWindow(_windowId: number, _uris: IURIToOpen[], _options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): Promise { + openWindow(_windowId: number, _uris: IURIToOpen[], _options: IOpenSettings): Promise { return Promise.resolve(); } @@ -1439,10 +1437,6 @@ export class TestWindowsService implements IWindowsService { return Promise.resolve(); } - showWindow(_windowId: number): Promise { - return Promise.resolve(); - } - getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { throw new Error('not implemented'); } diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index c314e28655..8c78e62762 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -115,7 +115,6 @@ import 'vs/workbench/services/backup/node/backupFileService'; import 'vs/workbench/services/editor/browser/editorService'; import 'vs/workbench/services/history/browser/history'; import 'vs/workbench/services/files/node/remoteFileService'; -import 'vs/workbench/services/files2/common/fileService2'; import 'vs/workbench/services/activity/browser/activityService'; import 'vs/workbench/browser/parts/views/views'; import 'vs/workbench/services/keybinding/electron-browser/keybindingService'; @@ -336,7 +335,7 @@ import 'vs/workbench/contrib/scm/browser/scmViewlet'; import 'vs/workbench/contrib/markers/browser/markers.contribution'; // Comments -import 'vs/workbench/contrib/comments/electron-browser/comments.contribution'; +import 'vs/workbench/contrib/comments/browser/comments.contribution'; // URL Support import 'vs/workbench/contrib/url/common/url.contribution'; diff --git a/src/vs/workbench/workbench.nodeless.main.ts b/src/vs/workbench/workbench.nodeless.main.ts index c307f39635..62302a6fda 100644 --- a/src/vs/workbench/workbench.nodeless.main.ts +++ b/src/vs/workbench/workbench.nodeless.main.ts @@ -97,6 +97,8 @@ import { IHeapService, NullHeapService } from 'vs/workbench/services/heap/common import { IBroadcastService, NullBroadcastService } from 'vs/workbench/services/broadcast/common/broadcast'; import 'vs/workbench/browser/nodeless.simpleservices'; +import 'vs/platform/dialogs/browser/dialogService'; + import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; // import 'vs/workbench/services/integrity/node/integrityService'; @@ -240,7 +242,7 @@ import 'vs/workbench/contrib/scm/browser/scmViewlet'; import 'vs/workbench/contrib/markers/browser/markers.contribution'; // Comments -// import 'vs/workbench/contrib/comments/electron-browser/comments.contribution'; +// import 'vs/workbench/contrib/comments/browser/comments.contribution'; // URL Support import 'vs/workbench/contrib/url/common/url.contribution'; diff --git a/test/smoke/src/application.ts b/test/smoke/src/application.ts index 0d816a5ff0..e9d2018e4b 100644 --- a/test/smoke/src/application.ts +++ b/test/smoke/src/application.ts @@ -143,6 +143,6 @@ export class Application { // wait a bit, since focus might be stolen off widgets // as soon as they open (eg quick open) - await new Promise(c => setTimeout(c, 500)); + await new Promise(c => setTimeout(c, 1000)); } } diff --git a/test/smoke/src/areas/debug/debug.test.ts b/test/smoke/src/areas/debug/debug.test.ts index 61ef74b99a..7f53a9d80b 100644 --- a/test/smoke/src/areas/debug/debug.test.ts +++ b/test/smoke/src/areas/debug/debug.test.ts @@ -61,16 +61,16 @@ export function setup() { it('focus stack frames and variables', async function () { const app = this.app as Application; - await app.workbench.debug.waitForVariableCount(4); + await app.workbench.debug.waitForVariableCount(4, 5); await app.workbench.debug.focusStackFrame('layer.js', 'looking for layer.js'); - await app.workbench.debug.waitForVariableCount(5); + await app.workbench.debug.waitForVariableCount(5, 6); await app.workbench.debug.focusStackFrame('route.js', 'looking for route.js'); - await app.workbench.debug.waitForVariableCount(3); + await app.workbench.debug.waitForVariableCount(3, 4); await app.workbench.debug.focusStackFrame('index.js', 'looking for index.js'); - await app.workbench.debug.waitForVariableCount(4); + await app.workbench.debug.waitForVariableCount(4, 5); }); it('stepOver, stepIn, stepOut', async function () { diff --git a/test/smoke/src/areas/debug/debugSmoke.ts b/test/smoke/src/areas/debug/debugSmoke.ts index 14b28b4427..dba2a65ac7 100644 --- a/test/smoke/src/areas/debug/debugSmoke.ts +++ b/test/smoke/src/areas/debug/debugSmoke.ts @@ -136,8 +136,9 @@ export class Debug extends Viewlet { await this.waitForOutput(output => accept(output[output.length - 1] || '')); } - async waitForVariableCount(count: number): Promise { - await this.code.waitForElements(VARIABLE, false, els => els.length === count); + // Different node versions give different number of variables. As a workaround be more relaxed when checking for variable count + async waitForVariableCount(count: number, alternativeCount: number): Promise { + await this.code.waitForElements(VARIABLE, false, els => els.length === count || els.length === alternativeCount); } private async waitForOutput(fn: (output: string[]) => boolean): Promise { diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 92e870c197..24dfcb5d58 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -48,7 +48,7 @@ export function setup() { await app.workbench.search.expandReplace(); await app.workbench.search.setReplaceText('ydob'); await app.workbench.search.replaceFileMatch('app.js'); - await app.workbench.search.waitForResultText('16 results in 5 files'); + await app.workbench.search.waitForResultText('12 results in 4 files'); await app.workbench.search.searchFor('ydob'); await app.workbench.search.setReplaceText('body'); diff --git a/test/smoke/src/vscode/code.ts b/test/smoke/src/vscode/code.ts index 9f94c93d88..56ed67a914 100644 --- a/test/smoke/src/vscode/code.ts +++ b/test/smoke/src/vscode/code.ts @@ -113,6 +113,7 @@ export async function spawn(options: SpawnOptions): Promise { '--skip-release-notes', '--sticky-quickopen', '--disable-telemetry', + '--disable-extensions', '--disable-updates', '--disable-crash-reporter', `--extensions-dir=${options.extensionsPath}`, diff --git a/tslint.json b/tslint.json index f464db92dc..008be35943 100644 --- a/tslint.json +++ b/tslint.json @@ -40,7 +40,8 @@ "duplicate-imports": true, "no-new-buffer": true, "translation-remind": true, - "no-standalone-editor": true + "no-standalone-editor": true, + "no-nls-in-standalone-editor": true }, "defaultSeverity": "warning" -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1af2fce6b3..8e5e9750c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6121,7 +6121,7 @@ node-abi@^2.2.0: dependencies: semver "^5.4.1" -node-addon-api@^1.3.0: +node-addon-api@1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.6.2.tgz#d8aad9781a5cfc4132cc2fecdbdd982534265217" integrity sha512-479Bjw9nTE5DdBSZZWprFryHGjUaQC31y1wHo19We/k0BZlrmhqQitWoUL0cD8+scljCbIUL+E58oRDEakdGGA== @@ -9783,6 +9783,13 @@ vscode-uri@^1.0.6: resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d" integrity sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww== +vscode-windows-ca-certs@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/vscode-windows-ca-certs/-/vscode-windows-ca-certs-0.1.0.tgz#d58eeb40b536130918cfde2b01e6dc7e5c1bd757" + integrity sha512-ZfZbfJIE09Q0dwGqmqTj7kuAq4g6lul9WPJvo0DkKjln8/FL+dY3wUKIKbYwWQp4x56SBTLBq3tJkD72xQ9Gqw== + dependencies: + node-addon-api "1.6.2" + vscode-windows-registry@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.1.tgz#bc9f765563eb6dc1c9ad9a41f9eaacc84dfadc7c" @@ -9790,10 +9797,10 @@ vscode-windows-registry@1.0.1: dependencies: nan "^2.12.1" -vscode-xterm@3.13.0-beta1: - version "3.13.0-beta1" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.13.0-beta1.tgz#c1f4d90fe201f7e3afe7e7e0835f0b093a09afd5" - integrity sha512-NxWVk+q5cbHpbLFME+RfQ2RmJdR95lxjnhZEkXgHPOkmblzoHxRsZY9yF85Zfl5oNaiLMre9OwuhkHAZBQX8vg== +vscode-xterm@3.13.0-beta2: + version "3.13.0-beta2" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.13.0-beta2.tgz#4517a96ef30d8a8d6f27c60bfc374a405811ee6e" + integrity sha512-9iu3oxpFbyUzsAPYeipMSaI7I/nY4WHlpq2kJXXzibZAO9l7pVFBxFR9H0gfDBVn1+fMJsLIzRWGX9lKC83lfQ== vso-node-api@6.1.2-preview: version "6.1.2-preview" @@ -9926,12 +9933,6 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -"win-ca-lib@https://github.com/chrmarti/win-ca/releases/download/v2.4.1-lib-test/win-ca-lib-2.4.1.tgz": - version "2.4.1" - resolved "https://github.com/chrmarti/win-ca/releases/download/v2.4.1-lib-test/win-ca-lib-2.4.1.tgz#92415b2c45d08b72217011db8050f8c957e208c4" - dependencies: - node-addon-api "^1.3.0" - window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"