From f6debb876919f15f5aacb63c08935f21a191b67c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Jun 2017 09:55:19 +0200 Subject: [PATCH] multi root - add action to add folders --- src/vs/code/electron-main/windows.ts | 11 ++- src/vs/platform/windows/common/windows.ts | 2 + src/vs/platform/windows/common/windowsIpc.ts | 6 ++ .../windows/electron-browser/windowService.ts | 4 + .../platform/windows/electron-main/windows.ts | 1 + .../windows/electron-main/windowsService.ts | 4 + .../workbench/browser/actions/fileActions.ts | 23 +++++ .../workbench/electron-browser/workbench.ts | 5 ++ .../files/browser/fileActions.contribution.ts | 3 +- .../workspace/common/workspaceEditing.ts | 22 +++++ .../workspace/node/workspaceEditingService.ts | 89 +++++++++++++++++++ .../workbench/test/workbenchTestServices.ts | 38 ++++++++ 12 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/vs/workbench/services/workspace/common/workspaceEditing.ts create mode 100644 src/vs/workbench/services/workspace/node/workspaceEditingService.ts diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index fede11c5cbb..c3d9765484c 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -28,6 +28,7 @@ import { isEqual, isEqualOrParent } from 'vs/base/common/paths'; import { IWindowsMainService, IOpenConfiguration } from "vs/platform/windows/electron-main/windows"; import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService"; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from "vs/base/common/platform"; +import { TPromise } from "vs/base/common/winjs.base"; enum WindowError { UNRESPONSIVE, @@ -1141,6 +1142,14 @@ export class WindowsManager implements IWindowsMainService { this.fileDialog.pickAndOpen({ pickFolders: true, forceNewWindow, window }, 'openFolder', data); } + public pickFolder(): TPromise { + return new TPromise((c, e) => { + this.fileDialog.getFileOrFolderPaths({ pickFolders: true }, folders => { + c(folders || []); + }); + }); + } + public quit(): void { // If the user selected to exit from an extension development host window, do not quit, but just @@ -1193,7 +1202,7 @@ class FileDialog { }); } - private getFileOrFolderPaths(options: INativeOpenDialogOptions, clb: (paths: string[]) => void): void { + public getFileOrFolderPaths(options: INativeOpenDialogOptions, clb: (paths: string[]) => void): void { const workingDir = options.path || this.storageService.getItem(FileDialog.workingDirPickerStorageKey); const focussedWindow = options.window || this.windowsMainService.getFocusedWindow(); diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 9002bd7e8f8..33545fb6215 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -24,6 +24,7 @@ export interface IWindowsService { pickFileFolderAndOpen(windowId: number, forceNewWindow?: boolean, data?: ITelemetryData): TPromise; pickFileAndOpen(windowId: number, forceNewWindow?: boolean, path?: string, data?: ITelemetryData): TPromise; pickFolderAndOpen(windowId: number, forceNewWindow?: boolean, data?: ITelemetryData): TPromise; + pickFolder(): TPromise; reloadWindow(windowId: number): TPromise; openDevTools(windowId: number): TPromise; toggleDevTools(windowId: number): TPromise; @@ -77,6 +78,7 @@ export interface IWindowService { pickFileFolderAndOpen(forceNewWindow?: boolean, data?: ITelemetryData): TPromise; pickFileAndOpen(forceNewWindow?: boolean, path?: string, data?: ITelemetryData): TPromise; pickFolderAndOpen(forceNewWindow?: boolean, data?: ITelemetryData): TPromise; + pickFolder(): TPromise; reloadWindow(): TPromise; openDevTools(): TPromise; toggleDevTools(): TPromise; diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts index 313e5b51135..1f113da4d8a 100644 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -17,6 +17,7 @@ export interface IWindowsChannel extends IChannel { call(command: 'pickFileFolderAndOpen', arg: [number, boolean, ITelemetryData]): TPromise; call(command: 'pickFileAndOpen', arg: [number, boolean, string, ITelemetryData]): TPromise; call(command: 'pickFolderAndOpen', arg: [number, boolean, ITelemetryData]): TPromise; + call(command: 'pickFolder'): TPromise; call(command: 'reloadWindow', arg: number): TPromise; call(command: 'toggleDevTools', arg: number): TPromise; call(command: 'closeFolder', arg: number): TPromise; @@ -67,6 +68,7 @@ export class WindowsChannel implements IWindowsChannel { case 'pickFileFolderAndOpen': return this.service.pickFileFolderAndOpen(arg[0], arg[1], arg[2]); case 'pickFileAndOpen': return this.service.pickFileAndOpen(arg[0], arg[1], arg[2], arg[3]); case 'pickFolderAndOpen': return this.service.pickFolderAndOpen(arg[0], arg[1], arg[2]); + case 'pickFolder': return this.service.pickFolder(); case 'reloadWindow': return this.service.reloadWindow(arg); case 'openDevTools': return this.service.openDevTools(arg); case 'toggleDevTools': return this.service.toggleDevTools(arg); @@ -127,6 +129,10 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('pickFolderAndOpen', [windowId, forceNewWindow, data]); } + pickFolder(): TPromise { + return this.channel.call('pickFolder'); + } + reloadWindow(windowId: number): TPromise { return this.channel.call('reloadWindow', windowId); } diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index 8db5deb0d79..ca405260ada 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -34,6 +34,10 @@ export class WindowService implements IWindowService { return this.windowsService.pickFolderAndOpen(this.windowId, forceNewWindow, data); } + pickFolder(): TPromise { + return this.windowsService.pickFolder(); + } + reloadWindow(): TPromise { return this.windowsService.reloadWindow(this.windowId); } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index fa1d6c001c6..c87bb8d570b 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -52,6 +52,7 @@ export interface IWindowsMainService { pickFileFolderAndOpen(forceNewWindow?: boolean, data?: ITelemetryData): void; pickFileAndOpen(forceNewWindow?: boolean, path?: string, window?: ICodeWindow, data?: ITelemetryData): void; pickFolderAndOpen(forceNewWindow?: boolean, window?: ICodeWindow, data?: ITelemetryData): void; + pickFolder(): TPromise; focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow; getLastActiveWindow(): ICodeWindow; findWindow(workspacePath: string, filePath?: string, extensionDevelopmentPath?: string): ICodeWindow; diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 1e5bc4b4a2d..0d8b8d0f088 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -67,6 +67,10 @@ export class WindowsService implements IWindowsService, IDisposable { return TPromise.as(null); } + pickFolder(): TPromise { + return this.windowsMainService.pickFolder(); + } + reloadWindow(windowId: number): TPromise { const codeWindow = this.windowsMainService.getWindowById(windowId); diff --git a/src/vs/workbench/browser/actions/fileActions.ts b/src/vs/workbench/browser/actions/fileActions.ts index d4f21f16f58..7ef5595dc8d 100644 --- a/src/vs/workbench/browser/actions/fileActions.ts +++ b/src/vs/workbench/browser/actions/fileActions.ts @@ -10,6 +10,8 @@ import { Action } from 'vs/base/common/actions'; import nls = require('vs/nls'); import { IWindowService } from 'vs/platform/windows/common/windows'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceEditingService } from "vs/workbench/services/workspace/common/workspaceEditing"; +import URI from "vs/base/common/uri"; export class OpenFolderAction extends Action { @@ -46,3 +48,24 @@ export class OpenFileFolderAction extends Action { return this.windowService.pickFileFolderAndOpen(undefined, data); } } + +export class AddFolderAction extends Action { + + static ID = 'workbench.action.files.addFolder'; + static LABEL = nls.localize('addFolder', "Add Folder..."); + + constructor( + id: string, + label: string, + @IWindowService private windowService: IWindowService, + @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService + ) { + super(id, label); + } + + public run(event?: any, data?: ITelemetryData): TPromise { + return this.windowService.pickFolder().then(folders => { + this.workspaceEditingService.addRoots(folders.map(folder => URI.file(folder))); + }); + } +} \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 6f3e80c62ef..4b0b4b861b5 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -96,6 +96,8 @@ import { OpenRecentAction, ToggleDevToolsAction, ReloadWindowAction, inRecentFil import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingsRegistry } from "vs/platform/keybinding/common/keybindingsRegistry"; import { getQuickNavigateHandler, inQuickOpenContext } from "vs/workbench/browser/parts/quickopen/quickopen"; +import { IWorkspaceEditingService } from "vs/workbench/services/workspace/common/workspaceEditing"; +import { WorkspaceEditingService } from "vs/workbench/services/workspace/node/workspaceEditingService"; export const MessagesVisibleContext = new RawContextKey('globalMessageVisible', false); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); @@ -567,6 +569,9 @@ export class Workbench implements IPartService { this.configurationEditingService = this.instantiationService.createInstance(ConfigurationEditingService); serviceCollection.set(IConfigurationEditingService, this.configurationEditingService); + // Workspace Editing + serviceCollection.set(IWorkspaceEditingService, new SyncDescriptor(WorkspaceEditingService)); + // Keybinding Editing serviceCollection.set(IKeybindingEditingService, this.instantiationService.createInstance(KeybindingsEditingService)); diff --git a/src/vs/workbench/parts/files/browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/browser/fileActions.contribution.ts index 2364d54dead..1db898fbdb6 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.contribution.ts @@ -19,7 +19,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { FileStat } from 'vs/workbench/parts/files/common/explorerViewModel'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/fileActions'; +import { OpenFolderAction, OpenFileFolderAction, AddFolderAction } from 'vs/workbench/browser/actions/fileActions'; import { copyFocusedFilesExplorerViewItem, revealInOSFocusedFilesExplorerItem, openFocusedExplorerItemSideBySideCommand, copyPathOfFocusedExplorerItem, copyPathCommand, revealInExplorerCommand, revealInOSCommand, openFolderPickerCommand, openWindowCommand, openFileInNewWindowCommand, deleteFocusedFilesExplorerViewItemCommand, moveFocusedFilesExplorerViewItemToTrashCommand, renameFocusedFilesExplorerViewItemCommand } from 'vs/workbench/parts/files/browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -196,6 +196,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(SaveFileAsAction, Save registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalNewUntitledFileAction, GlobalNewUntitledFileAction.ID, GlobalNewUntitledFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_N }), 'Files: New Untitled File', category); registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalRevealInOSAction, GlobalRevealInOSAction.ID, GlobalRevealInOSAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_R) }), 'Files: Reveal Active File', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowOpenedFileInNewWindow, ShowOpenedFileInNewWindow.ID, ShowOpenedFileInNewWindow.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'Files: Open Active File in New Window', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(AddFolderAction, AddFolderAction.ID, AddFolderAction.LABEL), 'Files: Add Folder...', category); if (isMacintosh) { registry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileFolderAction, OpenFileFolderAction.ID, OpenFileFolderAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }), 'Files: Open...', category); diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts new file mode 100644 index 00000000000..0f72ed45d1c --- /dev/null +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import URI from "vs/base/common/uri"; + +export const IWorkspaceEditingService = createDecorator('workspaceEditingService'); + +export interface IWorkspaceEditingService { + + _serviceBrand: ServiceIdentifier; + + addRoots(roots: URI[]): TPromise; + + removeRoots(roots: URI[]): TPromise; + + clearRoots(): TPromise; +} \ No newline at end of file diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts new file mode 100644 index 00000000000..0231a517c5c --- /dev/null +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IWorkspaceEditingService } from "vs/workbench/services/workspace/common/workspaceEditing"; +import URI from "vs/base/common/uri"; +import { equals, distinct } from "vs/base/common/arrays"; +import { TPromise } from "vs/base/common/winjs.base"; +import { IWorkspaceContextService } from "vs/platform/workspace/common/workspace"; +import { IConfigurationEditingService, ConfigurationTarget } from "vs/workbench/services/configuration/common/configurationEditing"; +import { IConfigurationService } from "vs/platform/configuration/common/configuration"; + +interface IWorkspaceConfiguration { + [master: string]: { + folders: string[]; + }; +} + +const workspaceConfigKey = 'workspace'; + +export class WorkspaceEditingService implements IWorkspaceEditingService { + + public _serviceBrand: any; + + constructor( + @IConfigurationEditingService private configurationEditingService: IConfigurationEditingService, + @IConfigurationService private configurationService: IConfigurationService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { + } + + public addRoots(rootsToAdd: URI[]): TPromise { + if (!this.contextService.hasWorkspace()) { + return TPromise.as(void 0); // we need a workspace to begin with + } + + const roots = this.contextService.getWorkspace2().roots; + + return this.doSetRoots([...roots, ...rootsToAdd]); + } + + public removeRoots(rootsToRemove: URI[]): TPromise { + if (!this.contextService.hasWorkspace()) { + return TPromise.as(void 0); // we need a workspace to begin with + } + + const roots = this.contextService.getWorkspace2().roots; + const rootsToRemoveRaw = rootsToRemove.map(root => root.toString()); + + return this.doSetRoots(roots.filter(root => rootsToRemoveRaw.indexOf(root.toString()) === -1)); + } + + public clearRoots(roots: URI[]): TPromise { + if (!this.contextService.hasWorkspace()) { + return TPromise.as(void 0); // we need a workspace to begin with + } + + return this.doSetRoots([]); + } + + private doSetRoots(roots: URI[]): TPromise { + const workspaceUserConfig = this.configurationService.lookup(workspaceConfigKey).user as IWorkspaceConfiguration || Object.create(null); + const master = this.contextService.getWorkspace2().roots[0].toString(); + + const currentWorkspaceRoots = (workspaceUserConfig[master.toString()] && workspaceUserConfig[master.toString()].folders) || []; + const newWorkspaceRoots = distinct(roots.map(root => root.toString())); + + // Make sure we do not set the master folder as root + const masterIndex = newWorkspaceRoots.indexOf(master); + if (masterIndex >= 0) { + newWorkspaceRoots.splice(masterIndex, 1); + } + + // See if there are any changes + if (equals(currentWorkspaceRoots, newWorkspaceRoots)) { + return TPromise.as(void 0); + } + + // Apply to config + workspaceUserConfig[master.toString()] = { + folders: newWorkspaceRoots + }; + + return this.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: workspaceConfigKey, value: workspaceUserConfig }).then(() => void 0); + } +} \ No newline at end of file diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index e042cf4694b..610419280e9 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -854,6 +854,10 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } + pickFolder(): TPromise { + return TPromise.as([]); + } + reloadWindow(): TPromise { return TPromise.as(void 0); } @@ -964,96 +968,130 @@ export class TestWindowsService implements IWindowsService { pickFileFolderAndOpen(windowId: number, forceNewWindow?: boolean): TPromise { return TPromise.as(void 0); } + pickFileAndOpen(windowId: number, forceNewWindow?: boolean, path?: string): TPromise { return TPromise.as(void 0); } + pickFolderAndOpen(windowId: number, forceNewWindow?: boolean): TPromise { return TPromise.as(void 0); } + + pickFolder(): TPromise { + return TPromise.as([]); + } + reloadWindow(windowId: number): TPromise { return TPromise.as(void 0); } + openDevTools(windowId: number): TPromise { return TPromise.as(void 0); } + toggleDevTools(windowId: number): TPromise { return TPromise.as(void 0); } + // TODO@joao: rename, shouldn't this be closeWindow? closeFolder(windowId: number): TPromise { return TPromise.as(void 0); } + toggleFullScreen(windowId: number): TPromise { return TPromise.as(void 0); } + setRepresentedFilename(windowId: number, fileName: string): TPromise { return TPromise.as(void 0); } + addToRecentlyOpen(paths: { path: string, isFile?: boolean }[]): TPromise { return TPromise.as(void 0); } + removeFromRecentlyOpen(paths: string[]): TPromise { return TPromise.as(void 0); } + clearRecentPathsList(): TPromise { return TPromise.as(void 0); } + getRecentlyOpen(windowId: number): TPromise<{ files: string[]; folders: string[]; }> { return TPromise.as(void 0); } + focusWindow(windowId: number): TPromise { return TPromise.as(void 0); } + isMaximized(windowId: number): TPromise { return TPromise.as(void 0); } + maximizeWindow(windowId: number): TPromise { return TPromise.as(void 0); } + unmaximizeWindow(windowId: number): TPromise { return TPromise.as(void 0); } + onWindowTitleDoubleClick(windowId: number): TPromise { return TPromise.as(void 0); } + setDocumentEdited(windowId: number, flag: boolean): TPromise { return TPromise.as(void 0); } + quit(): TPromise { return TPromise.as(void 0); } + relaunch(options: { addArgs?: string[], removeArgs?: string[] }): TPromise { return TPromise.as(void 0); } + whenSharedProcessReady(): TPromise { return TPromise.as(void 0); } + toggleSharedProcess(): TPromise { return TPromise.as(void 0); } + // Global methods openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean }): TPromise { return TPromise.as(void 0); } + openNewWindow(): TPromise { return TPromise.as(void 0); } + showWindow(windowId: number): TPromise { return TPromise.as(void 0); } + getWindows(): TPromise<{ id: number; path: string; title: string; }[]> { return TPromise.as(void 0); } + getWindowCount(): TPromise { return TPromise.as(this.windowCount); } + log(severity: string, ...messages: string[]): TPromise { return TPromise.as(void 0); } + // TODO@joao: what? closeExtensionHostWindow(extensionDevelopmentPaths: string[]): TPromise { return TPromise.as(void 0); } + showItemInFolder(path: string): TPromise { return TPromise.as(void 0); } -- GitLab