diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index ad076013e03..db901ed9e5e 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1212,3 +1212,18 @@ export function asCSSUrl(uri: URI): string { } return `url('${asDomUri(uri).toString(true).replace(/'/g, '%27')}')`; } + +export function triggerDownload(uri: URI, name: string): void { + // In order to download from the browser, the only way seems + // to be creating a element with download attribute that + // points to the file to download. + // See also https://developers.google.com/web/updates/2011/08/Downloading-resources-in-HTML5-a-download + const anchor = document.createElement('a'); + document.body.appendChild(anchor); + anchor.download = name; + anchor.href = uri.toString(true); + anchor.click(); + + // Ensure to remove the element from DOM eventually + setTimeout(() => document.body.removeChild(anchor)); +} diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 313a667124d..3013c1eaa39 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler } from 'vs/workbench/contrib/files/browser/fileActions'; +import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL } from 'vs/workbench/contrib/files/browser/fileActions'; import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/saveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; @@ -14,7 +14,7 @@ import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService, ExplorerResourceMoveableToTrash, ExplorerViewletVisibleContext } from 'vs/workbench/contrib/files/common/files'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from 'vs/workbench/browser/actions/workspaceCommands'; import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; @@ -23,7 +23,7 @@ import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { IsWebContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; +import { WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ActiveEditorIsSaveableContext } from 'vs/workbench/common/editor'; @@ -218,7 +218,6 @@ export function appendToCommandPalette(id: string, title: ILocalizedString, cate }); } -const downloadLabel = nls.localize('download', "Download"); appendToCommandPalette(COPY_PATH_COMMAND_ID, { value: nls.localize('copyPathOfActive', "Copy Path of Active File"), original: 'Copy Path of Active File' }, category); appendToCommandPalette(COPY_RELATIVE_PATH_COMMAND_ID, { value: nls.localize('copyRelativePathOfActive', "Copy Relative Path of Active File"), original: 'Copy Relative Path of Active File' }, category); appendToCommandPalette(SAVE_FILE_COMMAND_ID, { value: SAVE_FILE_LABEL, original: 'Save' }, category); @@ -231,7 +230,7 @@ appendToCommandPalette(SAVE_FILE_AS_COMMAND_ID, { value: SAVE_FILE_AS_LABEL, ori appendToCommandPalette(CLOSE_EDITOR_COMMAND_ID, { value: nls.localize('closeEditor', "Close Editor"), original: 'Close Editor' }, { value: nls.localize('view', "View"), original: 'View' }); appendToCommandPalette(NEW_FILE_COMMAND_ID, { value: NEW_FILE_LABEL, original: 'New File' }, category, WorkspaceFolderCountContext.notEqualsTo('0')); appendToCommandPalette(NEW_FOLDER_COMMAND_ID, { value: NEW_FOLDER_LABEL, original: 'New Folder' }, category, WorkspaceFolderCountContext.notEqualsTo('0')); -appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: downloadLabel, original: 'Download' }, category, ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file))); +appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: DOWNLOAD_LABEL, original: 'Download' }, category, ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file))); // Menu registration - open editors @@ -465,15 +464,24 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { when: ExplorerFolderContext }); -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: '5_cutcopypaste', - order: 30, - command: { - id: DOWNLOAD_COMMAND_ID, - title: downloadLabel, - }, - when: ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file)) -}); +MenuRegistry.appendMenuItem(MenuId.ExplorerContext, (() => { + const downloadMenuItem = { + group: '5_cutcopypaste', + order: 30, + command: { + id: DOWNLOAD_COMMAND_ID, + title: DOWNLOAD_LABEL, + }, + when: ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file)) + }; + + // Web: currently not supporting download of folders + if (isWeb) { + downloadMenuItem.when = ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file), ExplorerFolderContext.toNegated()); + } + + return downloadMenuItem; +})()); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { group: '6_copypath', diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 51321546420..cc1be00aadd 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/fileactions'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; -import { isWindows } from 'vs/base/common/platform'; +import { isWindows, isWeb } from 'vs/base/common/platform'; import * as extpath from 'vs/base/common/extpath'; import { extname, basename } from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; @@ -45,6 +45,8 @@ import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { onUnexpectedError, getErrorMessage } from 'vs/base/common/errors'; +import { asDomUri, triggerDownload } from 'vs/base/browser/dom'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -62,6 +64,8 @@ export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste"); export const FileCopiedContext = new RawContextKey('fileCopied', false); +export const DOWNLOAD_LABEL = nls.localize('download', "Download"); + const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; function onError(notificationService: INotificationService, error: any): void { @@ -1050,11 +1054,25 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { if (explorerContext.stat) { const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; stats.forEach(async s => { - const destination = await fileDialogService.showSaveDialog({ - availableFileSystems: [Schemas.file] - }); - if (destination) { - await fileService.copy(s.resource, destination); + if (isWeb) { + if (!s.isDirectory) { + triggerDownload(asDomUri(s.resource), s.name); + } + } else { + let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath() : fileDialogService.defaultFilePath(); + if (defaultUri && !s.isDirectory) { + defaultUri = resources.joinPath(defaultUri, s.name); + } + + const destination = await fileDialogService.showSaveDialog({ + availableFileSystems: [Schemas.file], + saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")), + title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), + defaultUri + }); + if (destination) { + await fileService.copy(s.resource, destination); + } } }); }