diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 9daa60dc2c7c7ba3df4e149cfd54f4986a824014..aa8193ec54842c5d8ca8e610054790349e4016d5 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -66,6 +66,12 @@ export interface ISaveDialogOptions { * A human-readable string for the ok button */ saveLabel?: string; + + /** + * Specifies a list of schemas for the file systems the user can save to. If not specified, uses the schema of the defaultURI or, if also not specified, + * the schema of the current window. + */ + availableFileSystems?: string[]; } export interface IOpenDialogOptions { @@ -104,6 +110,12 @@ export interface IOpenDialogOptions { * like "TypeScript", and an array of extensions. */ filters?: FileFilter[]; + + /** + * Specifies a list of schemas for the file systems the user can load from. If not specified, uses the schema of the defaultURI or, if also not available, + * the schema of the current window. + */ + availableFileSystems?: string[]; } @@ -150,21 +162,21 @@ export interface IFileDialogService { /** * The default path for a new file based on previously used files. - * @param schemeFilter The scheme of the file path. + * @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used. */ - defaultFilePath(schemeFilter: string): URI | undefined; + defaultFilePath(schemeFilter?: string): URI | undefined; /** * The default path for a new folder based on previously used folders. - * @param schemeFilter The scheme of the folder path. + * @param schemeFilter The scheme of the folder path. If no filter given, the scheme of the current window is used. */ - defaultFolderPath(schemeFilter: string): URI | undefined; + defaultFolderPath(schemeFilter?: string): URI | undefined; /** * The default path for a new workspace based on previously used workspaces. - * @param schemeFilter The scheme of the workspace path. + * @param schemeFilter The scheme of the workspace path. If no filter given, the scheme of the current window is used. */ - defaultWorkspacePath(schemeFilter: string): URI | undefined; + defaultWorkspacePath(schemeFilter?: string): URI | undefined; /** * Shows a file-folder selection dialog and opens the selected entry. diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index a8847e2cbb7eb23f935a620891fed3577ee40919..4520821cde9036c7dcb3db50a7308b9fb899435c 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -15,7 +15,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ICommandService } from 'vs/platform/commands/common/commands'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -161,7 +160,7 @@ export class SaveWorkspaceAsAction extends Action { saveLabel: mnemonicButtonLabel(nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), title: nls.localize('saveWorkspace', "Save Workspace"), filters: WORKSPACE_FILTER, - defaultUri: this.dialogService.defaultWorkspacePath(Schemas.file) + defaultUri: this.dialogService.defaultWorkspacePath() }); } } diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index 95c6eba1bcb9084366d3a946ae5ea4111445582a..b124ab902bc83e89eae675c5f3d060751bc7ec55 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -18,7 +18,6 @@ import { IQuickInputService, IPickOptions, IQuickPickItem } from 'vs/platform/qu import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { Schemas } from 'vs/base/common/network'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; export const ADD_ROOT_FOLDER_COMMAND_ID = 'addRootFolder'; @@ -64,7 +63,7 @@ CommandsRegistry.registerCommand({ title: nls.localize('addFolderToWorkspaceTitle', "Add Folder to Workspace"), canSelectFolders: true, canSelectMany: true, - defaultUri: dialogsService.defaultFolderPath(Schemas.file) + defaultUri: dialogsService.defaultFolderPath() }).then((folders): Promise | null => { if (!folders || !folders.length) { return null; diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 7700d71be143440512ca45b7e41a7ec0f0e07a2c..8771834095dc057a02928f36cc3548f825429f74 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -176,7 +176,7 @@ import { ToggleDevToolsAction } from 'vs/workbench/electron-browser/actions/deve import { registerWindowDriver } from 'vs/platform/driver/electron-browser/driver'; import { IExtensionUrlHandler, ExtensionUrlHandler } from 'vs/workbench/services/extensions/electron-browser/inactiveExtensionUrlHandler'; import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; -import { RemoteFileDialogService, DialogService } from 'vs/workbench/services/dialogs/electron-browser/dialogService'; +import { DialogService, FileDialogService } from 'vs/workbench/services/dialogs/electron-browser/dialogService'; import { ShowPreviousWindowTab, MoveWindowTabToNewWindow, MergeAllWindowTabs, ShowNextWindowTab, ToggleWindowTabsBar, NewWindowTab, OpenRecentAction, ReloadWindowAction, ReloadWindowWithExtensionsDisabledAction } from 'vs/workbench/electron-browser/actions/windowActions'; import { IBroadcastService, BroadcastService } from 'vs/workbench/services/broadcast/electron-browser/broadcastService'; import { WindowService } from 'vs/platform/windows/electron-browser/windowService'; @@ -716,7 +716,7 @@ export class Workbench extends Disposable implements IPartService { serviceCollection.set(IHistoryService, new SyncDescriptor(HistoryService)); // File Dialogs - serviceCollection.set(IFileDialogService, new SyncDescriptor(RemoteFileDialogService)); + serviceCollection.set(IFileDialogService, new SyncDescriptor(FileDialogService)); // Backup File Service if (this.workbenchParams.configuration.backupPath) { diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index dd4833fdec19b53256d486b774a5f5ccf7b9c322..769835433d6fc2d2fbddd16e18896f35883bf3d0 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import product from 'vs/platform/node/product'; import Severity from 'vs/base/common/severity'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import { IWindowService, INativeOpenDialogOptions, OpenDialogOptions } from 'vs/platform/windows/common/windows'; +import { IWindowService, INativeOpenDialogOptions, OpenDialogOptions, IURIToOpen, FileFilter } from 'vs/platform/windows/common/windows'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ILogService } from 'vs/platform/log/common/log'; @@ -15,11 +15,12 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; -import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { Schemas } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { RemoteFileDialog } from 'vs/workbench/services/dialogs/electron-browser/remoteFileDialog'; +import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; interface IMassagedMessageBoxOptions { @@ -163,10 +164,10 @@ export class FileDialogService implements IFileDialogService { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IHistoryService private readonly historyService: IHistoryService, @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { } - defaultFilePath(schemeFilter: string): URI | undefined { + defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined { // Check for last active file first... let candidate = this.historyService.getLastActiveFile(schemeFilter); @@ -179,7 +180,7 @@ export class FileDialogService implements IFileDialogService { return candidate && resources.dirname(candidate) || undefined; } - defaultFolderPath(schemeFilter: string): URI | undefined { + defaultFolderPath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined { // Check for last active file root first... let candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter); @@ -192,7 +193,7 @@ export class FileDialogService implements IFileDialogService { return candidate && resources.dirname(candidate) || undefined; } - defaultWorkspacePath(schemeFilter: string): URI | undefined { + defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined { // Check for current workspace config file first... if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { @@ -217,36 +218,66 @@ export class FileDialogService implements IFileDialogService { } pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise { - const defaultUri = options.defaultUri; - if (!defaultUri) { - options.defaultUri = this.defaultFilePath(Schemas.file); + const schema = this.getFileSystemSchema(options); + + if (!options.defaultUri) { + options.defaultUri = this.defaultFilePath(schema); + } + + if (schema !== Schemas.file) { + const title = nls.localize('openFileOrFolder.title', 'Open File Or Folder'); + const availableFileSystems = [schema, Schemas.file]; // always allow file as well + return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, options.forceNewWindow, true); } return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options)); } pickFileAndOpen(options: IPickAndOpenOptions): Promise { - const defaultUri = options.defaultUri; - if (!defaultUri) { - options.defaultUri = this.defaultFilePath(Schemas.file); + const schema = this.getFileSystemSchema(options); + + if (!options.defaultUri) { + options.defaultUri = this.defaultFilePath(schema); + } + + if (schema !== Schemas.file) { + const title = nls.localize('openFile.title', 'Open File'); + const availableFileSystems = [schema, Schemas.file]; // always allow file as well + return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, options.forceNewWindow, true); } return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options)); } pickFolderAndOpen(options: IPickAndOpenOptions): Promise { - const defaultUri = options.defaultUri; - if (!defaultUri) { - options.defaultUri = this.defaultFolderPath(Schemas.file); + const schema = this.getFileSystemSchema(options); + + if (!options.defaultUri) { + options.defaultUri = this.defaultFolderPath(schema); + } + + if (schema !== Schemas.file) { + const title = nls.localize('openFolder.title', 'Open Folder'); + const availableFileSystems = [schema, Schemas.file]; // always allow file as well + return this.pickRemoteResourceAndOpen({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, options.forceNewWindow, false); } return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options)); } pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise { - const defaultUri = options.defaultUri; - if (!defaultUri) { - options.defaultUri = this.defaultWorkspacePath(Schemas.file); + const schema = this.getFileSystemSchema(options); + + if (!options.defaultUri) { + options.defaultUri = this.defaultWorkspacePath(schema); + } + + if (schema !== Schemas.file) { + const title = nls.localize('openWorkspace.title', 'Open Workspace'); + const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }]; + const availableFileSystems = [schema, Schemas.file]; // always allow file as well + return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }, options.forceNewWindow, false); + } return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options)); @@ -262,9 +293,9 @@ export class FileDialogService implements IFileDialogService { } showSaveDialog(options: ISaveDialogOptions): Promise { - const defaultUri = options.defaultUri; - if (defaultUri && defaultUri.scheme !== Schemas.file) { - return Promise.reject(new Error('Not supported - Save-dialogs can only be opened on `file`-uris.')); + const schema = this.getFileSystemSchema(options); + if (schema !== Schemas.file) { + return this.saveRemoteResource(options); } return this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options)).then(result => { @@ -276,17 +307,16 @@ export class FileDialogService implements IFileDialogService { }); } - public showSaveRemoteDialog(options: ISaveDialogOptions): Promise { - const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog); - return remoteFileDialog.showSaveDialog(options); - } - showOpenDialog(options: IOpenDialogOptions): Promise { - const defaultUri = options.defaultUri; - if (defaultUri && defaultUri.scheme !== Schemas.file) { - return Promise.reject(new Error('Not supported - Open-dialogs can only be opened on `file`-uris.')); + const schema = this.getFileSystemSchema(options); + if (schema !== Schemas.file) { + return this.pickRemoteResource(options).then(urisToOpen => { + return urisToOpen && urisToOpen.map(uto => uto.uri); + }); } + const defaultUri = options.defaultUri; + const newOptions: OpenDialogOptions = { title: options.title, defaultPath: defaultUri && defaultUri.fsPath, @@ -312,31 +342,33 @@ export class FileDialogService implements IFileDialogService { return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined); } - public showOpenRemoteDialog(options: IOpenDialogOptions): Promise { + private pickRemoteResourceAndOpen(options: IOpenDialogOptions, forceNewWindow: boolean, forceOpenWorkspaceAsFile: boolean) { + return this.pickRemoteResource(options).then(urisToOpen => { + if (urisToOpen) { + return this.windowService.openWindow(urisToOpen, { forceNewWindow, forceOpenWorkspaceAsFile }); + } + return void 0; + }); + } + + private pickRemoteResource(options: IOpenDialogOptions): Promise { const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog); return remoteFileDialog.showOpenDialog(options); } -} -export class RemoteFileDialogService extends FileDialogService { + private saveRemoteResource(options: ISaveDialogOptions): Promise { + const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog); + return remoteFileDialog.showSaveDialog(options); + } - constructor( - @IWindowService windowService: IWindowService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IHistoryService historyService: IHistoryService, - @IEnvironmentService environmentService: IEnvironmentService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(windowService, contextService, historyService, environmentService, instantiationService); + private getSchemeFilterForWindow() { + return !this.windowService.getConfiguration().remoteAuthority ? Schemas.file : REMOTE_HOST_SCHEME; } - public showSaveDialog(options: ISaveDialogOptions): Promise { - const defaultUri = options.defaultUri; - if (defaultUri && defaultUri.scheme === REMOTE_HOST_SCHEME) { - return this.showSaveRemoteDialog(options); - } - return super.showSaveDialog(options); + private getFileSystemSchema(options: { availableFileSystems?: string[], defaultUri?: URI }): string { + return options.availableFileSystems && options.availableFileSystems[0] || options.defaultUri && options.defaultUri.scheme || this.getSchemeFilterForWindow(); } + } function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean { diff --git a/src/vs/workbench/services/dialogs/electron-browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/electron-browser/remoteFileDialog.ts index cb03268fdc2aaa4eec5e828c18c38b810620e545..6c53cf72a356eac876ae4f5096b7d93e9a7b6077 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/remoteFileDialog.ts @@ -12,10 +12,9 @@ import { URI } from 'vs/base/common/uri'; import { isWindows } from 'vs/base/common/platform'; import { ISaveDialogOptions, IOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; -import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IWindowService, IURIToOpen } from 'vs/platform/windows/common/windows'; import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { INotificationService } from 'vs/platform/notification/common/notification'; interface FileQuickPickItem extends IQuickPickItem { @@ -43,40 +42,35 @@ export class RemoteFileDialog { @IWindowService private readonly windowService: IWindowService, @ILabelService private readonly labelService: ILabelService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IEditorService private readonly editorService: IEditorService, @INotificationService private readonly notificationService: INotificationService, ) { this.remoteAuthority = this.windowService.getConfiguration().remoteAuthority; } - public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { - if (!this.remoteAuthority) { - this.notificationService.info(nls.localize('remoteFileDialog.notConnectedToRemote', 'Not connected to a remote.')); - return Promise.resolve(); - } + public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { const defaultUri = options.defaultUri ? options.defaultUri : URI.from({ scheme: REMOTE_HOST_SCHEME, authority: this.remoteAuthority, path: '/' }); + if (!this.remoteFileService.canHandleResource(defaultUri)) { + this.notificationService.info(nls.localize('remoteFileDialog.notConnectedToRemote', 'File system provider for {0} is not available.', defaultUri.toString())); + return Promise.resolve(undefined); + } + const title = nls.localize('remoteFileDialog.openTitle', 'Open File or Folder'); return this.pickResource({ title, defaultUri, canSelectFiles: true, canSelectFolders: true }).then(async fileFolderUri => { if (fileFolderUri) { const stat = await this.remoteFileService.resolveFile(fileFolderUri); - if (stat.isDirectory) { - return this.windowService.openWindow([{ uri: fileFolderUri, typeHint: 'folder' }]); - } else { - return this.editorService.openEditor({ resource: fileFolderUri }).then(() => { - return Promise.resolve(); - }); - } + return [{ uri: fileFolderUri, type: stat.isDirectory ? 'folder' : 'file' }]; } - return Promise.resolve(); + return Promise.resolve(undefined); }); } public showSaveDialog(options: ISaveDialogOptions): Promise { - if (!this.remoteAuthority) { - this.notificationService.info(nls.localize('remoteFileDialog.notConnectedToRemote', 'Not connected to a remote.')); + const defaultUri = options.defaultUri ? options.defaultUri : URI.from({ scheme: REMOTE_HOST_SCHEME, authority: this.remoteAuthority, path: '/' }); + if (!this.remoteFileService.canHandleResource(defaultUri)) { + this.notificationService.info(nls.localize('remoteFileDialog.notConnectedToRemote', 'File system provider for {0} is not available.', defaultUri.toString())); return Promise.resolve(undefined); } - const defaultUri = options.defaultUri ? options.defaultUri : URI.from({ scheme: REMOTE_HOST_SCHEME, authority: this.remoteAuthority, path: '/' }); + return new Promise((resolve) => { let saveNameBox = this.quickInputService.createInputBox(); saveNameBox.title = options.title; diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 8b51c7ab149c0775834042d11f34c6a42eebca2a..cd64deb76e8a89cd84c7592c280e6099503b1c51 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -412,13 +412,13 @@ export class TestFileDialogService implements IFileDialogService { public _serviceBrand: any; - public defaultFilePath(_schemeFilter: string): URI | undefined { + public defaultFilePath(_schemeFilter?: string): URI | undefined { return undefined; } - public defaultFolderPath(_schemeFilter: string): URI | undefined { + public defaultFolderPath(_schemeFilter?: string): URI | undefined { return undefined; } - public defaultWorkspacePath(_schemeFilter: string): URI | undefined { + public defaultWorkspacePath(_schemeFilter?: string): URI | undefined { return undefined; } public pickFileFolderAndOpen(_options: IPickAndOpenOptions): Promise {